Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nullable support using a refiner #128

Merged
merged 8 commits into from Jun 18, 2021
Merged
75 changes: 75 additions & 0 deletions JsonSchema.Generation.Tests/GeneratorTests.cs
Expand Up @@ -149,5 +149,80 @@ public void UriSchema()

AssertEqual(expected, actual);
}

[Test]
public void NullableBooleanSchema()
{
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.Boolean)
.Build();

var actual = new JsonSchemaBuilder().FromType<bool?>().Build();

AssertEqual(expected, actual);
}

[Test]
public void NullableDateTimeSchema()
{
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Format(Formats.DateTime)
.Build();

var actual = new JsonSchemaBuilder().FromType<DateTime?>().Build();

AssertEqual(expected, actual);
}

[Test]
public void NullableEnumSchema()
{
var expected = new JsonSchemaBuilder()
.Enum(Enum.GetNames(typeof(DayOfWeek)).Select(x => x.AsJsonElement()))
.Build();

var actual = new JsonSchemaBuilder().FromType<DayOfWeek?>().Build();

AssertEqual(expected, actual);
}

[Test]
public void NullableGuidSchema()
{
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Format(Formats.Uuid)
.Build();

var actual = new JsonSchemaBuilder().FromType<Guid?>().Build();

AssertEqual(expected, actual);
}

[Test]
public void NullableIntegerSchema()
{
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.Integer)
.Build();

var actual = new JsonSchemaBuilder().FromType<int?>().Build();

AssertEqual(expected, actual);
}


[Test]
public void NullableNumberSchema()
{
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.Number)
.Build();

var actual = new JsonSchemaBuilder().FromType<double?>().Build();

AssertEqual(expected, actual);
}
}
}
187 changes: 187 additions & 0 deletions JsonSchema.Generation.Tests/NullabilityTests.cs
@@ -0,0 +1,187 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Json.Schema.Generation.Tests
{
public class NullabilityTests
{
public class ReferenceMember
{
public string Property { get; set; }
}

public class ReferenceMemberWithNull
{
[Nullable(true)]
public string Property { get; set; }
}

public class ReferenceMemberWithNotNull
{
[Nullable(false)]
public string Property { get; set; }
}

public class NullableValueTypeMember
{
public int? Property { get; set; }
}

public class NullableValueTypeMemberWithNull
{
[Nullable(true)]
public int? Property { get; set; }
}

public class NullableValueTypeMemberWithNotNull
{
[Nullable(false)]
public int? Property { get; set; }
}

public class ValueTypeMember
{
public int Property { get; set; }
}

public class ValueTypeMemberWithNull
{
[Nullable(true)]
public int Property { get; set; }
}

public class ValueTypeMemberWithNotNull
{
[Nullable(false)]
public int Property { get; set; }
}

private static readonly Nullability Disabled = Nullability.Disabled;
private static readonly Nullability AllowForReferenceTypes = Nullability.AllowForReferenceTypes;
private static readonly Nullability AllowForNullableValueTypes = Nullability.AllowForNullableValueTypes;
private static readonly Nullability AllowForAllTypes = Nullability.AllowForAllTypes;

private static readonly SchemaValueType String = SchemaValueType.String;
private static readonly SchemaValueType Integer = SchemaValueType.Integer;
private static readonly SchemaValueType Null = SchemaValueType.Null;


public static IEnumerable<TestCaseData> MemberCases
{
get
{
yield return new TestCaseData(Disabled, typeof(ReferenceMember), String);
yield return new TestCaseData(Disabled, typeof(ReferenceMemberWithNull), String | Null);
yield return new TestCaseData(Disabled, typeof(ReferenceMemberWithNotNull), String);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ReferenceMember), String);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ReferenceMemberWithNull), String | Null);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ReferenceMemberWithNotNull), String);
yield return new TestCaseData(AllowForAllTypes, typeof(ReferenceMember), String | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(ReferenceMemberWithNull), String | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(ReferenceMemberWithNotNull), String);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ReferenceMember), String | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ReferenceMemberWithNull), String | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ReferenceMemberWithNotNull), String);

