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

Add UniqueItems Attribute #212

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Doc/Samples/Generation/GenerateWithJSchemaAttributes.aml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="GenerateWithJsonNetAttributes" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<introduction>
<para>This sample generates a new <codeEntityReference>T:Newtonsoft.Json.Schema.JSchema</codeEntityReference>
from a .NET type with JSchema serialization attributes.</para>
</introduction>
<section>
<title>Sample</title>
<content>
<code lang="cs" source="..\Src\Newtonsoft.Json.Schema.Tests\Documentation\Samples\Generation\GenerateWithJSchemaAttributes.cs" region="Types" title="Types" />
<code lang="cs" source="..\Src\Newtonsoft.Json.Schema.Tests\Documentation\Samples\Generation\GenerateWithJSchemaAttributes.cs" region="Usage" title="Usage" />
</content>
</section>
</developerConceptualDocument>
</topic>
1 change: 1 addition & 0 deletions Doc/doc.content
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Topic id="GenerateWithDescriptions" visible="True" isSelected="true" title="Generate with Descriptions" />
<Topic id="GenerateWithProvider" visible="True" title="Using JSchemaGenerationProvider" />
<Topic id="GenerateWithJsonNetAttributes" visible="True" title="Using Json.NET attributes" />
<Topic id="GenerateWithJSchemaAttributes" visible="True" title="Using JSchema attributes" />
<Topic id="GenerateWithDataAnnotations" visible="True" title="Using Data Annotation attributes" />
<Topic id="GenerateWithSchemaIdGenerationHandling" visible="True" title="Auto schema ID generation" />
<Topic id="CreateCustomProvider" visible="True" title="Create a custom JSchemaGenerationProvider" />
Expand Down
1 change: 1 addition & 0 deletions Doc/doc.shfbproj
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
<None Include="Samples\Load\JsonUrlSchemaResolverRelative.aml" />
<None Include="Samples\Load\JsonUrlSchemaResolverHttp.aml" />
<None Include="Samples\Generation\GenerateWithJsonNetAttributes.aml" />
<None Include="Samples\Generation\GenerateWithJSchemaAttributes.aml" />
<None Include="Samples\Generation\GenerateWithProvider.aml" />
<None Include="Samples\Generation\GenerateWithSchemaIdGenerationHandling.aml" />
<None Include="Samples\Generation\GenerateWithDataAnnotations.aml" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#region License
// Copyright (c) Newtonsoft. All Rights Reserved.
// License: https://raw.github.com/JamesNK/Newtonsoft.Json.Schema/master/LICENSE.md
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Schema.Generation;
using Newtonsoft.Json.Serialization;
#if DNXCORE50
using Xunit;
using Test = Xunit.FactAttribute;
using Assert = Newtonsoft.Json.Schema.Tests.XUnitAssert;
#else
using NUnit.Framework;
#endif

namespace Newtonsoft.Json.Schema.Tests.Documentation.Samples.Generation
{
[TestFixture]
public class GenerateWithJSchemaAttributes : TestFixtureBase
{
#region Types
public class Computer
{
// always require a string value
[UniqueItems]
public IEnumerable<string> DiskIds { get; set; }

public HashSet<string> ScreenIds { get; set; }
}
#endregion

[Test]
public void Example()
{
#region Usage
JSchemaGenerator generator = new JSchemaGenerator();

JSchema schema = generator.Generate(typeof(Computer));
//{
// "type": "object",
// "properties": {
// "DiskIds": {
// "type": "array",
// "items": {
// "type": "string",
// },
// "uniqueItems": true
// },
// "ScreenIds": {
// "type": "array",
// "items": {
// "type": "string",
// },
// "uniqueItems": true
// }
// }
//}
#endregion

Assert.IsTrue(schema.Properties["DiskIds"].UniqueItems);
#if !NET35
Assert.IsTrue(schema.Properties["ScreenIds"].UniqueItems);
#endif
}
}
}
64 changes: 64 additions & 0 deletions Src/Newtonsoft.Json.Schema.Tests/JSchemaGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,70 @@ public void GenerationProviderAttributeDerivedPlusConverterAttributeDerived()
Assert.IsNotNull(schema.Properties["Provider"]);
}

internal class UniqueItemsClassWithAttribute
{
[UniqueItems]
public IEnumerable<string> UniqueItemsProperty { get; set; }
}

internal class UniqueItemsClassWithHashSet
{
public HashSet<string> UniqueItemsProperty { get; set; }
}

internal class UniqueItemsClassWithoutAttribute
{
public IEnumerable<string> UniqueItemsProperty { get; set; }
}

internal class UniqueItemsClassWithAttributeButWithoutCollection
{
[UniqueItems]
public string UniqueItemsProperty { get; set; }
}

[Test]
public void GenerateUniqueItemsWithAttribute()
{
JSchemaGenerator generator = new JSchemaGenerator();
JSchema schema = generator.Generate(typeof(UniqueItemsClassWithAttribute));

Assert.IsNotNull(schema.Properties["UniqueItemsProperty"]);
Assert.IsTrue(schema.Properties["UniqueItemsProperty"].UniqueItems);
}

