/// <summary> /// Normalize a swagger schema by dereferencing schema references and evaluating /// schema composition /// </summary> /// <param name="schema">The schema to normalize</param> /// <returns>A normalized swagger schema</returns> public Schema Unwrap(Schema schema) { if (schema == null) { return null; } Schema unwrappedSchema = schema; // If referencing global definitions serializationProperty if (schema.Reference != null) { unwrappedSchema = Dereference(schema.Reference); } ExpandAllOf(unwrappedSchema); return unwrappedSchema; }
/// <summary> /// Evaluate the composition of properties for a swagger spec and save the /// evaluated form in the specification. This transformation is idempotent /// </summary> /// <param name="schema">The swagger schema to evaluate.</param> public void ExpandAllOf(Schema schema) { if (schema == null) { throw new ArgumentNullException("schema"); } if (schema.AllOf != null) { CheckCircularAllOf(schema, null, null); var references = schema.AllOf.Where(s => s.Reference != null).ToList(); if (references.Count == 1) { if (schema.Extends != null) { throw new ArgumentException( string.Format(CultureInfo.InvariantCulture, Properties.Resources.InvalidTypeExtendsWithAllOf, schema.Title)); } schema.Extends = references[0].Reference; schema.AllOf.Remove(references[0]); } var parentSchema = schema.Extends; var propertiesOnlySchema = new Schema { Properties = schema.Properties }; var schemaList = new List<Schema>().Concat(schema.AllOf) .Concat(new List<Schema> { propertiesOnlySchema }); schema.Properties = new Dictionary<string, Schema>(); foreach (var componentSchema in schemaList) { // keep the same resolver state for each of the children var unwrappedComponent = ((SchemaResolver)Clone()).Unwrap( componentSchema); if (unwrappedComponent != null && unwrappedComponent.Properties != null) { foreach (var propertyName in unwrappedComponent.Properties.Keys) { var unwrappedProperty = unwrappedComponent.Properties[propertyName]; if (schema.Properties.ContainsKey(propertyName)) { if (!SchemaTypesAreEquivalent( schema.Properties[propertyName], unwrappedProperty)) { throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, Properties.Resources.IncompatibleTypesInSchemaComposition, propertyName, unwrappedComponent.Properties[propertyName].Type, schema.Properties[propertyName].Type, schema.Title)); } } else { var parentProperty = ((SchemaResolver)Clone()) .FindProperty(parentSchema, propertyName); if (parentProperty != null) { if (!SchemaTypesAreEquivalent(parentProperty, unwrappedProperty)) { throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, Properties.Resources.IncompatibleTypesInBaseSchema, propertyName, parentProperty.Type, unwrappedProperty.Type, schema.Title)); } } else { schema.Properties[propertyName] = unwrappedProperty; } } } } if (unwrappedComponent != null && unwrappedComponent.Required != null) { var requiredProperties = schema.Required ?? new List<string>(); foreach (var requiredProperty in unwrappedComponent.Required) { if (!requiredProperties.Contains(requiredProperty)) { requiredProperties.Add(requiredProperty); } } schema.Required = requiredProperties; } } schema.AllOf = null; } }
/// <summary> /// Determine equivalence between the types described by two schemas. /// Limit the comparison to exclude comparison of complexe inline schemas. /// </summary> /// <param name="parentProperty"></param> /// <param name="unwrappedProperty"></param> /// <returns></returns> private bool SchemaTypesAreEquivalent(Schema parentProperty, Schema unwrappedProperty) { Debug.Assert(parentProperty != null && unwrappedProperty != null); if (parentProperty == null) { throw new ArgumentNullException("parentProperty"); } if (unwrappedProperty == null) { throw new ArgumentNullException("unwrappedProperty"); } if ((parentProperty.Type == null || parentProperty.Type == DataType.Object) && (unwrappedProperty.Type == null || unwrappedProperty.Type == DataType.Object)) { var parentPropertyToCompare = parentProperty; var unwrappedPropertyToCompare = unwrappedProperty; if (!string.IsNullOrEmpty(parentProperty.Reference)) { parentPropertyToCompare = Dereference(parentProperty.Reference); } if (!string.IsNullOrEmpty(unwrappedProperty.Reference)) { unwrappedPropertyToCompare = Dereference(unwrappedProperty.Reference); } if (parentPropertyToCompare == unwrappedPropertyToCompare) { return true; // when fully dereferenced, they can refer to the same thing } // or they can refer to different things... but there can be an inheritance relation... while (unwrappedPropertyToCompare != null && unwrappedPropertyToCompare.Extends != null) { unwrappedPropertyToCompare = Dereference(unwrappedPropertyToCompare.Extends); if (unwrappedPropertyToCompare == parentPropertyToCompare) { return true; } } return false; } if (parentProperty.Type == DataType.Array && unwrappedProperty.Type == DataType.Array) { return SchemaTypesAreEquivalent(parentProperty.Items, unwrappedProperty.Items); } return parentProperty.Type == unwrappedProperty.Type && parentProperty.Format == unwrappedProperty.Format; }
void CheckCircularAllOf(Schema schema, HashSet<Schema> visited, Stack<string> referenceChain) { visited = visited ?? new HashSet<Schema>(); referenceChain = referenceChain ?? new Stack<string>(); if (!visited.Add(schema)) // was already present in the set { var setDescription = "(" + String.Join(", ", referenceChain) + ")"; throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, Properties.Resources.CircularBaseSchemaSet, setDescription)); } if (schema.AllOf != null) { foreach (var reference in schema.AllOf.Select(s => s.Reference).Where(r => r != null)) { referenceChain.Push(reference); var deref = Dereference(reference); CheckCircularAllOf(deref, visited, referenceChain); Debug.Assert(reference == referenceChain.Peek()); referenceChain.Pop(); } } visited.Remove(schema); }
/// <summary> /// Determine whether a given property is defined in the schema or its ancestors. /// Return the property schema if it is defined, or null if not. /// </summary> /// <param name="schema">A schema</param> /// <param name="propertyName">The property to search for</param> /// <returns></returns> public Schema FindProperty(Schema schema, string propertyName) { Schema returnedSchema = null; ExpandAllOf(schema); if (schema.Properties != null && schema.Properties.ContainsKey(propertyName)) { returnedSchema = schema.Properties[propertyName]; } else { returnedSchema = FindProperty(schema.Extends, propertyName); } return returnedSchema; }
public override IModelType BuildServiceType(string serviceTypeName) { // Check if already generated if (serviceTypeName != null && Modeler.GeneratedTypes.ContainsKey(serviceTypeName)) { return Modeler.GeneratedTypes[serviceTypeName]; } _schema = Modeler.Resolver.Unwrap(_schema); // If it's a primitive type, let the parent build service handle it if (_schema.IsPrimitiveType()) { return _schema.GetBuilder(Modeler).ParentBuildServiceType(serviceTypeName); } // If it's known primary type, return that type var primaryType = _schema.GetSimplePrimaryType(); if (primaryType != KnownPrimaryType.None) { return New<PrimaryType>(primaryType); } // Otherwise create new object type var objectType = New<CompositeType>(serviceTypeName,new { SerializedName = serviceTypeName, Documentation = _schema.Description, ExternalDocsUrl = _schema.ExternalDocs?.Url, Summary = _schema.Title }); // Put this in already generated types serializationProperty Modeler.GeneratedTypes[serviceTypeName] = objectType; if (_schema.Type == DataType.Object && _schema.AdditionalProperties != null) { // this schema is defining 'additionalProperties' which expects to create an extra // property that will catch all the unbound properties during deserialization. var name = "additionalProperties"; var propertyType = New<DictionaryType>(new { ValueType = _schema.AdditionalProperties.GetBuilder(Modeler).BuildServiceType( _schema.AdditionalProperties.Reference != null ? _schema.AdditionalProperties.Reference.StripDefinitionPath() : serviceTypeName + "Value"), SupportsAdditionalProperties = true }); // now add the extra property to the type. objectType.Add(New<Property>(new { Name = name, ModelType = propertyType, Documentation = "Unmatched properties from the message are deserialized this collection" })); } if (_schema.Properties != null) { // Visit each property and recursively build service types foreach (var property in _schema.Properties) { string name = property.Key; if (name != _schema.Discriminator) { string propertyServiceTypeName; Schema refSchema = null; if (property.Value.ReadOnly && property.Value.IsRequired) { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.ReadOnlyNotRequired, name, serviceTypeName)); } if (property.Value.Reference != null) { propertyServiceTypeName = property.Value.Reference.StripDefinitionPath(); var unwrappedSchema = Modeler.Resolver.Unwrap(property.Value); // For Enums use the referenced schema in order to set the correct property Type and Enum values if (unwrappedSchema.Enum != null) { refSchema = new Schema().LoadFrom(unwrappedSchema); if (property.Value.IsRequired) { refSchema.IsRequired = property.Value.IsRequired; } //Todo: Remove the following when referenced descriptions are correctly ignored (Issue https://github.com/Azure/autorest/issues/1283) refSchema.Description = property.Value.Description; } } else { propertyServiceTypeName = serviceTypeName + "_" + property.Key; } var propertyType = refSchema != null ? refSchema.GetBuilder(Modeler).BuildServiceType(propertyServiceTypeName) : property.Value.GetBuilder(Modeler).BuildServiceType(propertyServiceTypeName); var propertyObj = New<Property>(new { Name = name, SerializedName = name, ModelType = propertyType, IsReadOnly = property.Value.ReadOnly, Summary = property.Value.Title }); PopulateParameter(propertyObj, refSchema != null ? refSchema : property.Value); var propertyCompositeType = propertyType as CompositeType; if (propertyObj.IsConstant || (propertyCompositeType != null && propertyCompositeType.ContainsConstantProperties)) { objectType.ContainsConstantProperties = true; } objectType.Add(propertyObj); } else { objectType.PolymorphicDiscriminator = name; } } } // Copy over extensions _schema.Extensions.ForEach(e => objectType.Extensions[e.Key] = e.Value); if (_schema.Extends != null) { // Optionally override the discriminator value for polymorphic types. We expect this concept to be // added to Swagger at some point, but until it is, we use an extension. object discriminatorValueExtension; if (objectType.Extensions.TryGetValue(DiscriminatorValueExtension, out discriminatorValueExtension)) { string discriminatorValue = discriminatorValueExtension as string; if (discriminatorValue != null) { objectType.SerializedName = discriminatorValue; } } // Put this in the extended type serializationProperty for building method return type in the end Modeler.ExtendedTypes[serviceTypeName] = _schema.Extends.StripDefinitionPath(); } return objectType; }
public SchemaBuilder(Schema schema, SwaggerModeler modeler) : base(schema, modeler) { _schema = schema; }
/// <summary> /// Compare a modified document node (this) to a previous one and look for breaking as well as non-breaking changes. /// </summary> /// <param name="context">The modified document context.</param> /// <param name="previous">The original document model.</param> /// <returns>A list of messages from the comparison.</returns> public override IEnumerable <ComparisonMessage> Compare(ComparisonContext context, SwaggerBase previous) { if (previous == null) { throw new ArgumentNullException("previous"); } context.CurrentRoot = this; context.PreviousRoot = previous; base.Compare(context, previous); var previousDefinition = previous as ServiceDefinition; if (previousDefinition == null) { throw new ArgumentException("Comparing a service definition with something else."); } if (Info != null && previousDefinition.Info != null) { context.PushProperty("info"); context.PushProperty("version"); CompareVersions(context, Info.Version, previousDefinition.Info.Version); context.Pop(); context.Pop(); } if (context.Strict) { // There was no version change between the documents. This is not an error, but noteworthy. context.LogInfo(ComparisonMessages.NoVersionChange); } // Check that all the protocols of the old version are supported by the new version. context.PushProperty("schemes"); foreach (var scheme in previousDefinition.Schemes) { if (!Schemes.Contains(scheme)) { context.LogBreakingChange(ComparisonMessages.ProtocolNoLongerSupported, scheme); } } context.Pop(); // Check that all the request body formats that were accepted still are. context.PushProperty("consumes"); foreach (var format in previousDefinition.Consumes) { if (!Consumes.Contains(format)) { context.LogBreakingChange(ComparisonMessages.RequestBodyFormatNoLongerSupported, format); } } context.Pop(); // Check that all the response body formats were also supported by the old version. context.PushProperty("produces"); foreach (var format in Produces) { if (!previousDefinition.Produces.Contains(format)) { context.LogBreakingChange(ComparisonMessages.ResponseBodyFormatNowSupported, format); } } context.Pop(); // Check that no paths were removed, and compare the paths that are still there. var newPaths = RemovePathVariables(Paths); context.PushProperty("paths"); foreach (var path in previousDefinition.Paths.Keys) { var p = Regex.Replace(path, @"\{\w*\}", @"{}"); context.PushProperty(path); Dictionary <string, Operation> operations = null; if (!newPaths.TryGetValue(p, out operations)) { context.LogBreakingChange(ComparisonMessages.RemovedPath, path); } else { Dictionary <string, Operation> previousOperations = previousDefinition.Paths[path]; foreach (var previousOperation in previousOperations) { Operation newOperation = null; if (!operations.TryGetValue(previousOperation.Key, out newOperation)) { context.LogBreakingChange(ComparisonMessages.RemovedOperation, previousOperation.Value.OperationId); } } foreach (var operation in operations) { Operation previousOperation = null; if (previousDefinition.Paths[path].TryGetValue(operation.Key, out previousOperation)) { context.PushProperty(operation.Key); operation.Value.Compare(context, previousOperation); context.Pop(); } } } context.Pop(); } context.Pop(); newPaths = RemovePathVariables(CustomPaths); context.PushProperty("x-ms-paths"); foreach (var path in previousDefinition.CustomPaths.Keys) { var p = Regex.Replace(path, @"\{\w*\}", @"{}"); context.PushProperty(path); Dictionary <string, Operation> operations = null; if (!newPaths.TryGetValue(p, out operations)) { context.LogBreakingChange(ComparisonMessages.RemovedPath, path); } else { Dictionary <string, Operation> previousOperations = previousDefinition.CustomPaths[path]; foreach (var previousOperation in previousOperations) { Operation newOperation = null; if (!operations.TryGetValue(previousOperation.Key, out newOperation)) { context.LogBreakingChange(ComparisonMessages.RemovedOperation, previousOperation.Value.OperationId); } } foreach (var operation in operations) { Operation previousOperation = null; if (previousDefinition.CustomPaths[path].TryGetValue(operation.Key, out previousOperation)) { context.PushProperty(operation.Key); operation.Value.Compare(context, previousOperation); context.Pop(); } } } context.Pop(); } context.Pop(); ReferenceTrackSchemas(this); ReferenceTrackSchemas(previousDefinition); context.PushProperty("parameters"); foreach (var def in previousDefinition.Parameters.Keys) { SwaggerParameter parameter = null; if (!Parameters.TryGetValue(def, out parameter)) { context.LogBreakingChange(ComparisonMessages.RemovedClientParameter, def); } else { context.PushProperty(def); parameter.Compare(context, previousDefinition.Parameters[def]); context.Pop(); } } context.Pop(); context.PushProperty("responses"); foreach (var def in previousDefinition.Responses.Keys) { OperationResponse response = null; if (!Responses.TryGetValue(def, out response)) { context.LogBreakingChange(ComparisonMessages.RemovedDefinition, def); } else { context.PushProperty(def); response.Compare(context, previousDefinition.Responses[def]); context.Pop(); } } context.Pop(); context.PushProperty("definitions"); foreach (var def in previousDefinition.Definitions.Keys) { Schema schema = null; Schema oldSchema = previousDefinition.Definitions[def]; if (!Definitions.TryGetValue(def, out schema)) { if (oldSchema.IsReferenced) { // It's only an error if the definition is referenced in the old service. context.LogBreakingChange(ComparisonMessages.RemovedDefinition, def); } } else if (schema.IsReferenced && oldSchema.IsReferenced) { context.PushProperty(def); schema.Compare(context, previousDefinition.Definitions[def]); context.Pop(); } } context.Pop(); context.Pop(); return(context.Messages); }
private void CompareProperties(ComparisonContext context, Schema priorSchema) { // Were any properties removed? if (priorSchema.Properties != null) { foreach (var def in priorSchema.Properties) { Schema model = null; if (Properties == null || !Properties.TryGetValue(def.Key, out model)) { context.LogBreakingChange(ComparisonMessages.RemovedProperty1, def.Key); } else { context.Push(def.Key); model.Compare(context, def.Value); context.Pop(); } } } // Were any required properties added? if (Properties != null) { foreach (var def in Properties.Keys) { Schema model = null; if (priorSchema.Properties == null || !priorSchema.Properties.TryGetValue(def, out model) && Required.Contains(def)) { context.LogBreakingChange(ComparisonMessages.AddedRequiredProperty1, def); } } } }
private void CompareAllOfs(ComparisonContext context, Schema priorSchema) { var different = 0; foreach (var schema in priorSchema.AllOf) { if (!AllOf.Select(s => s.Reference).ToArray().Contains(schema.Reference)) { different += 1; } } foreach (var schema in AllOf) { if (!priorSchema.AllOf.Select(s => s.Reference).ToArray().Contains(schema.Reference)) { different += 1; } } if (different > 0) { context.LogBreakingChange(ComparisonMessages.DifferentAllOf); } }