From 1a8cdc97c60883e1c127ec3b5f60f8fe56231501 Mon Sep 17 00:00:00 2001 From: Perth Charernwattanagul Date: Tue, 2 Oct 2018 11:44:37 -0700 Subject: [PATCH] Parse decimals and ints with CultureInfo.InvariantCulture since JSON/YAML doesn't vary by culture. For example, the parser should always treat 100.000 as one hundred, not one hundred thousand, even if the operating system on the machine is set to use French. --- .../V2/OpenApiDocumentDeserializer.cs | 3 +- .../V2/OpenApiHeaderDeserializer.cs | 17 +++-- .../V2/OpenApiParameterDeserializer.cs | 9 ++- .../V2/OpenApiSchemaDeserializer.cs | 19 ++--- .../V3/OpenApiSchemaDeserializer.cs | 19 ++--- .../V2Tests/OpenApiDocumentTests.cs | 73 ++++++++++++++++++ .../V3Tests/OpenApiAnyTests.cs | 7 +- .../V3Tests/OpenApiDocumentTests.cs | 74 +++++++++++++++++++ .../OpenApiWriterAnyExtensionsTests.cs | 3 +- 9 files changed, 189 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs index 525a3f52f..f8cacc352 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; @@ -191,7 +192,7 @@ private static string BuildUrl(string scheme, string host, string basePath) { var pieces = host.Split(':'); host = pieces.First(); - port = int.Parse(pieces.Last()); + port = int.Parse(pieces.Last(), CultureInfo.InvariantCulture); } var uriBuilder = new UriBuilder() diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs index f23746a0a..b7f07c4cd 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Globalization; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; @@ -54,10 +55,10 @@ internal static partial class OpenApiV2Deserializer GetOrCreateSchema(o).Default = n.CreateAny(); } }, - { + { "maximum", (o, n) => { - GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -69,7 +70,7 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -81,13 +82,13 @@ internal static partial class OpenApiV2Deserializer { "maxLength", (o, n) => { - GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -99,13 +100,13 @@ internal static partial class OpenApiV2Deserializer { "maxItems", (o, n) => { - GetOrCreateSchema(o).MaxItems = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - GetOrCreateSchema(o).MinItems = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -117,7 +118,7 @@ internal static partial class OpenApiV2Deserializer { "multipleOf", (o, n) => { - GetOrCreateSchema(o).MultipleOf = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs index 12b033f73..baf8499cb 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; @@ -90,25 +91,25 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maxLength", (o, n) => { - GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs index 4d08dd29d..dd76a38cc 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs @@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; using System.Collections.Generic; +using System.Globalization; namespace Microsoft.OpenApi.Readers.V2 { @@ -26,13 +27,13 @@ internal static partial class OpenApiV2Deserializer { "multipleOf", (o, n) => { - o.MultipleOf = decimal.Parse(n.GetScalarValue()); + o.MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - o.Maximum = decimal.Parse(n.GetScalarValue()); + o.Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -44,7 +45,7 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - o.Minimum = decimal.Parse(n.GetScalarValue()); + o.Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -56,13 +57,13 @@ internal static partial class OpenApiV2Deserializer { "maxLength", (o, n) => { - o.MaxLength = int.Parse(n.GetScalarValue()); + o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - o.MinLength = int.Parse(n.GetScalarValue()); + o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -74,13 +75,13 @@ internal static partial class OpenApiV2Deserializer { "maxItems", (o, n) => { - o.MaxItems = int.Parse(n.GetScalarValue()); + o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - o.MinItems = int.Parse(n.GetScalarValue()); + o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -92,13 +93,13 @@ internal static partial class OpenApiV2Deserializer { "maxProperties", (o, n) => { - o.MaxProperties = int.Parse(n.GetScalarValue()); + o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minProperties", (o, n) => { - o.MinProperties = int.Parse(n.GetScalarValue()); + o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs index a86ebcb50..94d21f723 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs @@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; using System.Collections.Generic; +using System.Globalization; namespace Microsoft.OpenApi.Readers.V3 { @@ -26,13 +27,13 @@ internal static partial class OpenApiV3Deserializer { "multipleOf", (o, n) => { - o.MultipleOf = decimal.Parse(n.GetScalarValue()); + o.MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - o.Maximum = decimal.Parse(n.GetScalarValue()); + o.Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -44,7 +45,7 @@ internal static partial class OpenApiV3Deserializer { "minimum", (o, n) => { - o.Minimum = decimal.Parse(n.GetScalarValue()); + o.Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -56,13 +57,13 @@ internal static partial class OpenApiV3Deserializer { "maxLength", (o, n) => { - o.MaxLength = int.Parse(n.GetScalarValue()); + o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - o.MinLength = int.Parse(n.GetScalarValue()); + o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -74,13 +75,13 @@ internal static partial class OpenApiV3Deserializer { "maxItems", (o, n) => { - o.MaxItems = int.Parse(n.GetScalarValue()); + o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - o.MinItems = int.Parse(n.GetScalarValue()); + o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -92,13 +93,13 @@ internal static partial class OpenApiV3Deserializer { "maxProperties", (o, n) => { - o.MaxProperties = int.Parse(n.GetScalarValue()); + o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minProperties", (o, n) => { - o.MinProperties = int.Parse(n.GetScalarValue()); + o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs index 6850628bd..5eaef365e 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Globalization; +using System.Threading; using FluentAssertions; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Models; @@ -66,5 +68,76 @@ public void ShouldThrowWhenReferenceDoesNotExist() new OpenApiError( new OpenApiException("Invalid Reference identifier 'doesnotexist'.")) }); doc.Should().NotBeNull(); } + + [Theory] + [InlineData("en-US")] + [InlineData("hi-IN")] + // The equivalent of English 1,000.36 in French and Danish is 1.000,36 + [InlineData("fr-FR")] + [InlineData("da-DK")] + public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + + var openApiDoc = new OpenApiStringReader().Read( + @" +swagger: 2.0 +info: + title: Simple Document + version: 0.9.1 +definitions: + sampleSchema: + type: object + properties: + sampleProperty: + type: double + minimum: 100.54 + maximum: 60,000,000.35 + exclusiveMaximum: true + exclusiveMinimum: false +paths: {}", + out var context); + + openApiDoc.ShouldBeEquivalentTo( + new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Simple Document", + Version = "0.9.1" + }, + Components = new OpenApiComponents() + { + Schemas = + { + ["sampleSchema"] = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["sampleProperty"] = new OpenApiSchema() + { + Type = "double", + Minimum = (decimal)100.54, + Maximum = (decimal)60000000.35, + ExclusiveMaximum = true, + ExclusiveMinimum = false + } + }, + Reference = new OpenApiReference() + { + Id = "sampleSchema", + Type = ReferenceType.Schema + } + } + } + }, + Paths = new OpenApiPaths() + }); + + context.ShouldBeEquivalentTo( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi2_0 }); + } } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs index 86706f9dc..1891d86ba 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Globalization; using System.IO; using System.Linq; using FluentAssertions; @@ -43,7 +44,7 @@ public void ParseMapAsAnyShouldSucceed() ["aString"] = new OpenApiString("fooBar"), ["aInteger"] = new OpenApiInteger(10), ["aDouble"] = new OpenApiDouble(2.34), - ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01")) + ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)) }); } @@ -75,7 +76,7 @@ public void ParseListAsAnyShouldSucceed() new OpenApiString("fooBar"), new OpenApiInteger(10), new OpenApiDouble(2.34), - new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01")) + new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)) }); } @@ -123,7 +124,7 @@ public void ParseScalarDateTimeAsAnyShouldSucceed() diagnostic.Errors.Should().BeEmpty(); any.ShouldBeEquivalentTo( - new OpenApiDateTime(DateTimeOffset.Parse("2012-07-23T12:33:00")) + new OpenApiDateTime(DateTimeOffset.Parse("2012-07-23T12:33:00", CultureInfo.InvariantCulture)) ); } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 8117e1c57..4a1f992b4 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using FluentAssertions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Validations; @@ -54,6 +56,78 @@ public void ParseDocumentFromInlineStringShouldSucceed() new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } + [Theory] + [InlineData("en-US")] + [InlineData("hi-IN")] + // The equivalent of English 1,000.36 in French and Danish is 1.000,36 + [InlineData("fr-FR")] + [InlineData("da-DK")] + public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + + var openApiDoc = new OpenApiStringReader().Read( + @" +openapi : 3.0.0 +info: + title: Simple Document + version: 0.9.1 +components: + schemas: + sampleSchema: + type: object + properties: + sampleProperty: + type: double + minimum: 100.54 + maximum: 60,000,000.35 + exclusiveMaximum: true + exclusiveMinimum: false +paths: {}", + out var context); + + openApiDoc.ShouldBeEquivalentTo( + new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Simple Document", + Version = "0.9.1" + }, + Components = new OpenApiComponents() + { + Schemas = + { + ["sampleSchema"] = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["sampleProperty"] = new OpenApiSchema() + { + Type = "double", + Minimum = (decimal)100.54, + Maximum = (decimal)60000000.35, + ExclusiveMaximum = true, + ExclusiveMinimum = false + } + }, + Reference = new OpenApiReference() + { + Id = "sampleSchema", + Type = ReferenceType.Schema + } + } + } + }, + Paths = new OpenApiPaths() + }); + + context.ShouldBeEquivalentTo( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); + } + [Fact] public void ParseBasicDocumentWithMultipleServersShouldSucceed() { diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs index cefc1e486..eb51c9f3e 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -93,7 +94,7 @@ public void WriteOpenApiDoubleAsJsonWorks(double input) public void WriteOpenApiDateTimeAsJsonWorks(string inputString) { // Arrange - var input = DateTimeOffset.Parse(inputString); + var input = DateTimeOffset.Parse(inputString, CultureInfo.InvariantCulture); var dateTimeValue = new OpenApiDateTime(input); var json = WriteAsJson(dateTimeValue);