Compare updated template evaluator (#4092)

This commit is contained in:
eric sciple
2025-11-07 14:18:52 -06:00
committed by GitHub
parent 53d69ff441
commit b5b7986cd6
188 changed files with 27222 additions and 4 deletions
@@ -0,0 +1,57 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class BooleanDefinition : ScalarDefinition
{
internal BooleanDefinition()
{
}
internal BooleanDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Boolean:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Boolean}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Boolean} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Boolean} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Boolean;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is BooleanToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// Defines the allowable schema for a user defined type
/// </summary>
internal abstract class Definition
{
protected Definition()
{
}
protected Definition(MappingToken definition)
{
for (var i = 0; i < definition.Count; )
{
var definitionKey = definition[i].Key.AssertString($"{TemplateConstants.Definition} key");
if (String.Equals(definitionKey.Value, TemplateConstants.Context, StringComparison.Ordinal))
{
var context = definition[i].Value.AssertSequence($"{TemplateConstants.Context}");
definition.RemoveAt(i);
var readerContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
var evaluatorContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
foreach (TemplateToken item in context)
{
var itemStr = item.AssertString($"{TemplateConstants.Context} item").Value;
readerContext.Add(itemStr);
// Remove min/max parameter info
var paramIndex = itemStr.IndexOf('(');
if (paramIndex > 0)
{
evaluatorContext.Add(String.Concat(itemStr.Substring(0, paramIndex + 1), ")"));
}
else
{
evaluatorContext.Add(itemStr);
}
}
ReaderContext = readerContext.ToArray();
EvaluatorContext = evaluatorContext.ToArray();
}
else if (String.Equals(definitionKey.Value, TemplateConstants.Description, StringComparison.Ordinal))
{
definition.RemoveAt(i);
}
else
{
i++;
}
}
}
internal abstract DefinitionType DefinitionType { get; }
/// <summary>
/// Used by the template reader to determine allowed expression values and functions.
/// Also used by the template reader to validate function min/max parameters.
/// </summary>
internal String[] ReaderContext { get; private set; } = new String[0];
/// <summary>
/// Used by the template evaluator to determine allowed expression values and functions.
/// The min/max parameter info is omitted.
/// </summary>
internal String[] EvaluatorContext { get; private set; } = new String[0];
internal abstract void Validate(
TemplateSchema schema,
String name);
}
}
@@ -0,0 +1,16 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal enum DefinitionType
{
Null,
Boolean,
Number,
String,
Sequence,
Mapping,
OneOf,
AllowedValues,
}
}
@@ -0,0 +1,111 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class MappingDefinition : Definition
{
internal MappingDefinition()
{
}
internal MappingDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Mapping:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Mapping}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} key");
switch (mappingKey.Value)
{
case TemplateConstants.Properties:
var properties = mappingPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties}");
foreach (var propertiesPair in properties)
{
var propertyName = propertiesPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties} key");
Properties.Add(propertyName.Value, new PropertyDefinition(propertiesPair.Value));
}
break;
case TemplateConstants.LooseKeyType:
var looseKeyType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.LooseKeyType}");
LooseKeyType = looseKeyType.Value;
break;
case TemplateConstants.LooseValueType:
var looseValueType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.LooseValueType}");
LooseValueType = looseValueType.Value;
break;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Mapping;
internal String LooseKeyType { get; set; }
internal String LooseValueType { get; set; }
internal Dictionary<String, PropertyDefinition> Properties { get; } = new Dictionary<String, PropertyDefinition>(StringComparer.Ordinal);
internal override void Validate(
TemplateSchema schema,
String name)
{
// Lookup loose key type
if (!String.IsNullOrEmpty(LooseKeyType))
{
schema.GetDefinition(LooseKeyType);
// Lookup loose value type
if (!String.IsNullOrEmpty(LooseValueType))
{
schema.GetDefinition(LooseValueType);
}
else
{
throw new ArgumentException($"Property '{TemplateConstants.LooseKeyType}' is defined but '{TemplateConstants.LooseValueType}' is not defined on '{name}'");
}
}
// Otherwise validate loose value type not be defined
else if (!String.IsNullOrEmpty(LooseValueType))
{
throw new ArgumentException($"Property '{TemplateConstants.LooseValueType}' is defined but '{TemplateConstants.LooseKeyType}' is not defined");
}
// Lookup each property
foreach (var property in Properties)
{
if (String.IsNullOrEmpty(property.Value.Type))
{
throw new ArgumentException($"Type not specified for the '{property.Key}' property on the '{name}' type");
}
schema.GetDefinition(property.Value.Type);
}
}
}
}
@@ -0,0 +1,58 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class NullDefinition : ScalarDefinition
{
internal NullDefinition()
{
}
internal NullDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Null:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Null}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Null} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Null} key");
break;
}
}
break;
case TemplateConstants.AllowedValues:
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Null;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is NullToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,57 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class NumberDefinition : ScalarDefinition
{
internal NumberDefinition()
{
}
internal NumberDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Number:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Number}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Number} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Number} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Number;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is NumberToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,191 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// Must resolve to exactly one of the referenced definitions
/// </summary>
internal sealed class OneOfDefinition : Definition
{
internal OneOfDefinition()
{
}
internal OneOfDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.OneOf:
var oneOf = definitionPair.Value.AssertSequence(TemplateConstants.OneOf);
foreach (var oneOfItem in oneOf)
{
var reference = oneOfItem.AssertString(TemplateConstants.OneOf);
OneOf.Add(reference.Value);
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.OneOf;
internal List<String> OneOf { get; } = new List<String>();
internal override void Validate(
TemplateSchema schema,
String name)
{
if (OneOf.Count == 0)
{
throw new ArgumentException($"'{name}' does not contain any references");
}
var foundLooseKeyType = false;
var mappingDefinitions = default(List<MappingDefinition>);
var sequenceDefinition = default(SequenceDefinition);
var nullDefinition = default(NullDefinition);
var booleanDefinition = default(BooleanDefinition);
var numberDefinition = default(NumberDefinition);
var stringDefinitions = default(List<StringDefinition>);
foreach (var nestedType in OneOf)
{
var nestedDefinition = schema.GetDefinition(nestedType);
if (nestedDefinition.ReaderContext.Length > 0)
{
throw new ArgumentException($"'{name}' is a one-of definition and references another definition that defines context. This is currently not supported.");
}
if (nestedDefinition is MappingDefinition mappingDefinition)
{
if (mappingDefinitions == null)
{
mappingDefinitions = new List<MappingDefinition>();
}
mappingDefinitions.Add(mappingDefinition);
if (!String.IsNullOrEmpty(mappingDefinition.LooseKeyType))
{
foundLooseKeyType = true;
}
}
else if (nestedDefinition is SequenceDefinition s)
{
// Multiple sequence definitions not allowed
if (sequenceDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Sequence}'");
}
sequenceDefinition = s;
}
else if (nestedDefinition is NullDefinition n)
{
// Multiple sequence definitions not allowed
if (nullDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Null}'");
}
nullDefinition = n;
}
else if (nestedDefinition is BooleanDefinition b)
{
// Multiple boolean definitions not allowed
if (booleanDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Boolean}'");
}
booleanDefinition = b;
}
else if (nestedDefinition is NumberDefinition num)
{
// Multiple number definitions not allowed
if (numberDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Number}'");
}
numberDefinition = num;
}
else if (nestedDefinition is StringDefinition stringDefinition)
{
// First string definition
if (stringDefinitions == null)
{
stringDefinitions = new List<StringDefinition>();
}
// Multiple string definitions, all must be 'Constant'
else if ((stringDefinitions.Count == 1 && String.IsNullOrEmpty(stringDefinitions[0].Constant))
|| String.IsNullOrEmpty(stringDefinition.Constant))
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Scalar}', but some do not set '{TemplateConstants.Constant}'");
}
stringDefinitions.Add(stringDefinition);
}
else if (nestedDefinition is OneOfDefinition oneOfDefinition)
{
// Allow one-of to reference another one-of
}
else
{
throw new ArgumentException($"'{name}' refers to a '{nestedDefinition.DefinitionType}' definition");
}
}
if (mappingDefinitions?.Count > 1)
{
if (foundLooseKeyType)
{
throw new ArgumentException($"'{name}' refers to two mappings that both set '{TemplateConstants.LooseKeyType}'");
}
var seenProperties = new Dictionary<String, PropertyDefinition>(StringComparer.Ordinal);
foreach (var mappingDefinition in mappingDefinitions)
{
foreach (var newProperty in mappingDefinition.Properties)
{
// Already seen
if (seenProperties.TryGetValue(newProperty.Key, out PropertyDefinition existingProperty))
{
// Types match
if (String.Equals(existingProperty.Type, newProperty.Value.Type, StringComparison.Ordinal))
{
continue;
}
// Collision
throw new ArgumentException($"'{name}' contains two mappings with the same property, but each refers to a different type. All matching properties must refer to the same type.");
}
// New
else
{
seenProperties.Add(newProperty.Key, newProperty.Value);
}
}
}
}
}
}
}
@@ -0,0 +1,47 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class PropertyDefinition
{
internal PropertyDefinition(TemplateToken token)
{
if (token is StringToken stringToken)
{
Type = stringToken.Value;
}
else
{
var mapping = token.AssertMapping($"{TemplateConstants.MappingPropertyValue}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.MappingPropertyValue} key");
switch (mappingKey.Value)
{
case TemplateConstants.Type:
Type = mappingPair.Value.AssertString($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Type}").Value;
break;
case TemplateConstants.Required:
Required = mappingPair.Value.AssertBoolean($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Required}").Value;
break;
case TemplateConstants.Description:
Description = mappingPair.Value.AssertString($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Description}").Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.MappingPropertyValue} key");
break;
}
}
}
}
internal String Type { get; set; }
internal Boolean Required { get; set; }
internal String Description { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal abstract class ScalarDefinition : Definition
{
internal ScalarDefinition()
{
}
internal ScalarDefinition(MappingToken definition)
: base(definition)
{
}
internal abstract Boolean IsMatch(LiteralToken literal);
}
}
@@ -0,0 +1,69 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class SequenceDefinition : Definition
{
internal SequenceDefinition()
{
}
internal SequenceDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Sequence:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Sequence}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Sequence} key");
switch (mappingKey.Value)
{
case TemplateConstants.ItemType:
var itemType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Sequence} {TemplateConstants.ItemType}");
ItemType = itemType.Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Sequence} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Sequence;
internal String ItemType { get; set; }
internal override void Validate(
TemplateSchema schema,
String name)
{
if (String.IsNullOrEmpty(ItemType))
{
throw new ArgumentException($"'{name}' does not define '{TemplateConstants.ItemType}'");
}
// Lookup item type
schema.GetDefinition(ItemType);
}
}
}
@@ -0,0 +1,116 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class StringDefinition : ScalarDefinition
{
internal StringDefinition()
{
}
internal StringDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.String:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.String}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.String} key");
switch (mappingKey.Value)
{
case TemplateConstants.Constant:
var constantStringToken = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.Constant}");
Constant = constantStringToken.Value;
break;
case TemplateConstants.IgnoreCase:
var ignoreCaseBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.IgnoreCase}");
IgnoreCase = ignoreCaseBooleanToken.Value;
break;
case TemplateConstants.RequireNonEmpty:
var requireNonEmptyBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.RequireNonEmpty}");
RequireNonEmpty = requireNonEmptyBooleanToken.Value;
break;
case TemplateConstants.IsExpression:
var isExpressionBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.IsExpression}");
IsExpression = isExpressionBooleanToken.Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.String} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.String;
internal String Constant { get; set; }
internal Boolean IgnoreCase { get; set; }
internal Boolean RequireNonEmpty { get; set; }
internal Boolean IsExpression { get; set; }
internal override Boolean IsMatch(LiteralToken literal)
{
if (literal is StringToken str)
{
var value = str.Value;
if (!String.IsNullOrEmpty(Constant))
{
var comparison = IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (String.Equals(Constant, value, comparison))
{
return true;
}
}
else if (RequireNonEmpty)
{
if (!String.IsNullOrEmpty(value))
{
return true;
}
}
else
{
return true;
}
}
return false;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
if (!String.IsNullOrEmpty(Constant) && RequireNonEmpty)
{
throw new ArgumentException($"Properties '{TemplateConstants.Constant}' and '{TemplateConstants.RequireNonEmpty}' cannot both be set");
}
}
}
}
@@ -0,0 +1,484 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// This models the root schema object and contains definitions
/// </summary>
public sealed class TemplateSchema
{
internal TemplateSchema()
: this(null)
{
}
private TemplateSchema(MappingToken mapping)
{
// Add built-in type: null
var nullDefinition = new NullDefinition();
Definitions.Add(TemplateConstants.Null, nullDefinition);
// Add built-in type: boolean
var booleanDefinition = new BooleanDefinition();
Definitions.Add(TemplateConstants.Boolean, booleanDefinition);
// Add built-in type: number
var numberDefinition = new NumberDefinition();
Definitions.Add(TemplateConstants.Number, numberDefinition);
// Add built-in type: string
var stringDefinition = new StringDefinition();
Definitions.Add(TemplateConstants.String, stringDefinition);
// Add built-in type: sequence
var sequenceDefinition = new SequenceDefinition { ItemType = TemplateConstants.Any };
Definitions.Add(TemplateConstants.Sequence, sequenceDefinition);
// Add built-in type: mapping
var mappingDefinition = new MappingDefinition { LooseKeyType = TemplateConstants.String, LooseValueType = TemplateConstants.Any };
Definitions.Add(TemplateConstants.Mapping, mappingDefinition);
// Add built-in type: any
var anyDefinition = new OneOfDefinition();
anyDefinition.OneOf.Add(TemplateConstants.Null);
anyDefinition.OneOf.Add(TemplateConstants.Boolean);
anyDefinition.OneOf.Add(TemplateConstants.Number);
anyDefinition.OneOf.Add(TemplateConstants.String);
anyDefinition.OneOf.Add(TemplateConstants.Sequence);
anyDefinition.OneOf.Add(TemplateConstants.Mapping);
Definitions.Add(TemplateConstants.Any, anyDefinition);
if (mapping != null)
{
foreach (var pair in mapping)
{
var key = pair.Key.AssertString($"{TemplateConstants.TemplateSchema} key");
switch (key.Value)
{
case TemplateConstants.Version:
var version = pair.Value.AssertString(TemplateConstants.Version);
Version = version.Value;
break;
case TemplateConstants.Definitions:
var definitions = pair.Value.AssertMapping(TemplateConstants.Definitions);
foreach (var definitionsPair in definitions)
{
var definitionsKey = definitionsPair.Key.AssertString($"{TemplateConstants.Definitions} key");
var definitionsValue = definitionsPair.Value.AssertMapping(TemplateConstants.Definition);
var definition = default(Definition);
foreach (var definitionPair in definitionsValue)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Null:
definition = new NullDefinition(definitionsValue);
break;
case TemplateConstants.Boolean:
definition = new BooleanDefinition(definitionsValue);
break;
case TemplateConstants.Number:
definition = new NumberDefinition(definitionsValue);
break;
case TemplateConstants.String:
definition = new StringDefinition(definitionsValue);
break;
case TemplateConstants.Sequence:
definition = new SequenceDefinition(definitionsValue);
break;
case TemplateConstants.Mapping:
definition = new MappingDefinition(definitionsValue);
break;
case TemplateConstants.OneOf:
definition = new OneOfDefinition(definitionsValue);
break;
case TemplateConstants.AllowedValues:
// Ignore allowed-values in CSharp parser, we don't need to support events here
definition = new NullDefinition(definitionsValue);
break;
case TemplateConstants.CoerceRaw:
case TemplateConstants.Context:
case TemplateConstants.Description:
continue;
default:
definitionKey.AssertUnexpectedValue("definition mapping key"); // throws
break;
}
break;
}
if (definition == null)
{
throw new ArgumentException($"Unable to determine definition details. Specify the '{TemplateConstants.Structure}' property");
}
Definitions.Add(definitionsKey.Value, definition);
}
break;
default:
key.AssertUnexpectedValue($"{TemplateConstants.TemplateSchema} key"); // throws
break;
}
}
}
}
internal Dictionary<String, Definition> Definitions { get; } = new Dictionary<String, Definition>(StringComparer.Ordinal);
internal String Version { get; }
/// <summary>
/// Loads a user's schema file
/// </summary>
internal static TemplateSchema Load(IObjectReader objectReader)
{
var context = new TemplateContext
{
CancellationToken = CancellationToken.None,
Errors = new TemplateValidationErrors(maxErrors: 10, maxMessageLength: 500),
Memory = new TemplateMemory(
maxDepth: 50,
maxEvents: 1000000, // 1 million
maxBytes: 1024 * 1024), // 1 mb
TraceWriter = new EmptyTraceWriter(),
};
var value = TemplateReader.Read(context, TemplateConstants.TemplateSchema, objectReader, null, Schema, out _);
if (context.Errors.Count > 0)
{
throw new TemplateValidationException(context.Errors);
}
var mapping = value.AssertMapping(TemplateConstants.TemplateSchema);
var schema = new TemplateSchema(mapping);
schema.Validate();
return schema;
}
internal IEnumerable<T> Get<T>(Definition definition)
where T : Definition
{
if (definition is T match)
{
yield return match;
}
else if (definition is OneOfDefinition oneOf)
{
foreach (var reference in oneOf.OneOf)
{
var nestedDefinition = GetDefinition(reference);
if (nestedDefinition is T match2)
{
yield return match2;
}
}
}
}
internal Definition GetDefinition(String type)
{
if (Definitions.TryGetValue(type, out Definition value))
{
return value;
}
throw new ArgumentException($"Schema definition '{type}' not found");
}
internal Boolean HasProperties(MappingDefinition definition)
{
return definition.Properties.Count > 0;
}
internal Boolean TryGetProperty(
MappingDefinition definition,
String name,
out String type)
{
if (definition.Properties.TryGetValue(name, out PropertyDefinition property))
{
type = property.Type;
return true;
}
type = null;
return false;
}
internal Boolean TryMatchKey(
List<MappingDefinition> definitions,
String key,
out String valueType)
{
valueType = null;
// Check for a matching well known property
var notFoundInSome = false;
for (var i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
if (TryGetProperty(definition, key, out String t))
{
if (valueType == null)
{
valueType = t;
}
}
else
{
notFoundInSome = true;
}
}
// Check if found
if (valueType != null)
{
// Filter the matched definitions if needed
if (notFoundInSome)
{
for (var i = 0; i < definitions.Count;)
{
if (TryGetProperty(definitions[i], key, out _))
{
i++;
}
else
{
definitions.RemoveAt(i);
}
}
}
return true;
}
return false;
}
/// <summary>
/// The built-in schema for reading schema files
/// </summary>
private static TemplateSchema Schema
{
get
{
if (s_schema == null)
{
var schema = new TemplateSchema();
StringDefinition stringDefinition;
SequenceDefinition sequenceDefinition;
MappingDefinition mappingDefinition;
OneOfDefinition oneOfDefinition;
// template-schema
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Version, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Definitions, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Definitions)));
schema.Definitions.Add(TemplateConstants.TemplateSchema, mappingDefinition);
// definitions
mappingDefinition = new MappingDefinition();
mappingDefinition.LooseKeyType = TemplateConstants.NonEmptyString;
mappingDefinition.LooseValueType = TemplateConstants.Definition;
schema.Definitions.Add(TemplateConstants.Definitions, mappingDefinition);
// definition
oneOfDefinition = new OneOfDefinition();
oneOfDefinition.OneOf.Add(TemplateConstants.NullDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.BooleanDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.NumberDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.StringDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.SequenceDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.MappingDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.OneOfDefinition);
schema.Definitions.Add(TemplateConstants.Definition, oneOfDefinition);
// null-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Null, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NullDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.NullDefinition, mappingDefinition);
// null-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.NullDefinitionProperties, mappingDefinition);
// boolean-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Boolean, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.BooleanDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.BooleanDefinition, mappingDefinition);
// boolean-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.BooleanDefinitionProperties, mappingDefinition);
// number-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Number, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NumberDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.NumberDefinition, mappingDefinition);
// number-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.NumberDefinitionProperties, mappingDefinition);
// string-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.String, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.StringDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.StringDefinition, mappingDefinition);
// string-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Constant, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.IgnoreCase, new PropertyDefinition(new StringToken(null, null, null,TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.RequireNonEmpty, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.IsExpression, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
schema.Definitions.Add(TemplateConstants.StringDefinitionProperties, mappingDefinition);
// sequence-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Sequence, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.SequenceDefinition, mappingDefinition);
// sequence-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.ItemType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
schema.Definitions.Add(TemplateConstants.SequenceDefinitionProperties, mappingDefinition);
// mapping-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Mapping, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.MappingDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.MappingDefinition, mappingDefinition);
// mapping-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Properties, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Properties)));
mappingDefinition.Properties.Add(TemplateConstants.LooseKeyType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.LooseValueType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
schema.Definitions.Add(TemplateConstants.MappingDefinitionProperties, mappingDefinition);
// properties
mappingDefinition = new MappingDefinition();
mappingDefinition.LooseKeyType = TemplateConstants.NonEmptyString;
mappingDefinition.LooseValueType = TemplateConstants.PropertyValue;
schema.Definitions.Add(TemplateConstants.Properties, mappingDefinition);
// property-value
oneOfDefinition = new OneOfDefinition();
oneOfDefinition.OneOf.Add(TemplateConstants.NonEmptyString);
oneOfDefinition.OneOf.Add(TemplateConstants.MappingPropertyValue);
schema.Definitions.Add(TemplateConstants.PropertyValue, oneOfDefinition);
// mapping-property-value
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Type, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Required, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
schema.Definitions.Add(TemplateConstants.MappingPropertyValue, mappingDefinition);
// one-of-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.OneOf, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.AllowedValues, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
schema.Definitions.Add(TemplateConstants.OneOfDefinition, mappingDefinition);
// non-empty-string
stringDefinition = new StringDefinition();
stringDefinition.RequireNonEmpty = true;
schema.Definitions.Add(TemplateConstants.NonEmptyString, stringDefinition);
// sequence-of-non-empty-string
sequenceDefinition = new SequenceDefinition();
sequenceDefinition.ItemType = TemplateConstants.NonEmptyString;
schema.Definitions.Add(TemplateConstants.SequenceOfNonEmptyString, sequenceDefinition);
schema.Validate();
Interlocked.CompareExchange(ref s_schema, schema, null);
}
return s_schema;
}
}
private void Validate()
{
var oneOfPairs = new List<KeyValuePair<String, OneOfDefinition>>();
foreach (var pair in Definitions)
{
var name = pair.Key;
if (!s_definitionNameRegex.IsMatch(name ?? String.Empty))
{
throw new ArgumentException($"Invalid definition name '{name}'");
}
var definition = pair.Value;
// Delay validation for 'one-of' definitions
if (definition is OneOfDefinition oneOf)
{
oneOfPairs.Add(new KeyValuePair<String, OneOfDefinition>(name, oneOf));
}
// Otherwise validate now
else
{
definition.Validate(this, name);
}
}
// Validate 'one-of' definitions
foreach (var pair in oneOfPairs)
{
var name = pair.Key;
var oneOf = pair.Value;
oneOf.Validate(this, name);
}
}
private static readonly Regex s_definitionNameRegex = new Regex("^[a-zA-Z_][a-zA-Z0-9_-]*$", RegexOptions.Compiled);
private static TemplateSchema s_schema;
}
}