From 0ba790bc48463ea2de9776caa67b4c05aaac8787 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 should be 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;