From 6b71ddaf374aa04cd2d3badb7fa9b55ba522b603 Mon Sep 17 00:00:00 2001 From: Vishrut Shah Date: Wed, 15 Jun 2016 17:42:43 -0700 Subject: [PATCH] Ruby - Move serialization/deserialization into clientruntime from sdk (#1106) --- .gitignore | 3 + .../AzureMethodTemplateModel.cs | 8 +- .../TemplateModels/AzureModelTemplateModel.cs | 32 +- .../Templates/AzureMethodTemplate.cshtml | 10 +- .../Ruby/Ruby.Tests/RspecTests/string_spec.rb | 10 + .../Ruby/Ruby/ClientModelExtensions.cs | 623 +++++++++++++----- .../TemplateModels/MethodTemplateModel.cs | 95 ++- .../Ruby/TemplateModels/ModelTemplateModel.cs | 60 +- .../Ruby/Ruby/Templates/MethodTemplate.cshtml | 14 +- .../Ruby/Ruby/Templates/ModelTemplate.cshtml | 122 +--- .../Templates/ServiceClientTemplate.cshtml | 1 + .../lib/ms_rest_azure/resource.rb | 116 ++-- .../lib/ms_rest_azure/sub_resource.rb | 53 +- .../Ruby/ms-rest-azure/spec/resource_spec.rb | 24 +- .../ms-rest-azure/spec/sub_resource_spec.rb | 15 +- ClientRuntimes/Ruby/ms-rest/lib/ms_rest.rb | 3 +- .../Ruby/ms-rest/lib/ms_rest/serialization.rb | 364 +++++++++- .../Ruby/ms-rest/spec/serialization_spec.rb | 25 - 18 files changed, 1037 insertions(+), 541 deletions(-) delete mode 100644 ClientRuntimes/Ruby/ms-rest/spec/serialization_spec.rb diff --git a/.gitignore b/.gitignore index 8d939cbe2121d..7723f5632d353 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ ipch/ *.psess *.vsp +# VS Code settings +*.vscode + # Code analysis *.CodeAnalysisLog.xml diff --git a/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureMethodTemplateModel.cs b/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureMethodTemplateModel.cs index abd746e1587ad..5ab6a30c6fa7d 100644 --- a/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureMethodTemplateModel.cs +++ b/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureMethodTemplateModel.cs @@ -82,7 +82,7 @@ public string DeserializePollingResponse(string variableName, IType type) { var builder = new IndentedStringBuilder(" "); - string serializationLogic = type.DeserializeType(this.Scope, variableName); + string serializationLogic = GetDeserializationString(type, variableName, variableName); return builder.AppendLine(serializationLogic).ToString(); } @@ -119,9 +119,9 @@ public override List ClassNamespaces get { return new List - { - "MsRestAzure" - }; + { + "MsRestAzure" + }; } } diff --git a/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureModelTemplateModel.cs b/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureModelTemplateModel.cs index 7d12b53d6974e..f99ce93115376 100644 --- a/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureModelTemplateModel.cs +++ b/AutoRest/Generators/Ruby/Azure.Ruby/TemplateModels/AzureModelTemplateModel.cs @@ -2,10 +2,10 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.Rest.Generator.Azure.Ruby.Templates; using Microsoft.Rest.Generator.ClientModel; using Microsoft.Rest.Generator.Ruby; using Microsoft.Rest.Generator.Utilities; -using Microsoft.Rest.Generator.Azure.Ruby.Templates; namespace Microsoft.Rest.Generator.Azure.Ruby { @@ -45,36 +45,6 @@ public override string GetBaseTypeName() return string.Empty; } - - /// - /// Generates code for model serialization. - /// - /// Variable serialize model from. - /// The type of the model. - /// The code for serialization in string format. - public override string SerializeProperty(string variableName, IType type) - { - var builder = new IndentedStringBuilder(" "); - - string serializationLogic = type.AzureSerializeType(this.Scope, variableName); - builder.AppendLine(serializationLogic); - - return builder.ToString(); - } - - /// - /// Generates code for model deserialization. - /// - /// Variable deserialize model from. - /// The type of the model. - /// The code for вуserialization in string format. - public override string DeserializeProperty(string variableName, IType type) - { - var builder = new IndentedStringBuilder(" "); - - string serializationLogic = type.AzureDeserializeType(this.Scope, variableName); - return builder.AppendLine(serializationLogic).ToString(); - } /// /// Gets the list of modules/classes which need to be included. diff --git a/AutoRest/Generators/Ruby/Azure.Ruby/Templates/AzureMethodTemplate.cshtml b/AutoRest/Generators/Ruby/Azure.Ruby/Templates/AzureMethodTemplate.cshtml index ce6484d4ba299..ad775f288cca0 100644 --- a/AutoRest/Generators/Ruby/Azure.Ruby/Templates/AzureMethodTemplate.cshtml +++ b/AutoRest/Generators/Ruby/Azure.Ruby/Templates/AzureMethodTemplate.cshtml @@ -37,7 +37,10 @@ def @(Model.Name)(@(Model.MethodParameterDeclaration)) promise = promise.then do |response| # Defining deserialization method. deserialize_method = lambda do |parsed_response| - @(Model.DeserializePollingResponse("parsed_response", Model.ReturnType.Body)) + @if(Model.ReturnType.Body != null) + { + @:@(Model.DeserializePollingResponse("parsed_response", Model.ReturnType.Body)) + } end @EmptyLine @@ -79,7 +82,10 @@ def @(Model.Name)(@(Model.MethodParameterDeclaration)) promise = promise.then do |response| # Defining deserialization method. deserialize_method = lambda do |parsed_response| - @(Model.DeserializePollingResponse("parsed_response", Model.ReturnType.Body)) + @if(Model.ReturnType.Body != null) + { + @:@(Model.DeserializePollingResponse("parsed_response", Model.ReturnType.Body)) + } end @EmptyLine diff --git a/AutoRest/Generators/Ruby/Ruby.Tests/RspecTests/string_spec.rb b/AutoRest/Generators/Ruby/Ruby.Tests/RspecTests/string_spec.rb index 912e5a87b6982..223d1cfdb6c1e 100644 --- a/AutoRest/Generators/Ruby/Ruby.Tests/RspecTests/string_spec.rb +++ b/AutoRest/Generators/Ruby/Ruby.Tests/RspecTests/string_spec.rb @@ -15,6 +15,8 @@ client = AutoRestSwaggerBATService.new(@credentials, @base_url) @string_client = StringModule::String.new(client) + + @enum_client = StringModule::Enum.new(client) end it 'should create test service' do @@ -62,4 +64,12 @@ expect(result.response.status).to eq(200) expect(result.body).to be_nil end + it 'should support valid enum valid value' do + result = @enum_client.get_not_expandable_async().value! + expect(result.response.status).to eq(200) + expect(result.response.body).to include('red color') + end + it 'should correctly handle invalid values for enum' do + expect { @enum_client.put_not_expandable_async('orange color').value! }.to raise_error(MsRest::ValidationError) + end end \ No newline at end of file diff --git a/AutoRest/Generators/Ruby/Ruby/ClientModelExtensions.cs b/AutoRest/Generators/Ruby/Ruby/ClientModelExtensions.cs index d5bd90e7ea0dd..05385f5a34fad 100644 --- a/AutoRest/Generators/Ruby/Ruby/ClientModelExtensions.cs +++ b/AutoRest/Generators/Ruby/Ruby/ClientModelExtensions.cs @@ -2,13 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.Rest.Generator.ClientModel; +using System.Globalization; using System.Linq; +using Microsoft.Rest.Generator.ClientModel; namespace Microsoft.Rest.Generator.Ruby.TemplateModels { - using Utilities; using System.Collections.Generic; + using System.Reflection; + using Utilities; /// /// Keeps a few aux method used across all templates/models. @@ -263,235 +265,490 @@ public static bool HasModelTypes(this ServiceClient client) } /// - /// Generates Ruby code in form of string for deserializing object of given type. + /// Determines whether one composite type derives directly or indirectly from another. /// - /// Type of object needs to be deserialized. - /// Current scope. - /// Reference to object which needs to be deserialized. - /// Generated Ruby code in form of string. - public static string DeserializeType( - this IType type, - IScopeProvider scope, - string valueReference) + /// Type to test. + /// Type that may be an ancestor of this type. + /// true if the type is an ancestor, false otherwise. + public static bool DerivesFrom(this CompositeType type, CompositeType possibleAncestorType) { - var composite = type as CompositeType; - var sequence = type as SequenceType; - var dictionary = type as DictionaryType; - var primary = type as PrimaryType; - var enumType = type as EnumType; + return + type.BaseModelType != null && + (type.BaseModelType.Equals(possibleAncestorType) || + type.BaseModelType.DerivesFrom(possibleAncestorType)); + } + + /// + /// Constructs blueprint of the given . + /// + /// Type for which mapper being generated. + /// Serialized name to be used. + /// Parameter of the composite type to construct the parameter constraints. + /// Expand composite type if true otherwise specify class_name in the mapper. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// One of the example of the mapper is + /// { + /// required: false, + /// serialized_name: 'Fish', + /// type: { + /// name: 'Composite', + /// polymorphic_discriminator: 'fishtype', + /// uber_parent: 'Fish', + /// class_name: 'Fish', + /// model_properties: { + /// species: { + /// required: false, + /// serialized_name: 'species', + /// type: { + /// name: 'String' + /// } + /// }, + /// length: { + /// required: true, + /// serialized_name: 'length', + /// type: { + /// name: 'Double' + /// } + /// }, + /// siblings: { + /// required: false, + /// serialized_name: 'siblings', + /// type: { + /// name: 'Sequence', + /// element: { + /// required: false, + /// serialized_name: 'FishElementType', + /// type: { + /// name: 'Composite', + /// polymorphic_discriminator: 'fishtype', + /// uber_parent: 'Fish', + /// class_name: 'Fish' + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// + public static string ConstructMapper(this IType type, string serializedName, IParameter parameter, bool expandComposite) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } var builder = new IndentedStringBuilder(" "); + CompositeType composite = type as CompositeType; + SequenceType sequence = type as SequenceType; + DictionaryType dictionary = type as DictionaryType; + PrimaryType primary = type as PrimaryType; + EnumType enumType = type as EnumType; + if (enumType != null && enumType.ModelAsString) + { + primary = new PrimaryType(KnownPrimaryType.String); + } + builder.AppendLine("").Indent(); + + builder.AppendLine(type.AddMetaData(serializedName, parameter)); + if (primary != null) { - if (primary.Type == KnownPrimaryType.Int || primary.Type == KnownPrimaryType.Long) - { - return builder.AppendLine("{0} = Integer({0}) unless {0}.to_s.empty?", valueReference).ToString(); - } + builder.AppendLine(primary.ContructMapperForPrimaryType()); + } + else if (enumType != null && enumType.Name != null) + { + builder.AppendLine(enumType.ContructMapperForEnumType()); + } + else if (sequence != null) + { + builder.AppendLine(sequence.ContructMapperForSequenceType()); + } + else if (dictionary != null) + { + builder.AppendLine(dictionary.ContructMapperForDictionaryType()); + } + else if (composite != null) + { + builder.AppendLine(composite.ContructMapperForCompositeType(expandComposite)); + } + else + { + throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "{0} is not a supported Type.", type)); + } + return builder.ToString(); + } - if (primary.Type == KnownPrimaryType.Double) - { - return builder.AppendLine("{0} = Float({0}) unless {0}.to_s.empty?", valueReference).ToString(); - } + /// + /// Adds metadata to the given . + /// + /// Type for which metadata being generated. + /// Serialized name to be used. + /// Parameter of the composite type to construct the parameter constraints. + /// Metadata as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for IParameter for IType. + /// required: true | false, -- whether this property is required or not + /// read_only: true | false, -- whether this property is read only or not. Default is false + /// is_constant: true | false, -- whether this property is constant or not. Default is false + /// serialized_name: 'name' -- serialized name of the property if provided + /// default_value: 'value' -- default value of the property if provided + /// constraints: { -- constraints of the property + /// key: value, -- constraint name and value if any + /// ***: ***** + /// } + /// + private static string AddMetaData(this IType type, string serializedName, IParameter parameter) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } - if (primary.Type == KnownPrimaryType.ByteArray) - { - return builder.AppendLine("{0} = Base64.strict_decode64({0}).unpack('C*') unless {0}.to_s.empty?", valueReference).ToString(); - } + if (serializedName == null) + { + throw new ArgumentNullException(nameof(serializedName)); + } - if (primary.Type == KnownPrimaryType.Date) - { - return builder.AppendLine("{0} = MsRest::Serialization.deserialize_date({0}) unless {0}.to_s.empty?", valueReference).ToString(); - } + IndentedStringBuilder builder = new IndentedStringBuilder(" "); - if (primary.Type == KnownPrimaryType.DateTime) - { - return builder.AppendLine("{0} = DateTime.parse({0}) unless {0}.to_s.empty?", valueReference).ToString(); - } + Dictionary constraints = null; + string defaultValue = null; + bool isRequired = false; + bool isConstant = false; + bool isReadOnly = false; + var property = parameter as Property; + if (property != null) + { + isReadOnly = property.IsReadOnly; + } + if (parameter != null) + { + defaultValue = parameter.DefaultValue; + isRequired = parameter.IsRequired; + isConstant = parameter.IsConstant; + constraints = parameter.Constraints; + } - if (primary.Type == KnownPrimaryType.DateTimeRfc1123) - { - return builder.AppendLine("{0} = DateTime.parse({0}) unless {0}.to_s.empty?", valueReference).ToString(); - } + CompositeType composite = type as CompositeType; + if (composite != null && composite.ContainsConstantProperties && isRequired) + { + defaultValue = "{}"; + } - if (primary.Type == KnownPrimaryType.UnixTime) - { - return builder.AppendLine("{0} = DateTime.strptime({0}.to_s, '%s') unless {0}.to_s.empty?", valueReference).ToString(); - } + if (isRequired) + { + builder.AppendLine("required: true,"); } - else if (enumType != null && !string.IsNullOrEmpty(enumType.Name)) + else { - return builder - .AppendLine("if (!{0}.nil? && !{0}.empty?)", valueReference) - .AppendLine( - " enum_is_valid = {0}.constants.any? {{ |e| {0}.const_get(e).to_s.downcase == {1}.downcase }}", - enumType.Name, valueReference) - .AppendLine( - " warn 'Enum {0} does not contain ' + {1}.downcase + ', but was received from the server.' unless enum_is_valid", enumType.Name, valueReference) - .AppendLine("end") - .ToString(); + builder.AppendLine("required: false,"); } - else if (sequence != null) + if (isReadOnly) + { + builder.AppendLine("read_only: true,"); + } + if (isConstant) { - var elementVar = scope.GetUniqueName("element"); - var innerSerialization = sequence.ElementType.DeserializeType(scope, elementVar); + builder.AppendLine("is_constant: true,"); + } + if (serializedName != null) + { + builder.AppendLine("serialized_name: '{0}',", serializedName); + } + if (defaultValue != null) + { + builder.AppendLine("default_value: {0},", defaultValue); + } - if (!string.IsNullOrEmpty(innerSerialization)) + if (constraints != null && constraints.Count > 0) + { + builder.AppendLine("constraints: {").Indent(); + var keys = constraints.Keys.ToList(); + for (int j = 0; j < keys.Count; j++) { - return - builder - .AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("deserialized_{0} = []", sequence.Name.ToLower()) - .AppendLine("{0}.each do |{1}|", valueReference, elementVar) - .Indent() - .AppendLine(innerSerialization) - .AppendLine("deserialized_{0}.push({1})", sequence.Name.ToLower(), elementVar) - .Outdent() - .AppendLine("end") - .AppendLine("{0} = deserialized_{1}", valueReference, sequence.Name.ToLower()) - .Outdent() - .AppendLine("end") - .ToString(); + var constraintValue = constraints[keys[j]]; + if (keys[j] == Constraint.Pattern) + { + constraintValue = string.Format(CultureInfo.InvariantCulture, "'{0}'", constraintValue); + } + if (j != keys.Count - 1) + { + builder.AppendLine("{0}: {1},", keys[j], constraintValue); + } + else + { + builder.AppendLine("{0}: {1}", keys[j], constraintValue); + } } + builder.Outdent() + .AppendLine("},"); } - else if (dictionary != null) + + return builder.ToString(); + } + + /// + /// Constructs blueprint of the given . + /// + /// PrimaryType for which mapper being generated. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for PrimaryType. + /// type: { + /// name: 'Boolean' -- This value should be one of the KnownPrimaryType + /// } + /// + private static string ContructMapperForPrimaryType(this PrimaryType primary) + { + if (primary == null) { - var valueVar = scope.GetUniqueName("valueElement"); - var innerSerialization = dictionary.ValueType.DeserializeType(scope, valueVar); - if (!string.IsNullOrEmpty(innerSerialization)) - { - return builder.AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("{0}.each do |key, {1}|", valueReference, valueVar) - .Indent() - .AppendLine(innerSerialization) - .AppendLine("{0}[key] = {1}", valueReference, valueVar) - .Outdent() - .AppendLine("end") - .Outdent() - .AppendLine("end").ToString(); - } + throw new ArgumentNullException(nameof(primary)); } - else if (composite != null) + + IndentedStringBuilder builder = new IndentedStringBuilder(" "); + + if (primary.Type == KnownPrimaryType.Boolean) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Boolean'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Double) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Double'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Int || primary.Type == KnownPrimaryType.Long || + primary.Type == KnownPrimaryType.Decimal) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Number'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.String || primary.Type == KnownPrimaryType.Uuid) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'String'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.ByteArray) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'ByteArray'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Base64Url) { - return builder.AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("{0} = {1}.deserialize_object({0})", valueReference, composite.Name) - .Outdent() - .AppendLine("end").ToString(); + builder.AppendLine("type: {").Indent().AppendLine("name: 'Base64Url'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Date) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Date'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.DateTime) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'DateTime'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.DateTimeRfc1123) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'DateTimeRfc1123'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.TimeSpan) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'TimeSpan'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.UnixTime) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'UnixTime'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Object) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Object'").Outdent().AppendLine("}"); + } + else if (primary.Type == KnownPrimaryType.Stream) + { + builder.AppendLine("type: {").Indent().AppendLine("name: 'Stream'").Outdent().AppendLine("}"); + } + else + { + throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "{0} is not a supported primary Type for {1}.", primary.Type, primary.SerializedName)); } - return string.Empty; + return builder.ToString(); } /// - /// Generates Ruby code in form of string for serializing object of given type. + /// Constructs blueprint of the given . /// - /// Type of object needs to be serialized. - /// Current scope. - /// Reference to object which needs to serialized. - /// Generated Ruby code in form of string. - public static string SerializeType( - this IType type, - IScopeProvider scope, - string valueReference) + /// EnumType for which mapper being generated. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for EnumType. + /// type: { + /// name: 'Enum', + /// module: 'module_name' -- name of the module to be looked for enum values + /// } + /// + private static string ContructMapperForEnumType(this EnumType enumeration) { - var composite = type as CompositeType; - var sequence = type as SequenceType; - var dictionary = type as DictionaryType; - var primary = type as PrimaryType; + if (enumeration == null) + { + throw new ArgumentNullException(nameof(enumeration)); + } - var builder = new IndentedStringBuilder(" "); + IndentedStringBuilder builder = new IndentedStringBuilder(" "); - if (primary != null) + builder.AppendLine("type: {").Indent() + .AppendLine("name: 'Enum',") + .AppendLine("module: '{0}'", enumeration.Name).Outdent() + .AppendLine("}"); + + return builder.ToString(); + } + + /// + /// Constructs blueprint of the given . + /// + /// SequenceType for which mapper being generated. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for SequenceType. + /// type: { + /// name: 'Sequence', + /// element: { + /// *** -- mapper of the IType from the sequence element + /// } + /// } + /// + private static string ContructMapperForSequenceType(this SequenceType sequence) + { + if (sequence == null) { - if (primary.Type == KnownPrimaryType.ByteArray) - { - return builder.AppendLine("{0} = Base64.strict_encode64({0}.pack('c*'))", valueReference).ToString(); - } + throw new ArgumentNullException(nameof(sequence)); + } - if (primary.Type == KnownPrimaryType.DateTime) - { - return builder.AppendLine("{0} = {0}.new_offset(0).strftime('%FT%TZ')", valueReference).ToString(); - } + IndentedStringBuilder builder = new IndentedStringBuilder(" "); - if (primary.Type == KnownPrimaryType.DateTimeRfc1123) - { - return builder.AppendLine("{0} = {0}.new_offset(0).strftime('%a, %d %b %Y %H:%M:%S GMT')", valueReference).ToString(); - } + builder.AppendLine("type: {").Indent() + .AppendLine("name: 'Sequence',") + .AppendLine("element: {").Indent() + .AppendLine("{0}", sequence.ElementType.ConstructMapper(sequence.ElementType.Name + "ElementType", null, false)).Outdent() + .AppendLine("}").Outdent() + .AppendLine("}"); - if (primary.Type == KnownPrimaryType.UnixTime) - { - return builder.AppendLine("{0} = {0}.new_offset(0).strftime('%s')", valueReference).ToString(); - } + return builder.ToString(); + } + + /// + /// Constructs blueprint of the given . + /// + /// DictionaryType for which mapper being generated. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for DictionaryType. + /// type: { + /// name: 'Dictionary', + /// value: { + /// *** -- mapper of the IType from the value type of dictionary + /// } + /// } + /// + private static string ContructMapperForDictionaryType(this DictionaryType dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(nameof(dictionary)); } - else if (sequence != null) + + IndentedStringBuilder builder = new IndentedStringBuilder(" "); + + builder.AppendLine("type: {").Indent() + .AppendLine("name: 'Dictionary',") + .AppendLine("value: {").Indent() + .AppendLine("{0}", dictionary.ValueType.ConstructMapper(dictionary.ValueType.Name + "ElementType", null, false)).Outdent() + .AppendLine("}").Outdent() + .AppendLine("}"); + + return builder.ToString(); + } + + /// + /// Constructs blueprint of the given . + /// + /// CompositeType for which mapper being generated. + /// Expand composite type if true otherwise specify class_name in the mapper. + /// Mapper for the as string. + /// Thrown when a required parameter is null. + /// + /// The below example shows possible mapper string for CompositeType. + /// type: { + /// name: 'Composite', + /// polymorphic_discriminator: 'property_name', -- name of the property for polymorphic discriminator + /// Used only when x-ms-discriminator-value applied + /// uber_parent: 'parent_class_name', -- name of the topmost level class on inheritance hierarchy + /// Used only when x-ms-discriminator-value applied + /// class_name: 'class_name', -- name of the modeled class + /// Used when is false + /// model_properties: { -- expanded properties of the model + /// Used when is true + /// property_name : { -- name of the property of this composite type + /// *** -- mapper of the IType from the type of the property + /// } + /// } + /// } + /// + private static string ContructMapperForCompositeType(this CompositeType composite, bool expandComposite) + { + if (composite == null) { - var elementVar = scope.GetUniqueName("element"); - var innerSerialization = sequence.ElementType.SerializeType(scope, elementVar); + throw new ArgumentNullException(nameof(composite)); + } + + IndentedStringBuilder builder = new IndentedStringBuilder(" "); + + builder.AppendLine("type: {").Indent() + .AppendLine("name: 'Composite',"); - if (!string.IsNullOrEmpty(innerSerialization)) + if (composite.PolymorphicDiscriminator != null) + { + builder.AppendLine("polymorphic_discriminator: '{0}',", composite.PolymorphicDiscriminator); + var polymorphicType = composite; + while (polymorphicType.BaseModelType != null) { - return - builder - .AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("serialized{0} = []", sequence.Name) - .AppendLine("{0}.each do |{1}|", valueReference, elementVar) - .Indent() - .AppendLine(innerSerialization) - .AppendLine("serialized{0}.push({1})", sequence.Name.ToPascalCase(), elementVar) - .Outdent() - .AppendLine("end") - .AppendLine("{0} = serialized{1}", valueReference, sequence.Name.ToPascalCase()) - .Outdent() - .AppendLine("end") - .ToString(); + polymorphicType = polymorphicType.BaseModelType; } + builder.AppendLine("uber_parent: '{0}',", polymorphicType.Name); } - else if (dictionary != null) + if (!expandComposite) { - var valueVar = scope.GetUniqueName("valueElement"); - var innerSerialization = dictionary.ValueType.SerializeType(scope, valueVar); - if (!string.IsNullOrEmpty(innerSerialization)) - { - return builder.AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("{0}.each {{ |key, {1}|", valueReference, valueVar) - .Indent() - .AppendLine(innerSerialization) - .AppendLine("{0}[key] = {1}", valueReference, valueVar) - .Outdent() - .AppendLine("}") - .Outdent() - .AppendLine("end").ToString(); - } + builder.AppendLine("class_name: '{0}'", composite.Name).Outdent().AppendLine("}"); } - else if (composite != null) + else { - return builder.AppendLine("unless {0}.nil?", valueReference) - .Indent() - .AppendLine("{0} = {1}.serialize_object({0})", valueReference, composite.Name) - .Outdent() - .AppendLine("end").ToString(); + builder.AppendLine("class_name: '{0}',", composite.Name) + .AppendLine("model_properties: {").Indent(); + var composedPropertyList = new List(composite.ComposedProperties); + for (var i = 0; i < composedPropertyList.Count; i++) + { + var prop = composedPropertyList[i]; + var serializedPropertyName = prop.SerializedName; + + if (i != composedPropertyList.Count - 1) + { + builder.AppendLine("{0}: {{{1}}},", prop.Name, prop.Type.ConstructMapper(serializedPropertyName, prop, false)); + } + else + { + builder.AppendLine("{0}: {{{1}}}", prop.Name, prop.Type.ConstructMapper(serializedPropertyName, prop, false)); + } + } + // end of modelProperties and type + builder.Outdent(). + AppendLine("}").Outdent(). + AppendLine("}"); } - return string.Empty; - } - - /// - /// Determines whether one composite type derives directly or indirectly from another. - /// - /// Type to test. - /// Type that may be an ancestor of this type. - /// true if the type is an ancestor, false otherwise. - public static bool DerivesFrom(this CompositeType type, CompositeType possibleAncestorType) - { - return - type.BaseModelType != null && - (type.BaseModelType.Equals(possibleAncestorType) || - type.BaseModelType.DerivesFrom(possibleAncestorType)); + return builder.ToString(); } } } diff --git a/AutoRest/Generators/Ruby/Ruby/TemplateModels/MethodTemplateModel.cs b/AutoRest/Generators/Ruby/Ruby/TemplateModels/MethodTemplateModel.cs index e51e53727c910..0ef44d300a900 100644 --- a/AutoRest/Generators/Ruby/Ruby/TemplateModels/MethodTemplateModel.cs +++ b/AutoRest/Generators/Ruby/Ruby/TemplateModels/MethodTemplateModel.cs @@ -122,8 +122,11 @@ public virtual string SkipEncodingQueryParamsRbDict /// public virtual IEnumerable EncodingPathParams { - get { return AllPathParams.Where(p => !(p.Extensions.ContainsKey(Generator.Extensions.SkipUrlEncodingExtension) && - String.Equals(p.Extensions[Generator.Extensions.SkipUrlEncodingExtension].ToString(), "true", StringComparison.OrdinalIgnoreCase))); } + get + { + return AllPathParams.Where(p => !(p.Extensions.ContainsKey(Generator.Extensions.SkipUrlEncodingExtension) && + String.Equals(p.Extensions[Generator.Extensions.SkipUrlEncodingExtension].ToString(), "true", StringComparison.OrdinalIgnoreCase))); + } } /// @@ -133,7 +136,7 @@ public virtual IEnumerable SkipEncodingPathParams { get { - return AllPathParams.Where(p => + return AllPathParams.Where(p => (p.Extensions.ContainsKey(Generator.Extensions.SkipUrlEncodingExtension) && String.Equals(p.Extensions[Generator.Extensions.SkipUrlEncodingExtension].ToString(), "true", StringComparison.OrdinalIgnoreCase) && !p.Extensions.ContainsKey("hostParameter"))); @@ -207,7 +210,7 @@ public virtual string SetDefaultHeaders /// Gets the list of method paramater templates. /// public List ParameterTemplateModels { get; private set; } - + /// /// Gets the list of parameter which need to be included into HTTP header. /// @@ -271,7 +274,7 @@ public string MethodParameterDeclaration PrimaryType type = parameter.Type as PrimaryType; if (type != null) { - if (type.Type == KnownPrimaryType.Boolean || type.Type == KnownPrimaryType.Double || + if (type.Type == KnownPrimaryType.Boolean || type.Type == KnownPrimaryType.Double || type.Type == KnownPrimaryType.Int || type.Type == KnownPrimaryType.Long || type.Type == KnownPrimaryType.String) { format = "{0} = " + parameter.DefaultValue; @@ -369,32 +372,12 @@ public virtual string CreateDeserializationString(string inputVariable, IType ty builder.AppendLine("{0} = {1}.to_s.empty? ? nil : JSON.load({1})", tempVariable, inputVariable); // Secondly parse each js object into appropriate Ruby type (DateTime, Byte array, etc.) - // and overwrite temporary variable variable value. - string deserializationLogic = type.DeserializeType(this.Scope, tempVariable); + // and overwrite temporary variable value. + string deserializationLogic = GetDeserializationString(type, outputVariable, tempVariable); builder.AppendLine(deserializationLogic); // Assigning value of temporary variable to the output variable. - return builder.AppendLine("{0} = {1}", outputVariable, tempVariable).ToString(); - } - - /// - /// Creates a code in form of string which serializes given input variable of given type. - /// - /// The input variable. - /// The type of input variable. - /// The output variable. - /// The serialization code. - public virtual string CreateSerializationString(string inputVariable, IType type, string outputVariable) - { - var builder = new IndentedStringBuilder(" "); - - // Firstly recursively serialize each component of the object. - string serializationLogic = type.SerializeType(this.Scope, inputVariable); - - builder.AppendLine(serializationLogic); - - // After that - generate JSON object after serializing each component. - return builder.AppendLine("{0} = {1} != nil ? JSON.generate({1}, quirks_mode: true) : nil", outputVariable, inputVariable).ToString(); + return builder.ToString(); } /// @@ -517,5 +500,61 @@ private string UrlPathNameFromPathPattern(string urlPathParamName) } return urlPathParamName; } + + /// + /// Constructs mapper for the request body. + /// + /// Name of the output variable. + /// Mapper for the request body as string. + public string ConstructRequestBodyMapper(string outputVariable = "request_mapper") + { + var builder = new IndentedStringBuilder(" "); + if (RequestBody.Type is CompositeType) + { + builder.AppendLine("{0} = {1}.mapper()", outputVariable, RequestBody.Type.Name); + } + else + { + builder.AppendLine("{0} = {{{1}}}", outputVariable, + RequestBody.Type.ConstructMapper(RequestBody.SerializedName, RequestBody, false)); + } + return builder.ToString(); + } + + /// + /// Creates deserialization logic for the given . + /// + /// Type for which deserialization logic being constructed. + /// Reference variable name. + /// Response variable name. + /// Deserialization logic for the given as string. + /// Thrown when a required parameter is null. + public string GetDeserializationString(IType type, string valueReference = "result", string responseVariable = "parsed_response") + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + var builder = new IndentedStringBuilder(" "); + if (type is CompositeType) + { + builder.AppendLine("result_mapper = {0}.mapper()", type.Name); + } + else + { + builder.AppendLine("result_mapper = {{{0}}}", type.ConstructMapper(responseVariable, null, false)); + } + if (Group == null) + { + builder.AppendLine("{1} = self.deserialize(result_mapper, {0}, '{1}')", responseVariable, valueReference); + } + else + { + builder.AppendLine("{1} = @client.deserialize(result_mapper, {0}, '{1}')", responseVariable, valueReference); + } + + return builder.ToString(); + } } } \ No newline at end of file diff --git a/AutoRest/Generators/Ruby/Ruby/TemplateModels/ModelTemplateModel.cs b/AutoRest/Generators/Ruby/Ruby/TemplateModels/ModelTemplateModel.cs index 1e0ca8ff4f3e3..11f7968dc5c5a 100644 --- a/AutoRest/Generators/Ruby/Ruby/TemplateModels/ModelTemplateModel.cs +++ b/AutoRest/Generators/Ruby/Ruby/TemplateModels/ModelTemplateModel.cs @@ -4,8 +4,8 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Rest.Generator.ClientModel; -using Microsoft.Rest.Generator.Utilities; using Microsoft.Rest.Generator.Ruby.TemplateModels; +using Microsoft.Rest.Generator.Utilities; namespace Microsoft.Rest.Generator.Ruby { @@ -61,7 +61,7 @@ public virtual List ClassNamespaces { get { - return new List {}; + return new List { }; } } @@ -91,7 +91,7 @@ public bool IsPolymorphic /// public string PolymorphicDiscriminatorProperty { - get + get { if (string.IsNullOrEmpty(this.PolymorphicDiscriminator) && this.parent != null) { @@ -124,6 +124,22 @@ public ModelTemplateModel(CompositeType source, ISet allTypes) PropertyTemplateModels = new List(); source.Properties.ForEach(p => PropertyTemplateModels.Add(new PropertyTemplateModel(p))); + if (!string.IsNullOrEmpty(source.PolymorphicDiscriminator)) + { + if (!source.Properties.Any(p => p.Name == source.PolymorphicDiscriminator)) + { + var polymorphicProperty = new Property + { + IsRequired = true, + Name = source.PolymorphicDiscriminator, + SerializedName = source.PolymorphicDiscriminator, + Documentation = "Polymorhpic Discriminator", + Type = new PrimaryType(KnownPrimaryType.String) + }; + source.Properties.Add(polymorphicProperty); + } + } + if (source.BaseModelType != null) { this.parent = new ModelTemplateModel(source.BaseModelType, allTypes); @@ -133,42 +149,24 @@ public ModelTemplateModel(CompositeType source, ISet allTypes) } /// - /// Generates code for model serialization. + /// Returns code for declaring inheritance. /// - /// Variable serialize model from. - /// The type of the model. - /// The code for serialization in string format. - public virtual string SerializeProperty(string variableName, IType type) + /// Code for declaring inheritance. + public virtual string GetBaseTypeName() { - var builder = new IndentedStringBuilder(" "); - - string serializationLogic = type.SerializeType(this.Scope, variableName); - builder.AppendLine(serializationLogic); - - return builder.ToString(); + return this.BaseModelType != null ? " < " + this.BaseModelType.Name : string.Empty; } /// - /// Generates code for model deserialization. + /// Constructs mapper for the model class. /// - /// Variable deserialize model from. - /// The type of the model. - /// The code for вуserialization in string format. - public virtual string DeserializeProperty(string variableName, IType type) + /// Mapper as string for this model class. + public virtual string ConstructModelMapper() { + var modelMapper = this.ConstructMapper(SerializedName, null, true); var builder = new IndentedStringBuilder(" "); - - string serializationLogic = type.DeserializeType(this.Scope, variableName); - return builder.AppendLine(serializationLogic).ToString(); - } - - /// - /// Returns code for declaring inheritance. - /// - /// Code for declaring inheritance. - public virtual string GetBaseTypeName() - { - return this.BaseModelType != null ? " < " + this.BaseModelType.Name : string.Empty; + builder.AppendLine("{{{0}}}", modelMapper); + return builder.ToString(); } } } \ No newline at end of file diff --git a/AutoRest/Generators/Ruby/Ruby/Templates/MethodTemplate.cshtml b/AutoRest/Generators/Ruby/Ruby/Templates/MethodTemplate.cshtml index b8d70fabe5b9a..d4efe8f433b75 100644 --- a/AutoRest/Generators/Ruby/Ruby/Templates/MethodTemplate.cshtml +++ b/AutoRest/Generators/Ruby/Ruby/Templates/MethodTemplate.cshtml @@ -95,11 +95,7 @@ def @(Model.Name)_async(@(Model.MethodParameterDeclaration)) if (parameter.IsConstant) { @:@(parameter.Name) = @(parameter.DefaultValue) - } - else - { - @:@(parameter.Type.ValidateType(Model.Scope, parameter.Name)) - } + } } request_headers = {} @if (Model.Parameters.Any(p => p.Location == ParameterLocation.Header)) @@ -120,9 +116,13 @@ def @(Model.Name)_async(@(Model.MethodParameterDeclaration)) @if (Model.RequestBody != null) { @EmptyLine - @:# Serialize Request @:request_headers['Content-Type'] = '@(Model.RequestContentType)' - @:@Model.CreateSerializationString(Model.RequestBody.Name, Model.RequestBody.Type, "request_content") + @EmptyLine + @:# Serialize Request + @:@Model.ConstructRequestBodyMapper("request_mapper") + @:request_content = @(Model.ClientReference).serialize(request_mapper, @(Model.RequestBody.Name), '@(Model.RequestBody.Name)') + @:request_content = request_content != nil ? JSON.generate(request_content, quirks_mode: true) : nil + @EmptyLine } path_template = '@Model.Url' diff --git a/AutoRest/Generators/Ruby/Ruby/Templates/ModelTemplate.cshtml b/AutoRest/Generators/Ruby/Ruby/Templates/ModelTemplate.cshtml index 7a79e622cef77..1107a06784d40 100644 --- a/AutoRest/Generators/Ruby/Ruby/Templates/ModelTemplate.cshtml +++ b/AutoRest/Generators/Ruby/Ruby/Templates/ModelTemplate.cshtml @@ -31,7 +31,18 @@ module @(Settings.Namespace) @:@@@@discriminatorMap["@derivedType.SerializedName"] = "@derivedType.Name.ToLowerInvariant()" } } - + + @if (Model.IsPolymorphic) + { + @EmptyLine + @:def initialize + @: @@@Model.PolymorphicDiscriminatorProperty = "@Model.SerializedName" + @:end + @EmptyLine + @:attr_accessor :@Model.PolymorphicDiscriminatorProperty + @EmptyLine + } + @foreach (var property in Model.PropertyTemplateModels) { @:@WrapComment("# ", string.Format("@return {0}{1}", property.Type.GetYardDocumentation(), property.Documentation)) @@ -41,115 +52,14 @@ module @(Settings.Namespace) @: } - # - # Validate the object. Throws ValidationError if validation fails. - # - def validate - @{ - bool anythingToValidate = false; - foreach (var property in Model.Properties.Where(p => p.IsRequired && !p.IsReadOnly && p.Type.IsNullable())) - { - anythingToValidate = true; - @:fail MsRest::ValidationError, 'property @property.Name is nil' if @@@(property.Name).nil? - } - foreach (var property in Model.Properties.Where(p => !(p.Type is PrimaryType))) - { - anythingToValidate = true; - @:@property.Type.ValidateType(Model.Scope, string.Format("@{0}", property.Name)) - @: - } - if (!anythingToValidate) - { - @:# Nothing to validate - } - } - end - - @EmptyLine - # - # Serializes given Model object into Ruby Hash. - @WrapComment("# ", string.Format("@param {0} {1}", "object", "Model object to serialize.")) - @WrapComment("# ", string.Format("@return [Hash] {0}", "Serialized object in form of Ruby Hash.")) - # - def self.serialize_object(object) - object.validate - output_object = {} - - @if (Model.IsPolymorphic) - { - @EmptyLine - @:unless object.@(Model.PolymorphicDiscriminatorProperty).nil? or object.@(Model.PolymorphicDiscriminatorProperty) == "@Model.SerializedName" - @:class_name = @@@@discriminatorMap[object.@Model.PolymorphicDiscriminatorProperty].capitalize - @:class_instance = Models.const_get(class_name) - @foreach (var ns in Model.ClassNamespaces) - { - @:class_instance = @(ns).const_get(class_name) if class_instance.nil? - } - @:output_object = class_instance.serialize_object(object) - @:else - @:output_object['@Model.PolymorphicDiscriminatorProperty'] = object.@Model.PolymorphicDiscriminatorProperty - @:end - } - - @foreach (var property in Model.ComposedProperties.OrderByDescending(x => x.IsRequired)) - { - @EmptyLine - @:serialized_property = @("object." + property.Name) - @:@Model.SerializeProperty("serialized_property", property.Type) - @:output_object['@(property.SerializedName)'] = serialized_property unless serialized_property.nil? - } - - @EmptyLine - output_object - end - @EmptyLine # - # Deserializes given Ruby Hash into Model object. - @WrapComment("# ", string.Format("@param {0} [Hash] {1}", "object", "Ruby Hash object to deserialize.")) - @WrapComment("# ", string.Format("@return [{0}] {1}", Model.Name, "Deserialized object.")) + @WrapComment("# ", string.Format("Mapper for {0} class as Ruby Hash.", Model.Name)) + # This will be used for serialization/deserialization. # - def self.deserialize_object(object) - return if object.nil? - output_object = @(Model.Name).new - - @if (Model.IsPolymorphic) - { - @EmptyLine - @:unless object['@(Model.PolymorphicDiscriminatorProperty)'].nil? or object['@(Model.PolymorphicDiscriminatorProperty)'] == "@Model.SerializedName" - @:class_name = @@@@discriminatorMap[object['@Model.PolymorphicDiscriminatorProperty']].capitalize - @:class_instance = Models.const_get(class_name) - @foreach (var ns in Model.ClassNamespaces) - { - @:class_instance = @(ns).const_get(class_name) if class_instance.nil? - } - @:output_object = class_instance.deserialize_object(object) - @:else - @:output_object.@Model.PolymorphicDiscriminatorProperty = object['@Model.PolymorphicDiscriminatorProperty'] - @:end - } - - @foreach (var property in Model.ComposedProperties.OrderByDescending(x => x.IsRequired)) - { - @EmptyLine - @:deserialized_property = @(string.Format("object['{0}']", property.SerializedName)) - @:@Model.DeserializeProperty("deserialized_property", property.Type) - @:output_object.@(property.Name) = deserialized_property - } - - @EmptyLine - output_object + def self.mapper() + @(Model.ConstructModelMapper()) end - - @if (Model.IsPolymorphic) - { - @EmptyLine - @:def initialize - @: @@@Model.PolymorphicDiscriminatorProperty = "@Model.SerializedName" - @:end - @EmptyLine - @:attr_accessor :@Model.PolymorphicDiscriminatorProperty - } end end end diff --git a/AutoRest/Generators/Ruby/Ruby/Templates/ServiceClientTemplate.cshtml b/AutoRest/Generators/Ruby/Ruby/Templates/ServiceClientTemplate.cshtml index a4f57b3169f4e..2eaa18e06e2c6 100644 --- a/AutoRest/Generators/Ruby/Ruby/Templates/ServiceClientTemplate.cshtml +++ b/AutoRest/Generators/Ruby/Ruby/Templates/ServiceClientTemplate.cshtml @@ -16,6 +16,7 @@ module @Settings.Namespace { @:include @(Settings.Namespace)::Models } + include MsRest::Serialization @foreach (var include in Model.Includes) { @:include @include diff --git a/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/resource.rb b/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/resource.rb index c539a42e865a7..7b12fd2d0497c 100644 --- a/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/resource.rb +++ b/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/resource.rb @@ -23,67 +23,61 @@ class Resource # @return [Hash{String => String}] the tags attached to resources (optional). attr_accessor :tags - # - # Serializes given resource object into hash. - # @param object [Resource] resource object to serialize. - # - # @return [Hash] hash representation of resource. - def self.serialize_object(object) - object.validate - output_object = {} - - serialized_property = object.id - output_object['id'] = serialized_property unless serialized_property.nil? - - serialized_property = object.name - output_object['name'] = serialized_property unless serialized_property.nil? - - serialized_property = object.type - output_object['type'] = serialized_property unless serialized_property.nil? - - serialized_property = object.location - output_object['location'] = serialized_property unless serialized_property.nil? - - serialized_property = object.tags - output_object['tags'] = serialized_property unless serialized_property.nil? - - output_object + def self.mapper + { + required: false, + serialized_name: 'Resource', + type: { + name: 'Composite', + class_name: 'Resource', + model_properties: { + id: { + required: false, + serialized_name: 'id', + type: { + name: 'String' + } + }, + name: { + required: false, + read_only: true, + serialized_name: 'name', + type: { + name: 'String' + } + }, + type: { + required: false, + read_only: true, + serialized_name: 'type', + type: { + name: 'String' + } + }, + location: { + required: false, + serialized_name: 'location', + type: { + name: 'String' + } + }, + tags: { + required: false, + serialized_name: 'tags', + type: { + name: 'Dictionary', + value: { + required: false, + serialized_name: 'StringElementType', + type: { + name: 'String' + } + } + } + } + } + } + } end - - # - # Deserializes given hash object into resource. - # @param object [Hash] resource in hash representation to deserialize. - # - # @return [Resource] deserialized resource. - def self.deserialize_object(object) - return if object.nil? - output_object = Resource.new - - deserialized_property = object['id'] - output_object.id = deserialized_property - - deserialized_property = object['name'] - output_object.name = deserialized_property - - deserialized_property = object['type'] - output_object.type = deserialized_property - - deserialized_property = object['location'] - output_object.location = deserialized_property - - deserialized_property = object['tags'] - output_object.tags = deserialized_property - - output_object.validate - output_object - end - - # - # Validates the resource. Throws error if there is any property is incorrect. - # - def validate - fail MsRest::ValidationError, 'Location cannot be nil in the Resource object' if @location.nil? - end - end end diff --git a/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/sub_resource.rb b/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/sub_resource.rb index 6493aaa5948c7..9cd9d31eae2cf 100644 --- a/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/sub_resource.rb +++ b/ClientRuntimes/Ruby/ms-rest-azure/lib/ms_rest_azure/sub_resource.rb @@ -11,41 +11,24 @@ class SubResource # @return [String] the id of the subresource. attr_accessor :id - # - # Serializes given subresource object into hash. - # @param object [SubResource] subresource object to serialize. - # - # @return [Hash] hash representation of subresource. - def self.serialize_object(object) - object.validate - output_object = {} - - serialized_property = object.id - output_object['id'] = serialized_property unless serialized_property.nil? - - output_object - end - - # - # Deserializes given hash object into subresource. - # @param object [Hash] subresource in hash representation to deserialize. - # - # @return [SubResource] deserialized subresource. - def self.deserialize_object(object) - return if object.nil? - output_object = SubResource.new - - deserialized_property = object['id'] - output_object.id = deserialized_property - - output_object.validate - output_object - end - - # - # Validates the subresource. Throws error if there is any property is incorrect. - # - def validate + def self.mapper + { + required: false, + serialized_name: 'SubResource', + type: { + name: 'Composite', + class_name: 'SubResource', + model_properties: { + id: { + required: false, + serialized_name: 'id', + type: { + name: 'String' + } + } + } + } + } end end end diff --git a/ClientRuntimes/Ruby/ms-rest-azure/spec/resource_spec.rb b/ClientRuntimes/Ruby/ms-rest-azure/spec/resource_spec.rb index 977c8ddb7cb3f..f02355b1371d9 100644 --- a/ClientRuntimes/Ruby/ms-rest-azure/spec/resource_spec.rb +++ b/ClientRuntimes/Ruby/ms-rest-azure/spec/resource_spec.rb @@ -6,8 +6,15 @@ require 'ms_rest_azure' module MsRestAzure + class Helper + include MsRest::Serialization + end describe Resource do + before (:all) do + @helper = Helper.new + end + it 'should serialize Resource correctly' do resource = Resource.new resource.id = 'id' @@ -19,7 +26,8 @@ module MsRestAzure 'tag2' => 'tag2_value' } - res = Resource.serialize_object(resource) + allow_any_instance_of(MsRest::Serialization::Serialization).to receive(:get_model).and_return(Resource) + res = @helper.serialize(Resource.mapper(), resource, 'resource') expect(res).to be_a(Hash) expect(res['id']).to eq('id') @@ -41,7 +49,8 @@ module MsRestAzure } } - res = Resource.deserialize_object(resource_hash) + allow_any_instance_of(MsRest::Serialization::Serialization).to receive(:get_model).and_return(Resource) + res = @helper.deserialize(Resource.mapper(), resource_hash, 'resource_hash') expect(res).to be_a(Resource) expect(res.id).to eq('id') @@ -50,16 +59,5 @@ module MsRestAzure expect(res.location).to eq('location') expect(res.tags).to eq({ 'tag1' => 'tag1_value', 'tag2' => 'tag2_value' }) end - - it 'should throw error if location isn\'t provided' do - resource_hash = { - 'id' => 'id', - 'name' => 'name', - 'type' => 'type' - } - - expect { Resource.deserialize_object(resource_hash) }.to raise_error(MsRest::ValidationError) - end end - end \ No newline at end of file diff --git a/ClientRuntimes/Ruby/ms-rest-azure/spec/sub_resource_spec.rb b/ClientRuntimes/Ruby/ms-rest-azure/spec/sub_resource_spec.rb index d6c65dc31e55b..fe045ab333d38 100644 --- a/ClientRuntimes/Ruby/ms-rest-azure/spec/sub_resource_spec.rb +++ b/ClientRuntimes/Ruby/ms-rest-azure/spec/sub_resource_spec.rb @@ -6,13 +6,21 @@ require 'ms_rest_azure' module MsRestAzure + class Helper + include MsRest::Serialization + end describe SubResource do + before (:all) do + @helper = Helper.new + end + it 'should serialize SubResource correctly' do sub_resource = SubResource.new sub_resource.id = 'the_id' - sub_resource_serialized = SubResource.serialize_object(sub_resource) + allow_any_instance_of(MsRest::Serialization::Serialization).to receive(:get_model).and_return(SubResource) + sub_resource_serialized = @helper.serialize(SubResource.mapper(), sub_resource, 'sub_resource') expect(sub_resource_serialized).to be_a(Hash) expect(sub_resource_serialized['id']).to eq('the_id') @@ -22,12 +30,11 @@ module MsRestAzure sub_resource_hash = { 'id' => 'the_id' } - - sub_resource = SubResource.deserialize_object(sub_resource_hash) + allow_any_instance_of(MsRest::Serialization::Serialization).to receive(:get_model).and_return(SubResource) + sub_resource = @helper.deserialize(SubResource.mapper(), sub_resource_hash, 'sub_resource_hash') expect(sub_resource).to be_a(SubResource) expect(sub_resource.id).to eq('the_id') end end - end \ No newline at end of file diff --git a/ClientRuntimes/Ruby/ms-rest/lib/ms_rest.rb b/ClientRuntimes/Ruby/ms-rest/lib/ms_rest.rb index 80a85e21a00bd..4ac4394dae640 100644 --- a/ClientRuntimes/Ruby/ms-rest/lib/ms_rest.rb +++ b/ClientRuntimes/Ruby/ms-rest/lib/ms_rest.rb @@ -24,4 +24,5 @@ require 'ms_rest/retry_policy_middleware' require 'ms_rest/service_client' -module MsRest; end +module MsRest end +module MsRest::Serialization end diff --git a/ClientRuntimes/Ruby/ms-rest/lib/ms_rest/serialization.rb b/ClientRuntimes/Ruby/ms-rest/lib/ms_rest/serialization.rb index ce633b3e9c99e..6772d5d90fde8 100644 --- a/ClientRuntimes/Ruby/ms-rest/lib/ms_rest/serialization.rb +++ b/ClientRuntimes/Ruby/ms-rest/lib/ms_rest/serialization.rb @@ -3,20 +3,364 @@ # Licensed under the MIT License. See License.txt in the project root for license information. module MsRest + # Base module for Ruby serialization and deserialization. # - # Class which keeps the auxiliary for (de)serializing JSON requests and responses from server. - # - class Serialization + # Provides methods to serialize Ruby object into Ruby Hash and + # to deserialize Ruby Hash into Ruby object. + module Serialization + # + # Deserialize the response from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # @param object_name [String] Name of the deserialized object. + # + def deserialize(mapper, response_body, object_name) + serialization = Serialization.new(self) + serialization.deserialize(mapper, response_body, object_name) + end + + # + # Serialize the Ruby object into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # @param object_name [String] Name of the serialized object. + # + def serialize(mapper, object, object_name) + serialization = Serialization.new(self) + serialization.serialize(mapper, object, object_name) + end # - # Deserializes given string value into Ruby Date object. - # @param [String] string_value string value to deserialize. + # Class to handle serialization & deserialization. # - # @return [Date] deserialized Date object. - def self.deserialize_date(string_value) - result = Timeliness.parse(string_value, :strict => true) - fail DeserializationError.new('Error occured in deserializing the response', nil, nil, string_value) if result.nil? - return ::Date.parse(result.to_s) + class Serialization + def initialize(context) + @context = context + end + + # + # Deserialize the response from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # @param object_name [String] Name of the deserialized object. + # + def deserialize(mapper, response_body, object_name) + return response_body if response_body.nil? + + object_name = mapper[:serialized_name] unless object_name.nil? + mapper_type = mapper[:type][:name] + + if !mapper_type.match(/^(Number|Double|ByteArray|Boolean|Date|DateTime|DateTimeRfc1123|UnixTime|Enum|String|Object|Stream)$/i).nil? + payload = deserialize_primary_type(mapper, response_body) + elsif !mapper_type.match(/^Dictionary$/i).nil? + payload = deserialize_dictionary_type(mapper, response_body, object_name) + elsif !mapper_type.match(/^Composite$/i).nil? + payload = deserialize_composite_type(mapper, response_body, object_name) + elsif !mapper_type.match(/^Sequence$/i).nil? + payload = deserialize_sequence_type(mapper, response_body, object_name) + else + payload = "" + end + + payload = mapper[:default_value] if mapper[:is_constant] + + payload + end + + # + # Deserialize the response of known primary type from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # + def deserialize_primary_type(mapper, response_body) + result = "" + case mapper[:type][:name] + when 'Number' + result = Integer(response_body) unless response_body.to_s.empty? + when 'Double' + result = Float(response_body) unless response_body.to_s.empty? + when 'ByteArray' + result = Base64.strict_decode64(response_body).unpack('C*') unless response_body.to_s.empty? + when 'String', 'Boolean', 'Object', 'Stream' + result = response_body + when 'Enum' + unless response_body.nil? || response_body.empty? + unless enum_is_valid(mapper, response_body) + warn "Enum #{model} does not contain #{response_body.downcase}, but was received from the server." + end + end + result = response_body + when 'Date' + unless response_body.to_s.empty? + result = Timeliness.parse(response_body, :strict => true) + fail DeserializationError.new('Error occured in deserializing the response_body', nil, nil, response_body) if result.nil? + result = ::Date.parse(result.to_s) + end + when 'DateTime', 'DateTimeRfc1123' + result = DateTime.parse(response_body) unless response_body.to_s.empty? + when 'UnixTime' + result = DateTime.strptime(response_body.to_s, '%s') unless response_body.to_s.empty? + else + result + end + result + end + + # + # Deserialize the response of dictionary type from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # @param object_name [String] Name of the deserialized object. + # + def deserialize_dictionary_type(mapper, response_body, object_name) + if mapper[:type][:value].nil? || !mapper[:type][:value].is_a?(Hash) + fail DeserializationError.new("'value' metadata for a dictionary type must be defined in the mapper and it must be of type Hash in #{object_name}", nil, nil, response_body) + end + + result = Hash.new + response_body.each do |key, val| + result[key] = deserialize(mapper[:type][:value], val, object_name) + end + result + end + + # + # Deserialize the response of composite type from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # @param object_name [String] Name of the deserialized object. + # + def deserialize_composite_type(mapper, response_body, object_name) + if mapper[:type][:class_name].nil? + fail DeserializationError.new("'class_name' metadata for a composite type must be defined in the mapper and it must be of type Hash in #{object_name}", nil, nil, response_body) + end + + if !mapper[:type][:polymorphic_discriminator].nil? + # Handle polymorphic types + parent_class = get_model(mapper[:type][:class_name]) + discriminator = parent_class.class_eval("@@discriminatorMap") + model_name = response_body["#{mapper[:type][:polymorphic_discriminator]}"] + model_class = get_model(discriminator[model_name].capitalize) + else + model_class = get_model(mapper[:type][:class_name]) + end + + result = model_class.new + + model_mapper = model_class.mapper() + model_props = model_mapper[:type][:model_properties] + + unless model_props.nil? + model_props.each do |key, val| + result.instance_variable_set("@#{key}", deserialize(val, response_body[val[:serialized_name].to_s], object_name)) + end + end + result + end + + # + # Deserialize the response of sequence type from the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the response_body. + # @param response_body [Hash] Ruby Hash object to deserialize. + # @param object_name [String] Name of the deserialized object. + # + def deserialize_sequence_type(mapper, response_body, object_name) + if mapper[:type][:element].nil? || !mapper[:type][:element].is_a?(Hash) + fail DeserializationError.new("'element' metadata for a sequence type must be defined in the mapper and it must be of type Hash in #{object_name}", nil, nil, response_body) + end + + return response_body if response_body.nil? + + result = [] + response_body.each do |element| + result.push(deserialize(mapper[:type][:element], element, object_name)) + end + + result + end + + # + # Serialize the Ruby object into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # @param object_name [String] Name of the serialized object. + # + def serialize(mapper, object, object_name) + object_name = mapper[:serialized_name] unless object_name.nil? + + if mapper[:required] && object.nil? && !mapper[:is_constant] + fail ValidationError, "#{object_name} is required and cannot be nil" + end + + if !mapper[:required] && object.nil? + return object + end + + # Set defaults + unless mapper[:default_value].nil? + object = mapper[:default_value] if object.nil? + end + object = mapper[:default_value] if mapper[:is_constant] + + payload = Hash.new + mapper_type = mapper[:type][:name] + if !mapper_type.match(/^(Number|Double|ByteArray|Boolean|Date|DateTime|DateTimeRfc1123|UnixTime|Enum|String|Object|Stream)$/i).nil? + payload = serialize_primary_type(mapper, object) + elsif !mapper_type.match(/^Dictionary$/i).nil? + payload = serialize_dictionary_type(mapper, object, object_name) + elsif !mapper_type.match(/^Composite$/i).nil? + payload = serialize_composite_type(mapper, object, object_name) + elsif !mapper_type.match(/^Sequence$/i).nil? + payload = serialize_sequence_type(mapper, object, object_name) + end + payload + end + + # + # Serialize the Ruby object of known primary type into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # + def serialize_primary_type(mapper, object) + mapper_type = mapper[:type][:name] + payload = nil + case mapper_type + when 'Number', 'Double', 'String', 'Date', 'Boolean', 'Object', 'Stream' + payload = object != nil ? object : nil + when 'Enum' + unless object.nil? || object.empty? + unless enum_is_valid(mapper, object) + fail ValidationError, "Enum #{mapper[:type][:module]} does not contain #{object.to_s}, but trying to send it to the server." + end + end + payload = object != nil ? object : nil + when 'ByteArray' + payload = Base64.strict_encode64(object.pack('c*')) + when 'DateTime' + payload = object.new_offset(0).strftime('%FT%TZ') + when 'DateTimeRfc1123' + payload = object.new_offset(0).strftime('%a, %d %b %Y %H:%M:%S GMT') + when 'UnixTime' + payload = object.new_offset(0).strftime('%s') unless object.nil? + end + payload + end + + # + # Serialize the Ruby object of dictionary type into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # @param object_name [String] Name of the serialized object. + # + def serialize_dictionary_type(mapper, object, object_name) + unless object.is_a?(Hash) + fail DeserializationError.new("#{object_name} must be of type Hash", nil, nil, object) + end + + unless mapper[:type][:value].nil? || mapper[:type][:value].is_a?(Hash) + fail DeserializationError.new("'value' metadata for a dictionary type must be defined in the mapper and it must be of type Hash in #{object_name}", nil, nil, object) + end + + payload = Hash.new + object.each do |key, value| + if !value.nil? && value.respond_to?(:validate) + value.validate + end + + payload[key] = serialize(mapper[:type][:value], value, object_name) + end + payload + end + + # + # Serialize the Ruby object of composite type into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # @param object_name [String] Name of the serialized object. + # + def serialize_composite_type(mapper, object, object_name) + if !mapper[:type][:polymorphic_discriminator].nil? + # Handle polymorphic types + model_name = object.class.to_s.split('::')[-1] + model_class = get_model(model_name) + else + model_class = get_model(mapper[:type][:class_name]) + end + + payload = Hash.new + model_mapper = model_class.mapper() + model_props = model_mapper[:type][:model_properties] + + unless model_props.nil? + model_props.each do |key, value| + instance_variable = object.instance_variable_get("@#{key}") + if !instance_variable.nil? && instance_variable.respond_to?(:validate) + instance_variable.validate + end + + sub_payload = serialize(value, instance_variable, object_name) + payload[value[:serialized_name].to_s] = sub_payload unless instance_variable.nil? + end + end + payload + end + + # + # Serialize the Ruby object of sequence type into Ruby Hash to send it to the server using the mapper. + # + # @param mapper [Hash] Ruby Hash object to represent expected structure of the object. + # @param object [Object] Ruby object to serialize. + # @param object_name [String] Name of the serialized object. + # + def serialize_sequence_type(mapper, object, object_name) + unless object.is_a?(Array) + fail DeserializationError.new("#{object_name} must be of type of Array", nil, nil, object) + end + + unless mapper[:type][:element].nil? || mapper[:type][:element].is_a?(Hash) + fail DeserializationError.new("'element' metadata for a sequence type must be defined in the mapper and it must be of type Hash in #{object_name}", nil, nil, object) + end + + payload = Array.new + object.each do |element| + if !element.nil? && element.respond_to?(:validate) + element.validate + end + payload.push(serialize(mapper[:type][:element], element, object_name)) + end + payload + end + + # + # Retrieves model of the model_name + # + # @param model_name [String] Name of the model to retrieve. + # + def get_model(model_name) + Object.const_get(@context.class.to_s.split('::')[0...-1].join('::') + "::Models::#{model_name}") + end + + # + # Checks whether given enum_value is valid for the mapper or not + # + # @param mapper [Hash] Ruby Hash object containing meta data + # @param enum_value [String] Enum value to validate + # + def enum_is_valid(mapper, enum_value) + model = get_model(mapper[:type][:module]) + model.constants.any? { |e| model.const_get(e).to_s.downcase == enum_value.downcase } + end end end end diff --git a/ClientRuntimes/Ruby/ms-rest/spec/serialization_spec.rb b/ClientRuntimes/Ruby/ms-rest/spec/serialization_spec.rb deleted file mode 100644 index f78daa60367d7..0000000000000 --- a/ClientRuntimes/Ruby/ms-rest/spec/serialization_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# encoding: utf-8 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. - -require 'rspec' -require 'ms_rest' - -module MsRest - - describe Serialization do - it 'should parse correct date' do - parsed_date = Serialization.deserialize_date('1/1/2000') - expect(parsed_date).to eq(Date.new(2000, 1, 1)) - end - - it 'should throw error if incorrect date is provided' do - expect { Serialization.deserialize_date('13/1/2000') }.to raise_error(DeserializationError) - end - - it 'should throw error if incorrect date format is provided' do - expect { Serialization.deserialize_date('invalid_date') }.to raise_error(DeserializationError) - end - end - -end \ No newline at end of file