From 52a6e3d853101fecbf7ea89b2399d5d648addc54 Mon Sep 17 00:00:00 2001 From: TimLovellSmith Date: Fri, 20 May 2016 11:58:29 -0700 Subject: [PATCH] Fix Swagger SchemaResolver to allow for the fact that properties may be defined by chains of indirect references to some type which inherits from another compatible type, and that is fine! --- .../AutoRest.Modeler.Swagger.Tests.csproj | 4 + .../Swagger/swagger-redis-sample.json | 683 ++++++++++++++++++ .../Swagger.Tests/SwaggerModelerRedisTests.cs | 35 + AutoRest/Modelers/Swagger/SchemaResolver.cs | 49 +- 4 files changed, 756 insertions(+), 15 deletions(-) create mode 100644 AutoRest/Modelers/Swagger.Tests/Swagger/swagger-redis-sample.json create mode 100644 AutoRest/Modelers/Swagger.Tests/SwaggerModelerRedisTests.cs diff --git a/AutoRest/Modelers/Swagger.Tests/AutoRest.Modeler.Swagger.Tests.csproj b/AutoRest/Modelers/Swagger.Tests/AutoRest.Modeler.Swagger.Tests.csproj index ea2b011b53..dda50548a3 100644 --- a/AutoRest/Modelers/Swagger.Tests/AutoRest.Modeler.Swagger.Tests.csproj +++ b/AutoRest/Modelers/Swagger.Tests/AutoRest.Modeler.Swagger.Tests.csproj @@ -36,9 +36,13 @@ True Resources.resx + + + PreserveNewest + PreserveNewest diff --git a/AutoRest/Modelers/Swagger.Tests/Swagger/swagger-redis-sample.json b/AutoRest/Modelers/Swagger.Tests/Swagger/swagger-redis-sample.json new file mode 100644 index 0000000000..bf80d0515b --- /dev/null +++ b/AutoRest/Modelers/Swagger.Tests/Swagger/swagger-redis-sample.json @@ -0,0 +1,683 @@ +{ + "swagger": "2.0", + "info": { + "title": "RedisManagementClient", + "description": "REST API for Azure Redis Cache Service", + "version": "2016-04-01" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "text/json" + ], + "produces": [ + "application/json", + "text/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}": { + "put": { + "tags": [ + "Redis" + ], + "operationId": "Redis_CreateOrUpdate", + "description": "Create a redis cache, or replace (overwrite/recreate, with potential downtime) an existing cache", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisCreateOrUpdateParameters" + }, + "description": "Parameters supplied to the CreateOrUpdate redis operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisResourceWithAccessKey" + } + }, + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisResourceWithAccessKey" + } + } + } + }, + "delete": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Delete", + "description": "Deletes a redis cache. This operation takes a while to complete.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "" + }, + "204": { + "description": "" + } + } + }, + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Get", + "description": "Gets a redis cache (resource description).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisResource" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ListByResourceGroup", + "description": "Gets all redis caches in a resource group.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/Microsoft.Cache/Redis/": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_List", + "description": "Gets all redis caches in the specified subscription.", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/listKeys": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ListKeys", + "description": "Retrieve a redis cache's access keys. This operation requires write permission to the cache resource.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Lists the keys for Redis Cache", + "schema": { + "$ref": "#/definitions/RedisListKeysResult" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/regenerateKey": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_RegenerateKey", + "description": "Regenerate redis cache's access keys. This operation requires write permission to the cache resource.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisRegenerateKeyParameters" + }, + "description": "Specifies which key to reset." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Lists the regenerated keys for Redis Cache", + "schema": { + "$ref": "#/definitions/RedisListKeysResult" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/forceReboot": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ForceReboot", + "description": "Reboot specified redis node(s). This operation requires write permission to the cache resource. There can be potential data loss.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisRebootParameters" + }, + "description": "Specifies which redis node(s) to reboot." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "" + }, + "204": { + "description": "" + } + } + } + } + }, + "definitions": { + "Sku": { + "properties": { + "name": { + "type": "string", + "description": "What type of redis cache to deploy. Valid values: (Basic, Standard, Premium)", + "enum": [ + "Basic", + "Standard", + "Premium" + ], + "x-ms-enum": { + "name": "SkuName", + "modelAsString": true + } + }, + "family": { + "type": "string", + "description": "Which family to use. Valid values: (C, P)", + "enum": [ + "C", + "P" + ], + "x-ms-enum": { + "name": "SkuFamily", + "modelAsString": true + } + }, + "capacity": { + "type": "integer", + "format": "int32", + "description": "What size of redis cache to deploy. Valid values: for C family (0, 1, 2, 3, 4, 5, 6), for P family (1, 2, 3, 4)" + } + }, + "required": [ + "name", + "family", + "capacity" + ], + "description": "Sku parameters supplied to the create redis operation." + }, + "RedisProperties": { + "properties": { + "redisVersion": { + "type": "string", + "description": "RedisVersion parameter has been deprecated. As such, it is no longer necessary to provide this parameter and any value specified is ignored." + }, + "sku": { + "$ref": "#/definitions/Sku", + "description": "What sku of redis cache to deploy." + }, + "redisConfiguration": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "All Redis Settings. Few possible keys: rdb-backup-enabled,rdb-storage-connection-string,rdb-backup-frequency,maxmemory-delta,maxmemory-policy,notify-keyspace-events,maxmemory-samples,slowlog-log-slower-than,slowlog-max-len,list-max-ziplist-entries,list-max-ziplist-value,hash-max-ziplist-entries,hash-max-ziplist-value,set-max-intset-entries,zset-max-ziplist-entries,zset-max-ziplist-value etc." + }, + "enableNonSslPort": { + "type": "boolean", + "description": "If the value is true, then the non-ssl redis server port (6379) will be enabled." + }, + "tenantSettings": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "tenantSettings" + }, + "shardCount": { + "type": "integer", + "format": "int32", + "description": "The number of shards to be created on a Premium Cluster Cache." + }, + "subnetId": { + "type": "string", + "description": "The full resource ID of a subnet in a virtual network to deploy the redis cache in. Example format: /subscriptions/{subid}/resourceGroups/{resourceGroupName}/Microsoft.{Network|ClassicNetwork}/VirtualNetworks/vnet1/subnets/subnet1" + }, + "staticIP": { + "type": "string", + "description": "Required when deploying a redis cache inside an existing Azure Virtual Network." + } + }, + "required": [ + "sku" + ], + "description": "Parameters supplied to CreateOrUpdate redis operation." + }, + "Resource": { + "properties": { + "id": { + "readOnly": true, + "type": "string", + "description": "Resource Id" + }, + "name": { + "readOnly": true, + "type": "string", + "description": "Resource name" + }, + "type": { + "readOnly": true, + "type": "string", + "description": "Resource type" + }, + "location": { + "type": "string", + "description": "Resource location" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Resource tags" + } + }, + "required": [ + "location" + ], + "x-ms-azure-resource": true + }, + "RedisCreateOrUpdateParameters": { + "properties": { + "properties": { + "x-ms-client-flatten": true, + "$ref": "#/definitions/RedisProperties", + "description": "Redis cache properties." + } + }, + "required": [ + "properties" + ], + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], + "description": "Parameters supplied to the CreateOrUpdate Redis operation." + }, + "RedisAccessKeys": { + "properties": { + "primaryKey": { + "type": "string", + "description": "The current primary key that clients can use to authenticate with redis cache." + }, + "secondaryKey": { + "type": "string", + "description": "The current secondary key that clients can use to authenticate with redis cache." + } + }, + "description": "Redis cache access keys." + }, + "RedisReadableProperties": { + "properties": { + "provisioningState": { + "type": "string", + "description": "Redis instance provisioning status" + }, + "hostName": { + "type": "string", + "description": "Redis host name" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "Redis non-ssl port" + }, + "sslPort": { + "type": "integer", + "format": "int32", + "description": "Redis ssl port" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RedisProperties" + } + ], + "description": "Parameters describing a redis instance" + }, + "RedisReadablePropertiesWithAccessKey": { + "properties": { + "accessKeys": { + "$ref": "#/definitions/RedisAccessKeys", + "description": "Redis cache access keys." + } + }, + "allOf": [ + { + "$ref": "#/definitions/RedisReadableProperties" + } + ], + "description": "Properties generated only in response to CreateOrUpdate redis operation." + }, + "RedisResourceWithAccessKey": { + "properties": { + "properties": { + "x-ms-client-flatten": true, + "$ref": "#/definitions/RedisReadablePropertiesWithAccessKey", + "description": "Redis cache properties" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RedisResource" + } + ], + "description": "A redis item in CreateOrUpdate Operation response." + }, + "RedisResource": { + "properties": { + "properties": { + "x-ms-client-flatten": true, + "$ref": "#/definitions/RedisReadableProperties", + "description": "Redis cache properties" + } + }, + "allOf": [ + { + "$ref": "#/definitions/Resource" + } + ], + "description": "A single redis item in List or Get Operation." + }, + "RedisListResult": { + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/RedisResource" + }, + "description": "Results of the list operation" + }, + "nextLink": { + "type": "string", + "description": "Link for next set of locations." + } + }, + "description": "The response of list redis operation." + }, + "RedisListKeysResult": { + "properties": { + "primaryKey": { + "type": "string", + "description": "The current primary key that clients can use to authenticate with redis cache." + }, + "secondaryKey": { + "type": "string", + "description": "The current secondary key that clients can use to authenticate with redis cache." + } + }, + "description": "The response of redis list keys operation." + }, + "RedisRegenerateKeyParameters": { + "properties": { + "keyType": { + "type": "string", + "description": "Which redis access key to reset", + "enum": [ + "Primary", + "Secondary" + ], + "x-ms-enum": { + "name": "RedisKeyType", + "modelAsString": false + } + } + }, + "required": [ + "keyType" + ], + "description": "Specifies which redis access keys to reset." + }, + "RedisRebootParameters": { + "properties": { + "rebootType": { + "type": "string", + "description": "Which redis node(s) to reboot. Depending on this value data loss is possible.", + "enum": [ + "PrimaryNode", + "SecondaryNode", + "AllNodes" + ], + "x-ms-enum": { + "name": "RebootType", + "modelAsString": false + } + }, + "shardId": { + "type": "integer", + "format": "int32", + "description": "In case of cluster cache, this specifies shard id which should be rebooted." + } + }, + "required": [ + "rebootType" + ], + "description": "Specifies which redis node(s) to reboot." + } + }, + "parameters": { + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "Gets subscription credentials which uniquely identify Microsoft Azure subscription. The subscription ID forms part of the URI for every service call." + }, + "ApiVersionParameter": { + "name": "api-version", + "in": "query", + "required": true, + "type": "string", + "description": "Client Api Version." + } + } +} \ No newline at end of file diff --git a/AutoRest/Modelers/Swagger.Tests/SwaggerModelerRedisTests.cs b/AutoRest/Modelers/Swagger.Tests/SwaggerModelerRedisTests.cs new file mode 100644 index 0000000000..e19bd34e90 --- /dev/null +++ b/AutoRest/Modelers/Swagger.Tests/SwaggerModelerRedisTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using Microsoft.Rest.Generator; +using Microsoft.Rest.Generator.ClientModel; +using Microsoft.Rest.Generator.CSharp; +using Microsoft.Rest.Generator.Extensibility; +using Microsoft.Rest.Generator.Utilities; +using Xunit; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Rest.Modeler.Swagger.Tests +{ + [Collection("AutoRest Tests")] + public class SwaggerModelerRedisTests + { + [Fact] + public void RedisResponseWithAccessKeys_IsAssignableTo_RedisResponse() + { + Generator.Modeler modeler = new SwaggerModeler(new Settings + { + Namespace = "Test", + Input = Path.Combine("Swagger", "swagger-redis-sample.json") + }); + var clientModel = modeler.Build(); + var redisResponseModel = clientModel.ModelTypes.Single(x => x.Name == "RedisResource"); + var redisResponseWithAccessKeyModel = clientModel.ModelTypes.Single(x => x.Name == "RedisResourceWithAccessKey"); + Assert.Equal(redisResponseModel, redisResponseWithAccessKeyModel.BaseModelType); + } + } +} \ No newline at end of file diff --git a/AutoRest/Modelers/Swagger/SchemaResolver.cs b/AutoRest/Modelers/Swagger/SchemaResolver.cs index 092a8c145f..6fbf3603c6 100644 --- a/AutoRest/Modelers/Swagger/SchemaResolver.cs +++ b/AutoRest/Modelers/Swagger/SchemaResolver.cs @@ -18,7 +18,6 @@ public class SchemaResolver : ICloneable private const int MaximumReferenceDepth = 40; private readonly SwaggerModeler _modeler; private readonly ServiceDefinition _serviceDefinition; - private readonly List _visitedReferences; /// /// Create a new schema resolver in the context of the given swagger spec @@ -33,7 +32,6 @@ public SchemaResolver(SwaggerModeler modeler) _modeler = modeler; _serviceDefinition = modeler.ServiceDefinition; - _visitedReferences = new List(); } /// @@ -44,11 +42,6 @@ public SchemaResolver(SwaggerModeler modeler) public object Clone() { var resolver = new SchemaResolver(_modeler); - foreach (string reference in _visitedReferences) - { - resolver._visitedReferences.Add(reference); - } - return resolver; } @@ -201,12 +194,32 @@ private bool SchemaTypesAreEquivalent(Schema parentProperty, if ((parentProperty.Type == null || parentProperty.Type == DataType.Object) && (unwrappedProperty.Type == null || unwrappedProperty.Type == DataType.Object)) { - if (!string.IsNullOrEmpty(parentProperty.Reference) || - !string.IsNullOrEmpty(unwrappedProperty.Reference)) + var parentPropertyToCompare = parentProperty; + var unwrappedPropertyToCompare = unwrappedProperty; + if (!string.IsNullOrEmpty(parentProperty.Reference)) + { + parentPropertyToCompare = Dereference(parentProperty.Reference); + } + if (!string.IsNullOrEmpty(unwrappedProperty.Reference)) { - return parentProperty.Reference == unwrappedProperty.Reference; + unwrappedPropertyToCompare = Dereference(unwrappedProperty.Reference); } - // do not compare inline schemas + + 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 && @@ -252,6 +265,12 @@ private Schema FindParentProperty(string parentReference, string propertyName) /// The schema reference to dereference. /// The dereferenced schema. private Schema Dereference(string referencePath) + { + var vistedReferences = new List(); + return DereferenceInner(referencePath, vistedReferences); + } + + private Schema DereferenceInner(string referencePath, List visitedReferences) { // Check if external reference string[] splitReference = referencePath.Split(new[] {'#'}, StringSplitOptions.RemoveEmptyEntries); @@ -260,17 +279,17 @@ private Schema Dereference(string referencePath) referencePath = "#" + splitReference[1]; } - if (_visitedReferences.Contains(referencePath.ToLower(CultureInfo.InvariantCulture))) + if (visitedReferences.Contains(referencePath.ToLower(CultureInfo.InvariantCulture))) { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CircularReference, referencePath)); } - if (_visitedReferences.Count >= MaximumReferenceDepth) + if (visitedReferences.Count >= MaximumReferenceDepth) { throw new ArgumentException(Properties.Resources.ExceededMaximumReferenceDepth, referencePath); } - _visitedReferences.Add(referencePath.ToLower(CultureInfo.InvariantCulture)); + visitedReferences.Add(referencePath.ToLower(CultureInfo.InvariantCulture)); var definitions = _serviceDefinition.Definitions; if (definitions == null || !definitions.ContainsKey(referencePath.StripDefinitionPath())) { @@ -282,7 +301,7 @@ private Schema Dereference(string referencePath) var schema = _serviceDefinition.Definitions[referencePath.StripDefinitionPath()]; if (schema.Reference != null) { - schema = Dereference(schema.Reference); + schema = DereferenceInner(schema.Reference, visitedReferences); } return schema;