using System; using System.Collections.Generic; using System.ComponentModel; using System.Text.RegularExpressions; using System.Threading; using GitHub.DistributedTask.ObjectTemplating.Tokens; namespace GitHub.DistributedTask.ObjectTemplating.Schema { /// /// This models the root schema object and contains definitions /// [EditorBrowsable(EditorBrowsableState.Never)] 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.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 Definitions { get; } = new Dictionary(StringComparer.Ordinal); internal String Version { get; } /// /// Loads a user's schema file /// 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 Get(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) { for (int i = 0; i < 10; i++) { if (definition.Properties.Count > 0) { return true; } if (String.IsNullOrEmpty(definition.Inherits)) { return false; } definition = GetDefinition(definition.Inherits) as MappingDefinition; } throw new InvalidOperationException("Inheritance depth exceeded 10"); } internal Boolean TryGetProperty( MappingDefinition definition, String name, out String type) { for (int i = 0; i < 10; i++) { if (definition.Properties.TryGetValue(name, out PropertyValue property)) { type = property.Type; return true; } if (String.IsNullOrEmpty(definition.Inherits)) { type = default; return false; } definition = GetDefinition(definition.Inherits) as MappingDefinition; } throw new InvalidOperationException("Inheritance depth exceeded 10"); } internal Boolean TryMatchKey( List 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; } /// /// The built-in schema for reading schema files /// 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 PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Definitions, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Null, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Boolean, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Number, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.String, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.IgnoreCase, new PropertyValue(new StringToken(null, null, null,TemplateConstants.Boolean))); mappingDefinition.Properties.Add(TemplateConstants.RequireNonEmpty, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Boolean))); schema.Definitions.Add(TemplateConstants.StringDefinitionProperties, mappingDefinition); // sequence-definition mappingDefinition = new MappingDefinition(); mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Sequence, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString))); schema.Definitions.Add(TemplateConstants.SequenceDefinitionProperties, mappingDefinition); // mapping-definition mappingDefinition = new MappingDefinition(); mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Mapping, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.Properties))); mappingDefinition.Properties.Add(TemplateConstants.LooseKeyType, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.LooseValueType, new PropertyValue(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 PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.Required, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Boolean))); schema.Definitions.Add(TemplateConstants.MappingPropertyValue, mappingDefinition); // one-of-definition mappingDefinition = new MappingDefinition(); mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String))); mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString))); mappingDefinition.Properties.Add(TemplateConstants.OneOf, new PropertyValue(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>(); 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(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; } }