#if !NET35
[Test]
public void GenerateUniqueItemsWithHashSet()
{
JSchemaGenerator generator = new JSchemaGenerator();
JSchema schema = generator.Generate(typeof(UniqueItemsClassWithHashSet));

Assert.IsNotNull(schema.Properties["UniqueItemsProperty"]);
Assert.IsTrue(schema.Properties["UniqueItemsProperty"].UniqueItems);
}
#endif

[Test]
public void GenerateUniqueItemsWithoutAttribute()
{
JSchemaGenerator generator = new JSchemaGenerator();
JSchema schema = generator.Generate(typeof(UniqueItemsClassWithoutAttribute));

Assert.IsNotNull(schema.Properties["UniqueItemsProperty"]);
Assert.IsFalse(schema.Properties["UniqueItemsProperty"].UniqueItems);
}

[Test]
public void GenerateUniqueItemsWithtAttributeButWithoutCollection()
{
JSchemaGenerator generator = new JSchemaGenerator();
JSchema schema = generator.Generate(typeof(UniqueItemsClassWithAttributeButWithoutCollection));

Assert.IsNotNull(schema.Properties["UniqueItemsProperty"]);
Assert.IsFalse(schema.Properties["UniqueItemsProperty"].UniqueItems);
}

internal class MyRootJsonClass
{
public Dictionary<string, BlockBase> Blocks { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ private void PopulateSchema(JSchema schema, JsonContract contract, JsonProperty
schema.Type = AddNullType(JSchemaType.Array, valueRequired);
schema.MinimumItems = AttributeHelpers.GetMinLength(memberProperty);
schema.MaximumItems = AttributeHelpers.GetMaxLength(memberProperty);
schema.UniqueItems = ReflectionUtils.IsISetType(nonNullableUnderlyingType) || AttributeHelpers.GetUniqueItems(memberProperty) ;

JsonArrayAttribute arrayAttribute = ReflectionUtils.GetAttribute<JsonArrayAttribute>(nonNullableUnderlyingType);

Expand Down
13 changes: 13 additions & 0 deletions Src/Newtonsoft.Json.Schema/Generation/UniqueItemsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Newtonsoft.Json.Schema.Generation
{

/// <summary>
/// Instructs the <see cref="JSchemaGenerator"/> to add unique items is true to the member.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Parameter, AllowMultiple = false)]
public class UniqueItemsAttribute : Attribute
{
}
}
12 changes: 12 additions & 0 deletions Src/Newtonsoft.Json.Schema/Infrastructure/AttributeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal static class AttributeHelpers
private const string EmailAddressAttributeName = "System.ComponentModel.DataAnnotations.EmailAddressAttribute";
private const string StringLengthAttributeName = "System.ComponentModel.DataAnnotations.StringLengthAttribute";
private const string EnumDataTypeAttributeName = "System.ComponentModel.DataAnnotations.EnumDataTypeAttribute";
private const string UniqueItemsAttributeName = "Newtonsoft.Json.Schema.Generation.UniqueItemsAttribute";

private static bool GetDisplay(Type type, JsonProperty memberProperty, out string name, out string description)
{
Expand Down Expand Up @@ -288,6 +289,17 @@ public static string GetFormat(JsonProperty property)
return null;
}

public static bool GetUniqueItems(JsonProperty property)
{
if (property == null)
{
return false;
}

var uniqueItems = GetAttributeByName(property, UniqueItemsAttributeName, out _);
return uniqueItems != null && ReflectionUtils.IsCollectionItemType(property.PropertyType);
}

private static Attribute GetAttributeByName(JsonProperty property, string name, out Type matchingType)
{
return GetAttributeByName(property.AttributeProvider, name, out matchingType);
Expand Down
60 changes: 60 additions & 0 deletions Src/Newtonsoft.Json.Schema/Infrastructure/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,66 @@ public static bool IsNullableType(Type t)
return (t.IsGenericType() && t.GetGenericTypeDefinition() == typeof(Nullable<>));
}

/// <summary>
/// Gets if the type is a collection.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>True if the type is a collection</returns>
public static bool IsCollectionItemType(Type type)
{
ValidationUtils.ArgumentNotNull(type, nameof(type));

if (type.IsArray)
{
return true;
}

if (ImplementsGenericDefinition(type, typeof(IEnumerable<>), out Type genericListType))
{
if (genericListType.IsGenericTypeDefinition())
{
return false;
}

return true;
}

if (typeof(IEnumerable).IsAssignableFrom(type))
{
return true;
}

return false;
}

/// <summary>
/// Checks if the type is an ISet.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>True if the type is an ISet</returns>
public static bool IsISetType(Type type)
{
#if NET35
return false;
#else
ValidationUtils.ArgumentNotNull(type, nameof(type));
foreach (Type i in type.GetInterfaces())
{
if (i.IsGenericType())
{
Type interfaceDefinition = i.GetGenericTypeDefinition();

if (typeof(ISet<>) == interfaceDefinition)
{
return true;
}
}
}

return false;
#endif
}

/// <summary>
/// Gets the type of the typed collection's items.
/// </summary>
Expand Down