Skip to content

Commit a1e87ca

Browse files
Support exclusive(Minimum|Maximum)
- Add support for emitting `exclusive(Minimum|Maximum)` when `RangeAttribute.(Minimum|Maximum)IsExclusive` is set. - Add tests for `[Range]` attribute handling.
1 parent ec9e760 commit a1e87ca

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
102102

103103
if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var minDecimal))
104104
{
105-
schema[OpenApiSchemaKeywords.MinimumKeyword] = minDecimal;
105+
schema[rangeAttribute.MinimumIsExclusive ? OpenApiSchemaKeywords.ExclusiveMinimum : OpenApiSchemaKeywords.MinimumKeyword] = minDecimal;
106106
}
107107
if (decimal.TryParse(maxString, NumberStyles.Any, targetCulture, out var maxDecimal))
108108
{
109-
schema[OpenApiSchemaKeywords.MaximumKeyword] = maxDecimal;
109+
schema[rangeAttribute.MaximumIsExclusive ? OpenApiSchemaKeywords.ExclusiveMaximum : OpenApiSchemaKeywords.MaximumKeyword] = maxDecimal;
110110
}
111111
}
112112
else if (attribute is RegularExpressionAttribute regularExpressionAttribute)

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,21 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
262262
var minimum = reader.GetDecimal();
263263
schema.Minimum = minimum.ToString(CultureInfo.InvariantCulture);
264264
break;
265+
case OpenApiSchemaKeywords.ExclusiveMinimum:
266+
reader.Read();
267+
var exclusiveMinimum = reader.GetDecimal();
268+
schema.ExclusiveMinimum = exclusiveMinimum.ToString(CultureInfo.InvariantCulture);
269+
break;
265270
case OpenApiSchemaKeywords.MaximumKeyword:
266271
reader.Read();
267272
var maximum = reader.GetDecimal();
268273
schema.Maximum = maximum.ToString(CultureInfo.InvariantCulture);
269274
break;
275+
case OpenApiSchemaKeywords.ExclusiveMaximum:
276+
reader.Read();
277+
var exclusiveMaximum = reader.GetDecimal();
278+
schema.ExclusiveMaximum = exclusiveMaximum.ToString(CultureInfo.InvariantCulture);
279+
break;
270280
case OpenApiSchemaKeywords.PatternKeyword:
271281
reader.Read();
272282
var pattern = reader.GetString();

src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ internal class OpenApiSchemaKeywords
1919
public const string MaxLengthKeyword = "maxLength";
2020
public const string PatternKeyword = "pattern";
2121
public const string MinimumKeyword = "minimum";
22+
public const string ExclusiveMinimum = "exclusiveMinimum";
2223
public const string MaximumKeyword = "maximum";
24+
public const string ExclusiveMaximum = "exclusiveMaximum";
2325
public const string MinItemsKeyword = "minItems";
2426
public const string MaxItemsKeyword = "maxItems";
2527
public const string RefKeyword = "$ref";
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel.DataAnnotations;
5+
using System.Globalization;
6+
using System.Text.Json.Nodes;
7+
8+
namespace Microsoft.AspNetCore.OpenApi.Tests;
9+
10+
public static class JsonNodeSchemaExtensionsTests
11+
{
12+
public static TheoryData<string, bool, RangeAttribute, string, string> TestCases()
13+
{
14+
bool[] isExclusive = [false, true];
15+
16+
string[] invariantOrEnglishCultures =
17+
[
18+
string.Empty,
19+
"en",
20+
"en-AU",
21+
"en-GB",
22+
"en-US",
23+
];
24+
25+
string[] commaForDecimalCultures =
26+
[
27+
"de-DE",
28+
"fr-FR",
29+
"sv-SE",
30+
];
31+
32+
Type[] fractionNumberTypes =
33+
[
34+
typeof(float),
35+
typeof(double),
36+
typeof(decimal),
37+
];
38+
39+
var testCases = new TheoryData<string, bool, RangeAttribute, string, string>();
40+
41+
foreach (var culture in invariantOrEnglishCultures)
42+
{
43+
foreach (var exclusive in isExclusive)
44+
{
45+
testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
46+
testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
47+
testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
48+
49+
foreach (var type in fractionNumberTypes)
50+
{
51+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
52+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56");
53+
}
54+
}
55+
}
56+
57+
foreach (var culture in commaForDecimalCultures)
58+
{
59+
foreach (var exclusive in isExclusive)
60+
{
61+
testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
62+
testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
63+
testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
64+
65+
foreach (var type in fractionNumberTypes)
66+
{
67+
testCases.Add(culture, exclusive, new(type, "1,23", "4,56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
68+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56");
69+
}
70+
}
71+
}
72+
73+
// Numbers using numeric format, such as with thousands separators
74+
testCases.Add("en-GB", false, new(typeof(float), "-12,445.7", "12,445.7"), "-12445.7", "12445.7");
75+
testCases.Add("fr-FR", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7");
76+
testCases.Add("sv-SE", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7");
77+
78+
// Decimal value that would lose precision if parsed as a float or double
79+
foreach (var exclusive in isExclusive)
80+
{
81+
testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "12345678901234567890.123456789", "12345678901234567890.123456789");
82+
testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "12345678901234567890.123456789", "12345678901234567890.123456789");
83+
}
84+
85+
return testCases;
86+
}
87+
88+
[Theory]
89+
[MemberData(nameof(TestCases))]
90+
public static void ApplyValidationAttributes_Handles_RangeAttribute_Correctly(
91+
string cultureName,
92+
bool isExclusive,
93+
RangeAttribute rangeAttribute,
94+
string expectedMinimum,
95+
string expectedMaximum)
96+
{
97+
// Arrange
98+
var minimum = decimal.Parse(expectedMinimum, CultureInfo.InvariantCulture);
99+
var maximum = decimal.Parse(expectedMaximum, CultureInfo.InvariantCulture);
100+
101+
var schema = new JsonObject();
102+
103+
// Act
104+
var previous = CultureInfo.CurrentCulture;
105+
106+
try
107+
{
108+
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName);
109+
110+
schema.ApplyValidationAttributes([rangeAttribute]);
111+
}
112+
finally
113+
{
114+
CultureInfo.CurrentCulture = previous;
115+
}
116+
117+
// Assert
118+
if (isExclusive)
119+
{
120+
Assert.Equal(minimum, schema["exclusiveMinimum"].GetValue<decimal>());
121+
Assert.Equal(maximum, schema["exclusiveMaximum"].GetValue<decimal>());
122+
Assert.False(schema.TryGetPropertyValue("minimum", out _));
123+
Assert.False(schema.TryGetPropertyValue("maximum", out _));
124+
}
125+
else
126+
{
127+
Assert.Equal(minimum, schema["minimum"].GetValue<decimal>());
128+
Assert.Equal(maximum, schema["maximum"].GetValue<decimal>());
129+
Assert.False(schema.TryGetPropertyValue("exclusiveMinimum", out _));
130+
Assert.False(schema.TryGetPropertyValue("exclusiveMaximum", out _));
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)