Skip to content

Commit

Permalink
Yaml attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
anpetroc committed Dec 4, 2023
1 parent 0938da7 commit cf3dcd9
Show file tree
Hide file tree
Showing 10 changed files with 472 additions and 21 deletions.
19 changes: 19 additions & 0 deletions src/PAModel/PAConvert/Yaml/Attributes/YamlObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics;

namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml;

/// <summary>
/// Specifies that a object should be serialized to YAML.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
[DebuggerDisplay("{Name}")]
public class YamlObject : Attribute
{
/// <summary>
/// Name of the object in the YAML file.
/// </summary>
public string Name { get; set; }
}
49 changes: 49 additions & 0 deletions src/PAModel/PAConvert/Yaml/Attributes/YamlProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics;

namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml;

/// <summary>
/// Specifies that a property should be serialized to YAML.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[DebuggerDisplay("{Name}")]
public class YamlProperty : Attribute, IComparable<YamlProperty>
{
/// <summary>
/// Name of the property in the YAML file.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Order in which the property should be serialized.
/// If not specified, the property will be serialized last.
/// </summary>
public int Order { get; init; } = int.MaxValue;

/// <summary>
/// Default value of the property.
/// </summary>
public object DefaultValue { get; set; }

/// <summary>
/// Compare the order of this property to another.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(YamlProperty other)
{
if (other == null)
return 1;

if (Order != other.Order)
return Order.CompareTo(other.Order);

if (Name == null)
return -1;

return Name.CompareTo(other.Name);
}
}
32 changes: 22 additions & 10 deletions src/PAModel/PAConvert/Yaml/YamlLexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ internal class YamlLexer : IDisposable

// for error handling
private readonly string _currentFileName;
private int _currentLine; // 1-based line number

private string _currentLineContents = null;

Expand Down Expand Up @@ -159,6 +158,14 @@ public YamlLexer(TextReader source, string filenameHint = null)
};
}

private int _currentLine;
/// <summary>
/// Current line number. 1-based.
/// </summary>
public int CurrentLine => _currentLine;

public YamlLexerOptions Options { get; set; } = YamlLexerOptions.EnforceLeadingEquals;

private LineParser PeekLine()
{
if (_currentLineContents == null)
Expand Down Expand Up @@ -374,9 +381,10 @@ private YamlToken ReadNextWorker()
iSpaces++; // skip optional spaces
}

