/// <summary> /// Since renaming a path parameter doesn't logically alter the path, we must remove the parameter names /// before comparing paths using string comparison. /// </summary> /// <param name="paths">A dictionary of paths, potentially with embedded parameter names.</param> /// <returns>A transformed dictionary, where paths do not embed parameter names.</returns> private Dictionary <string, Dictionary <string, Operation> > RemovePathVariables( Dictionary <string, Dictionary <string, Operation> > paths ) => paths.ToDictionary(kv => ObjectPath.OpenApiPathName(kv.Key), kv => kv.Value);
/// <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 <ServiceDefinition> context, ServiceDefinition previousDefinition ) { if (context.CurrentRoot != this) { throw new ArgumentException("context.CurrentRoot != this"); } if (context.PreviousRoot != previousDefinition) { throw new ArgumentException("context.PreviousRoot != previousDefinition"); } if (previousDefinition == null) { throw new ArgumentException("Comparing a service definition with something else."); } base.Compare(context, previousDefinition); if (Info?.Version != null && previousDefinition.Info?.Version != 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 = ObjectPath.OpenApiPathName(path); context.PushPathProperty(path); if (!newPaths.TryGetValue(p, out var operations)) { // Entrie path was removeed context.LogBreakingChange(ComparisonMessages.RemovedPath, path); } else { // 1. Remove this path from the current list to find the added paths newPaths.Remove(p); var copyOfOperations = operations.ToDictionary(e => e.Key, e => e.Value); // 2. look for operation match inside this path var previousOperations = previousDefinition.Paths[path]; foreach (var previousOperation in previousOperations) { if (!operations.TryGetValue(previousOperation.Key, out var newOperation)) { // Operation was removed from the path context.LogBreakingChange(ComparisonMessages.RemovedOperation, previousOperation.Value.OperationId); } else { copyOfOperations.Remove(previousOperation.Key); } } // Look for added operations foreach (var copyOfOperation in copyOfOperations) { context.PushProperty(copyOfOperation.Key); context.LogInfo(ComparisonMessages.AddedOperation); context.Pop(); } // Compare operations foreach (var operation in operations) { if (previousDefinition.Paths[path].TryGetValue(operation.Key, out var previousOperation)) { context.PushProperty(operation.Key); operation.Value.Compare(context, previousOperation); context.Pop(); } } } context.Pop(); } // Check wether any new paths are being added foreach (var path in newPaths.Keys) { context.PushPathProperty(path); context.LogInfo(ComparisonMessages.AddedPath); context.Pop(); } context.Pop(); // Check for custom paths : x-ms-paths var newCustomPaths = RemovePathVariables(CustomPaths); context.PushProperty("x-ms-paths"); foreach (var path in previousDefinition.CustomPaths.Keys) { var p = ObjectPath.OpenApiPathName(path); context.PushPathProperty(path); Dictionary <string, Operation> operations = null; if (!newCustomPaths.TryGetValue(p, out operations)) { context.LogBreakingChange(ComparisonMessages.RemovedPath, path); } else { // 1. Remove this custom path from the current list to find the added paths newCustomPaths.Remove(p); Dictionary <string, Operation> copyOfOperations = operations.ToDictionary(e => e.Key, e => e.Value); // 2. look for operation match inside this path 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); } } // Look for added operations foreach (var copyOfOperation in copyOfOperations) { context.PushProperty(copyOfOperation.Key); context.LogInfo(ComparisonMessages.AddedOperation); context.Pop(); } // Compare operations 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(); } // Check wether any new paths are being added into x-ms-paths foreach (var path in newCustomPaths.Keys) { context.PushPathProperty(path); context.LogInfo(ComparisonMessages.AddedPath); 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 { context.PushProperty(def); schema.Compare(context, previousDefinition.Definitions[def]); context.Pop(); } } context.Pop(); context.Pop(); return(context.Messages); }