diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs index 50ec431cc..5a6672eab 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs @@ -6,95 +6,19 @@ using System.Linq; using System.Text; using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Readers.ParseNodes { internal static class OpenApiAnyConverter { - /// - /// Converts the s in the given - /// into the most specific type based on the value. - /// - public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny) - { - if (openApiAny is OpenApiArray openApiArray) - { - var newArray = new OpenApiArray(); - foreach (var element in openApiArray) - { - newArray.Add(GetSpecificOpenApiAny(element)); - } - - return newArray; - } - - if (openApiAny is OpenApiObject openApiObject) - { - var newObject = new OpenApiObject(); - - foreach (var key in openApiObject.Keys.ToList()) - { - newObject[key] = GetSpecificOpenApiAny(openApiObject[key]); - } - - return newObject; - } - - if ( !(openApiAny is OpenApiString)) - { - return openApiAny; - } - - var value = ((OpenApiString)openApiAny).Value; - - if (value == null || value == "null") - { - return new OpenApiNull(); - } - - if (value == "true") - { - return new OpenApiBoolean(true); - } - - if (value == "false") - { - return new OpenApiBoolean(false); - } - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return new OpenApiInteger(intValue); - } - - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return new OpenApiLong(longValue); - } - - if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) - { - return new OpenApiDouble(doubleValue); - } - - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) - { - return new OpenApiDateTime(dateTimeValue); - } - - // if we can't identify the type of value, return it as string. - return new OpenApiString(value); - } - /// /// Converts the s in the given /// into the appropriate type based on the given . /// For those strings that the schema does not specify the type for, convert them into /// the most specific type based on the value. /// - public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny, OpenApiSchema schema) + public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny, OpenApiSchema schema = null) { if (openApiAny is OpenApiArray openApiArray) { @@ -113,9 +37,9 @@ public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny, OpenApiS foreach (var key in openApiObject.Keys.ToList()) { - if ( schema != null && schema.Properties != null && schema.Properties.ContainsKey(key) ) + if (schema?.Properties != null && schema.Properties.TryGetValue(key, out var property)) { - newObject[key] = GetSpecificOpenApiAny(openApiObject[key], schema.Properties[key]); + newObject[key] = GetSpecificOpenApiAny(openApiObject[key], property); } else { @@ -126,133 +50,162 @@ public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny, OpenApiS return newObject; } - if (!(openApiAny is OpenApiString)) + if (!(openApiAny is OpenApiString) || ((OpenApiString)openApiAny).IsExplicit()) { return openApiAny; } - if (schema?.Type == null) - { - return GetSpecificOpenApiAny(openApiAny); - } - - var type = schema.Type; - var format = schema.Format; - var value = ((OpenApiString)openApiAny).Value; - if (value == null || value == "null") { return new OpenApiNull(); } - if (type == "integer" && format == "int32") + if (schema?.Type == null) { - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (value == "true") { - return new OpenApiInteger(intValue); + return new OpenApiBoolean(true); } - } - if (type == "integer" && format == "int64") - { - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) + if (value == "false") { - return new OpenApiLong(longValue); + return new OpenApiBoolean(false); } - } - if (type == "integer") - { if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { return new OpenApiInteger(intValue); } - } - if (type == "number" && format == "float") - { - if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatValue)) + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) { - return new OpenApiFloat(floatValue); + return new OpenApiLong(longValue); } - } - if (type == "number" && format == "double" ) - { if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) { return new OpenApiDouble(doubleValue); } - } - if (type == "number") - { - if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) { - return new OpenApiDouble(doubleValue); + return new OpenApiDateTime(dateTimeValue); } } - - if (type == "string" && format == "byte") + else { - try + var type = schema.Type; + var format = schema.Format; + + if (type == "integer" && format == "int32") { - return new OpenApiByte(Convert.FromBase64String(value)); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return new OpenApiInteger(intValue); + } } - catch(FormatException) - { } - } - // binary - if (type == "string" && format == "binary") - { - try + if (type == "integer" && format == "int64") { - return new OpenApiBinary(Encoding.UTF8.GetBytes(value)); + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) + { + return new OpenApiLong(longValue); + } } - catch(EncoderFallbackException) - { } - } - if (type == "string" && format == "date") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) + if (type == "integer") { - return new OpenApiDate(dateValue.Date); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return new OpenApiInteger(intValue); + } } - } - if (type == "string" && format == "date-time") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + if (type == "number" && format == "float") { - return new OpenApiDateTime(dateTimeValue); + if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatValue)) + { + return new OpenApiFloat(floatValue); + } } - } - if (type == "string" && format == "password") - { - return new OpenApiPassword(value); - } + if (type == "number" && format == "double") + { + if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + { + return new OpenApiDouble(doubleValue); + } + } - if (type == "string") - { - return new OpenApiString(value); - } + if (type == "number") + { + if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + { + return new OpenApiDouble(doubleValue); + } + } - if (type == "boolean") - { - if (bool.TryParse(value, out var booleanValue)) + if (type == "string" && format == "byte") + { + try + { + return new OpenApiByte(Convert.FromBase64String(value)); + } + catch (FormatException) + { } + } + + // binary + if (type == "string" && format == "binary") { - return new OpenApiBoolean(booleanValue); + try + { + return new OpenApiBinary(Encoding.UTF8.GetBytes(value)); + } + catch (EncoderFallbackException) + { } + } + + if (type == "string" && format == "date") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) + { + return new OpenApiDate(dateValue.Date); + } + } + + if (type == "string" && format == "date-time") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + { + return new OpenApiDateTime(dateTimeValue); + } + } + + if (type == "string" && format == "password") + { + return new OpenApiPassword(value); + } + + if (type == "string") + { + return new OpenApiString(value); + } + + if (type == "boolean") + { + if (bool.TryParse(value, out var booleanValue)) + { + return new OpenApiBoolean(booleanValue); + } } } // If data conflicts with the given type, return a string. // This converter is used in the parser, so it does not perform any validations, // but the validator can be used to validate whether the data and given type conflicts. - return new OpenApiString(value); + return openApiAny; } } } diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs index b7669b538..324510f87 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; -using System.Globalization; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Readers.Exceptions; +using SharpYaml; using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers.ParseNodes @@ -36,7 +35,7 @@ public override string GetScalarValue() public override IOpenApiAny CreateAny() { var value = GetScalarValue(); - return new OpenApiString(value); + return new OpenApiString(value, this._node.Style == ScalarStyle.SingleQuoted || this._node.Style == ScalarStyle.DoubleQuoted || this._node.Style == ScalarStyle.Literal || this._node.Style == ScalarStyle.Folded); } } } diff --git a/src/Microsoft.OpenApi/Any/OpenApiString.cs b/src/Microsoft.OpenApi/Any/OpenApiString.cs index ebd31449e..2ead9b6c5 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiString.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiString.cs @@ -8,18 +8,29 @@ namespace Microsoft.OpenApi.Any /// public class OpenApiString : OpenApiPrimitive { + private bool isExplicit; + /// /// Initializes the class. /// /// - public OpenApiString(string value) + public OpenApiString(string value, bool isExplicit = false) : base(value) { + this.isExplicit = isExplicit; } /// /// The primitive class this object represents. /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.String; + + /// + /// True if string was specified explicitly by the means of double quotes, single quotes, or literal or folded style. + /// + public bool IsExplicit() + { + return this.isExplicit; + } } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index e24afc871..ba3df3cc7 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -12,6 +12,9 @@ true ..\..\src\Microsoft.OpenApi.snk + + + Never @@ -143,6 +146,7 @@ Never + Never diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs index 20e3f33eb..e38893884 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs @@ -75,5 +75,15 @@ public void ParseAdvancedExampleShouldSucceed() }); } } + + [Fact] + public void ParseExampleForcedStringSucceed() + { + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "explicitString.yaml"))) + { + new OpenApiStreamReader().Read(stream, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs index c9e53f8c9..089ae57a2 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs @@ -71,8 +71,8 @@ public void ParseAdvancedInfoShouldSucceed() }, ["x-list"] = new OpenApiArray { - new OpenApiInteger(1), - new OpenApiInteger(2) + new OpenApiString("1"), + new OpenApiString("2") } } }); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml new file mode 100644 index 000000000..bb2baa79e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Test API +paths: + /test-path: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/test-schema' + responses: + default: + description: '' +components: + schemas: + test-schema: + type: object + properties: + sub: + $ref: '#/components/schemas/test-sub-schema' + required: + - origin + example: + sub: + test-property: "12345" + test-sub-schema: + type: object + properties: + test-property: + type: string + example: "12345" \ No newline at end of file