YamlToken error;
if (line.Current == 0) // EOL
{
var error = _currentIndent.Peek().CheckDuplicate(propName, _currentLine);
error = _currentIndent.Peek().CheckDuplicate(propName, _currentLine);
if (error != null)
{
error.Span = Loc(line);
Expand Down Expand Up @@ -487,19 +495,23 @@ private YamlToken ReadNextWorker()

value = value.Substring(1); // move past '='
}
else
else if (Options.HasFlag(YamlLexerOptions.EnforceLeadingEquals))
{
// Warn on legal yaml escapes (>) that we don't support in our subset here.
return Error(line, "Expected either '=' for a single line expression or '|' to begin a multiline expression");
}
else
{
// Single line. Property doesn't include \n at end.
value = line.RestOfLine;
MoveNextLine();
}

error = _currentIndent.Peek().CheckDuplicate(propName, _currentLine);
if (error != null)
{
var error = _currentIndent.Peek().CheckDuplicate(propName, _currentLine);
if (error != null)
{
error.Span = Loc(line);
return error;
}
error.Span = Loc(line);
return error;
}

int endIndex = line._line.Length + 1;
Expand Down Expand Up @@ -697,7 +709,7 @@ public int EatIndent()
private string DebuggerToString()
{
var idx = Math.Min(_line.Length, this._idx);
return _line.Substring(0, idx) + "|" + _line.Substring(idx);
return _line.Substring(0, idx) + "¤" + _line.Substring(idx);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/PAModel/PAConvert/Yaml/YamlLexerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml;

[Flags]
public enum YamlLexerOptions
{
None = 0,
EnforceLeadingEquals = 1
}
12 changes: 12 additions & 0 deletions src/PAModel/PAConvert/Yaml/YamlParseException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml;

public class YamlParseException : Exception
{
public YamlParseException(string message, int line, Exception innerException = null)
: base(message, innerException)
{
Line = line;
}

public int Line { get; init; }
}
123 changes: 123 additions & 0 deletions src/PAModel/PAConvert/Yaml/YamlPocoDeserializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections;
using System.IO;
using System.Linq;
using System.Reflection;
using YamlDotNet.Serialization;

namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml;

/// <summary>
/// Deserializer from Yaml
/// </summary>
public class YamlPocoDeserializer : IDisposable
{
YamlLexer _yamlLexer;
private bool _isDisposed;

public YamlPocoDeserializer()
{
}

public YamlPocoDeserializer(Stream stream)
{
_ = stream ?? throw new ArgumentNullException(nameof(stream));
_yamlLexer = new YamlLexer(new StreamReader(stream));
}

public YamlPocoDeserializer(TextReader reader)
{
_ = reader ?? throw new ArgumentNullException(nameof(reader));
_yamlLexer = new YamlLexer(reader);
}

public YamlLexerOptions Options
{
get => _yamlLexer.Options;
set => _yamlLexer.Options = value;
}

public T Deserialize<T>(string name = null)
{
var result = Activator.CreateInstance<T>();

// Read the object name.
var objType = typeof(T);
var yamlObject = objType.GetCustomAttribute<YamlObject>() ?? new YamlObject() { Name = objType.Name };
if (string.IsNullOrWhiteSpace(yamlObject.Name))
yamlObject.Name = string.IsNullOrWhiteSpace(name) ? objType.Name : name;

// Build dictionary of expected properties that have the YamlProperty attribute.
var yamlProps = new Dictionary<string, (PropertyInfo info, YamlProperty attr)>();
foreach (var prop in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(prop => Attribute.IsDefined(prop, typeof(YamlProperty))))
{
var yamlProperty = prop.GetCustomAttribute<YamlProperty>();
if (string.IsNullOrWhiteSpace(yamlProperty.Name))
yamlProperty.Name = prop.Name;
yamlProps.Add(yamlProperty.Name, (prop, yamlProperty));
}

// Stream must start with expected object.
YamlToken token;
token = _yamlLexer.ReadNext();
if (token.Kind != YamlTokenKind.StartObj)
{
if (token.Kind == YamlTokenKind.Error)
throw new YamlParseException(token.Value, _yamlLexer.CurrentLine);
else
throw new YamlParseException($"Expected '{YamlTokenKind.StartObj}', found '{token.Kind}'", _yamlLexer.CurrentLine);
}
if (!token.Property.Equals(yamlObject.Name))
throw new YamlParseException($"Expected '{yamlObject.Name}', found '{token.Property}'", _yamlLexer.CurrentLine);

// Parse the stream.
while ((token = _yamlLexer.ReadNext()) != YamlToken.EndObj)
{
if (token.Kind == YamlTokenKind.Property)
{
if (yamlProps.TryGetValue(token.Property, out var propInfo))
{
try
{
var typedValue = Convert.ChangeType(token.Value, propInfo.info.PropertyType);
propInfo.info.SetValue(result, typedValue);
}
catch (Exception ex) when (ex is FormatException || ex is InvalidCastException || ex is OverflowException)
{
throw new YamlParseException($"Error parsing property '{token.Property}'", _yamlLexer.CurrentLine, ex);
}
}
}
}

return result;
}

#region IDisposable

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_yamlLexer?.Dispose();
_yamlLexer = null;
}

_isDisposed = true;
}
}

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

#endregion
}
Loading

0 comments on commit cf3dcd9

Please sign in to comment.