yield return new TestCaseData(Disabled, typeof(NullableValueTypeMember), Integer);
yield return new TestCaseData(Disabled, typeof(NullableValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(Disabled, typeof(NullableValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(NullableValueTypeMember), Integer | Null);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(NullableValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(NullableValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForAllTypes, typeof(NullableValueTypeMember), Integer | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(NullableValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(NullableValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForReferenceTypes, typeof(NullableValueTypeMember), Integer);
yield return new TestCaseData(AllowForReferenceTypes, typeof(NullableValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(NullableValueTypeMemberWithNotNull), Integer);

yield return new TestCaseData(Disabled, typeof(ValueTypeMember), Integer);
yield return new TestCaseData(Disabled, typeof(ValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(Disabled, typeof(ValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ValueTypeMember), Integer);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(ValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForAllTypes, typeof(ValueTypeMember), Integer);
yield return new TestCaseData(AllowForAllTypes, typeof(ValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(ValueTypeMemberWithNotNull), Integer);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ValueTypeMember), Integer);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ValueTypeMemberWithNull), Integer | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(ValueTypeMemberWithNotNull), Integer);

}
}

[TestCaseSource(nameof(MemberCases))]
public void MemberNullability(Nullability nullability, Type type, SchemaValueType valueType)
{
var config = new SchemaGeneratorConfiguration
{
Nullability = nullability
};

// Nullability affects root schema so only PropertiesKeywords are compared
var expected = new JsonSchemaBuilder()
.Properties(
(nameof(ReferenceMember.Property), new JsonSchemaBuilder().Type(valueType)))
.Build()
.Keywords
.OfType<PropertiesKeyword>()
.First();

var actual = new JsonSchemaBuilder()
.FromType(type, config)
.Build()
.Keywords
.OfType<PropertiesKeyword>()
.First();

Assert.AreEqual(expected, actual);
}


public static IEnumerable<TestCaseData> TypeCases
{
get
{
yield return new TestCaseData(Disabled, typeof(string), String);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(string), String);
yield return new TestCaseData(AllowForAllTypes, typeof(string), String | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(string), String | Null);

yield return new TestCaseData(Disabled, typeof(int?), Integer);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(int?), Integer | Null);
yield return new TestCaseData(AllowForAllTypes, typeof(int?), Integer | Null);
yield return new TestCaseData(AllowForReferenceTypes, typeof(int?), Integer);

yield return new TestCaseData(Disabled, typeof(int), Integer);
yield return new TestCaseData(AllowForNullableValueTypes, typeof(int), Integer);
yield return new TestCaseData(AllowForAllTypes, typeof(int), Integer);
yield return new TestCaseData(AllowForReferenceTypes, typeof(int), Integer);

}
}

[TestCaseSource(nameof(TypeCases))]
public void TypeNullability(Nullability nullability, Type type, SchemaValueType valueType)
{
var config = new SchemaGeneratorConfiguration
{
Nullability = nullability
};

var expected = new JsonSchemaBuilder()
.Type(valueType)
.Build();

var actual = new JsonSchemaBuilder()
.FromType(type, config)
.Build();

Assert.AreEqual(expected, actual);
}

}
}
22 changes: 22 additions & 0 deletions JsonSchema.Generation.Tests/SchemaGenerationTests.cs
Expand Up @@ -25,6 +25,17 @@ public static IEnumerable<TestCaseData> SimpleTypeCases
yield return new TestCaseData(typeof(float), SchemaValueType.Number);
yield return new TestCaseData(typeof(double), SchemaValueType.Number);
yield return new TestCaseData(typeof(decimal), SchemaValueType.Number);
yield return new TestCaseData(typeof(bool?), SchemaValueType.Boolean);
yield return new TestCaseData(typeof(byte?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(short?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(ushort?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(int?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(uint?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(long?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(ulong?), SchemaValueType.Integer);
yield return new TestCaseData(typeof(float?), SchemaValueType.Number);
yield return new TestCaseData(typeof(double?), SchemaValueType.Number);
yield return new TestCaseData(typeof(decimal?), SchemaValueType.Number);
}
}

Expand All @@ -49,6 +60,17 @@ public void IntArray()
AssertEqual(expected, actual);
}

[Test]
public void NullableIntArray()
{
JsonSchema expected = new JsonSchemaBuilder().Type(SchemaValueType.Array)
.Items(new JsonSchemaBuilder().Type(SchemaValueType.Integer));

JsonSchema actual = new JsonSchemaBuilder().FromType<int?[]>();

AssertEqual(expected, actual);
}

[Test]
// ReSharper disable once InconsistentNaming
public void IEnumerableOfListOfStrings()
Expand Down
1 change: 1 addition & 0 deletions JsonSchema.Generation/GeneratorRegistry.cs
Expand Up @@ -13,6 +13,7 @@ public static class GeneratorRegistry
private static readonly List<ISchemaGenerator> Generators =
new List<ISchemaGenerator>
{
new NullableValueTypeSchemaGenerator(),
new BooleanSchemaGenerator(),
new IntegerSchemaGenerator(),
new NumberSchemaGenerator(),
Expand Down
@@ -0,0 +1,22 @@
using System;

namespace Json.Schema.Generation.Generators
{
internal class NullableValueTypeSchemaGenerator : ISchemaGenerator
{
public bool Handles(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}

public void AddConstraints(SchemaGeneratorContext context)
{
var underlyingType = Nullable.GetUnderlyingType(context.Type);

if (underlyingType == null) return;
var underlyingContext = SchemaGenerationContextCache.Get(underlyingType, context.Attributes, context.Configuration);

context.Intents.AddRange(underlyingContext.Intents);
}
}
}
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/DeprecatedIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class DeprecatedIntent : ISchemaKeywordIntent
/// <summary>
/// The value.
/// </summary>
public bool Value { get; }
public bool Value { get; set; }

/// <summary>
/// Creates a new <see cref="DeprecatedIntent"/> instance.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/EnumIntent.cs
Expand Up @@ -13,7 +13,7 @@ public class EnumIntent : ISchemaKeywordIntent
/// <summary>
/// The names defined by the enumeration.
/// </summary>
public List<string> Names { get; }
public List<string> Names { get; set; }

/// <summary>
/// Applies the keyword to the <see cref="JsonSchemaBuilder"/>.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/ExclusiveMaximumIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class ExclusiveMaximumIntent : ISchemaKeywordIntent
/// <summary>
/// The value.
/// </summary>
public decimal Value { get; }
public decimal Value { get; set; }

/// <summary>
/// Creates a new <see cref="ExclusiveMaximumIntent"/> instance.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/ExclusiveMinimumIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class ExclusiveMinimumIntent : ISchemaKeywordIntent
/// <summary>
/// The value.
/// </summary>
public decimal Value { get; }
public decimal Value { get; set; }

/// <summary>
/// Creates a new <see cref="ExclusiveMinimumIntent"/> instance.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/FormatIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class FormatIntent : ISchemaKeywordIntent
/// <summary>
/// The format.
/// </summary>
public Format Format { get; }
public Format Format { get; set; }

/// <summary>
/// Creates a new <see cref="FormatIntent"/> instance.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/MaxItemsIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class MaxItemsIntent : ISchemaKeywordIntent
/// <summary>
/// The value.
/// </summary>
public uint Value { get; }
public uint Value { get; set; }

/// <summary>
/// Creates a new <see cref="MaxItemsIntent"/> instance.
Expand Down
2 changes: 1 addition & 1 deletion JsonSchema.Generation/Intents/MaxLengthIntent.cs
Expand Up @@ -8,7 +8,7 @@ public class MaxLengthIntent : ISchemaKeywordIntent
/// <summary>
/// The value.
/// </summary>
public uint Value { get; }
public uint Value { get; set; }

/// <summary>
/// Creates a new <see cref="MaxLengthIntent"/> instance.
Expand Down