From f176a299a7caed8314f39c2ce8d73c9f041e4e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Fr=C3=A8rebeau?= Date: Tue, 5 May 2020 17:52:45 +0200 Subject: [PATCH] Add UniqueItems Attribute #143 --- .../GenerateWithJSchemaAttributes.aml | 16 ++++ Doc/doc.content | 1 + Doc/doc.shfbproj | 1 + .../GenerateWithJSchemaAttributes.cs | 87 +++++++++++++++++++ .../JSchemaGeneratorTests.cs | 46 ++++++++++ .../Generation/JSchemaGeneratorInternal.cs | 1 + .../Generation/UniqueItemsAttribute.cs | 13 +++ .../Infrastructure/AttributeHelpers.cs | 12 +++ .../Infrastructure/ReflectionUtils.cs | 62 +++++++++++++ 9 files changed, 239 insertions(+) create mode 100644 Doc/Samples/Generation/GenerateWithJSchemaAttributes.aml create mode 100644 Src/Newtonsoft.Json.Schema.Tests/Documentation/Samples/Generation/GenerateWithJSchemaAttributes.cs create mode 100644 Src/Newtonsoft.Json.Schema/Generation/UniqueItemsAttribute.cs diff --git a/Doc/Samples/Generation/GenerateWithJSchemaAttributes.aml b/Doc/Samples/Generation/GenerateWithJSchemaAttributes.aml new file mode 100644 index 00000000..b0291022 --- /dev/null +++ b/Doc/Samples/Generation/GenerateWithJSchemaAttributes.aml @@ -0,0 +1,16 @@ + + + + + This sample generates a new T:Newtonsoft.Json.Schema.JSchema + from a .NET type with JSchema serialization attributes. + +
+ Sample + + + + +
+
+
\ No newline at end of file diff --git a/Doc/doc.content b/Doc/doc.content index 684b5444..8dfeb628 100644 --- a/Doc/doc.content +++ b/Doc/doc.content @@ -35,6 +35,7 @@ + diff --git a/Doc/doc.shfbproj b/Doc/doc.shfbproj index 67b8340d..27267cf9 100644 --- a/Doc/doc.shfbproj +++ b/Doc/doc.shfbproj @@ -110,6 +110,7 @@ + diff --git a/Src/Newtonsoft.Json.Schema.Tests/Documentation/Samples/Generation/GenerateWithJSchemaAttributes.cs b/Src/Newtonsoft.Json.Schema.Tests/Documentation/Samples/Generation/GenerateWithJSchemaAttributes.cs new file mode 100644 index 00000000..8daa79e8 --- /dev/null +++ b/Src/Newtonsoft.Json.Schema.Tests/Documentation/Samples/Generation/GenerateWithJSchemaAttributes.cs @@ -0,0 +1,87 @@ +#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 DiskIds { get; set; } + + public HashSet 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", + // "null" + // ], + // "items": { + // "type": [ + // "string", + // "null" + // ] + // }, + // "uniqueItems": true + // }, + // "ScreenIds": { + // "type": [ + // "array", + // "null" + // ], + // "items": { + // "type": [ + // "string", + // "null" + // ] + // }, + // "uniqueItems": true + // } + // }, + // "required": [ + // "DiskIds", + // "ScreenIds" + // ], + // "dependencies": {} + //} + #endregion + + Assert.IsTrue(schema.Properties["DiskIds"].UniqueItems); + Assert.IsTrue(schema.Properties["ScreenIds"].UniqueItems); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Schema.Tests/JSchemaGeneratorTests.cs b/Src/Newtonsoft.Json.Schema.Tests/JSchemaGeneratorTests.cs index 7e2e2c90..c2b8a5d2 100644 --- a/Src/Newtonsoft.Json.Schema.Tests/JSchemaGeneratorTests.cs +++ b/Src/Newtonsoft.Json.Schema.Tests/JSchemaGeneratorTests.cs @@ -1798,6 +1798,52 @@ public void GenerationProviderAttributeDerivedPlusConverterAttributeDerived() Assert.IsNotNull(schema.Properties["Provider"]); } + internal class UniqueItemsClassWithAttribute + { + [UniqueItems] + public IEnumerable UniqueItemsProperty { get; set; } + } + + internal class UniqueItemsClassWithHashSet + { + public HashSet UniqueItemsProperty { get; set; } + } + + internal class UniqueItemsClassWithoutAttribute + { + public IEnumerable 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); + } + + [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); + } + + [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); + } + internal class MyRootJsonClass { public Dictionary Blocks { get; set; } diff --git a/Src/Newtonsoft.Json.Schema/Generation/JSchemaGeneratorInternal.cs b/Src/Newtonsoft.Json.Schema/Generation/JSchemaGeneratorInternal.cs index 56dfca01..fcf7bee5 100644 --- a/Src/Newtonsoft.Json.Schema/Generation/JSchemaGeneratorInternal.cs +++ b/Src/Newtonsoft.Json.Schema/Generation/JSchemaGeneratorInternal.cs @@ -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(nonNullableUnderlyingType); diff --git a/Src/Newtonsoft.Json.Schema/Generation/UniqueItemsAttribute.cs b/Src/Newtonsoft.Json.Schema/Generation/UniqueItemsAttribute.cs new file mode 100644 index 00000000..15babd04 --- /dev/null +++ b/Src/Newtonsoft.Json.Schema/Generation/UniqueItemsAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Newtonsoft.Json.Schema.Generation +{ + + /// + /// Instructs the to add unique items is true to the member. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Parameter, AllowMultiple = false)] + public class UniqueItemsAttribute : Attribute + { + } +} diff --git a/Src/Newtonsoft.Json.Schema/Infrastructure/AttributeHelpers.cs b/Src/Newtonsoft.Json.Schema/Infrastructure/AttributeHelpers.cs index 8b409459..153599ef 100644 --- a/Src/Newtonsoft.Json.Schema/Infrastructure/AttributeHelpers.cs +++ b/Src/Newtonsoft.Json.Schema/Infrastructure/AttributeHelpers.cs @@ -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) { @@ -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); diff --git a/Src/Newtonsoft.Json.Schema/Infrastructure/ReflectionUtils.cs b/Src/Newtonsoft.Json.Schema/Infrastructure/ReflectionUtils.cs index 8a4a4ab6..20da7fe3 100644 --- a/Src/Newtonsoft.Json.Schema/Infrastructure/ReflectionUtils.cs +++ b/Src/Newtonsoft.Json.Schema/Infrastructure/ReflectionUtils.cs @@ -168,6 +168,68 @@ public static bool IsNullableType(Type t) return (t.IsGenericType() && t.GetGenericTypeDefinition() == typeof(Nullable<>)); } + /// + /// Gets if the type is a collection. + /// + /// The type. + /// True if the type is a collection + 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; + } + + /// + /// Gets if the type is a ISet. + /// + /// The type. + /// True if the type is a collection + 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 + } + + + /// /// Gets the type of the typed collection's items. ///