diff --git a/.editorconfig b/.editorconfig index 5c4c2f7..29d599f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -33,7 +33,7 @@ trim_trailing_whitespace = true # MSBuild project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] -indent_size = 2 +indent_size = 4 # Xml config files [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] @@ -84,7 +84,7 @@ dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case # Constants are PascalCase dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants -dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style +dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.constants.applicable_kinds = field, local dotnet_naming_symbols.constants.required_modifiers = const @@ -94,7 +94,7 @@ dotnet_naming_style.constant_style.capitalization = pascal_case # Static fields are camelCase dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields -dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style +dotnet_naming_rule.static_fields_should_be_camel_case.style = camel_case_style dotnet_naming_symbols.static_fields.applicable_kinds = field dotnet_naming_symbols.static_fields.required_modifiers = static @@ -104,7 +104,7 @@ dotnet_naming_style.static_field_style.capitalization = camel_case # Instance fields are camelCase dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields -dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style +dotnet_naming_rule.instance_fields_should_be_camel_case.style = camel_case_style dotnet_naming_symbols.instance_fields.applicable_kinds = field @@ -122,7 +122,7 @@ dotnet_naming_style.camel_case_style.capitalization = camel_case # Local functions are PascalCase dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions -dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style +dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.local_functions.applicable_kinds = local_function @@ -131,7 +131,7 @@ dotnet_naming_style.local_function_style.capitalization = pascal_case # By default, name items with PascalCase dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members -dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.all_members.applicable_kinds = * @@ -209,6 +209,7 @@ dotnet_diagnostic.SA1130.severity = silent # IDE1006: Naming Styles - StyleCop handles these for us dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.IDE0290.severity = silent dotnet_diagnostic.DOC100.severity = silent dotnet_diagnostic.DOC104.severity = warning @@ -226,7 +227,7 @@ csharp_prefer_simple_using_statement = true:suggestion csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_primary_constructors = false:silent csharp_prefer_system_threading_lock = true:suggestion csharp_style_expression_bodied_lambdas = when_on_single_line:silent csharp_style_expression_bodied_local_functions = when_on_single_line:silent diff --git a/.runsettings b/.runsettings new file mode 100644 index 0000000..b4601e7 --- /dev/null +++ b/.runsettings @@ -0,0 +1,11 @@ + + + + + + true + false + 5 + + + diff --git a/nvim-diff.runsettings b/nvim-diff.runsettings new file mode 100644 index 0000000..017ba6f --- /dev/null +++ b/nvim-diff.runsettings @@ -0,0 +1,11 @@ + + + + + Neovim + false + false + 5 + + + diff --git a/src/SharpSchema.Annotations.Source/SharpSchema.Annotations.Source.csproj b/src/SharpSchema.Annotations.Source/SharpSchema.Annotations.Source.csproj index 38285dd..ef4752a 100644 --- a/src/SharpSchema.Annotations.Source/SharpSchema.Annotations.Source.csproj +++ b/src/SharpSchema.Annotations.Source/SharpSchema.Annotations.Source.csproj @@ -14,4 +14,10 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/SharpSchema.Generator/Accessibilities.cs b/src/SharpSchema.Annotations/AccessibilityMode.cs similarity index 83% rename from src/SharpSchema.Generator/Accessibilities.cs rename to src/SharpSchema.Annotations/AccessibilityMode.cs index 78f45f9..2dadef7 100644 --- a/src/SharpSchema.Generator/Accessibilities.cs +++ b/src/SharpSchema.Annotations/AccessibilityMode.cs @@ -1,10 +1,17 @@ -namespace SharpSchema.Generator; +using System; + +namespace SharpSchema.Annotations; /// /// Specifies the allowed accessibilities. /// [Flags] -public enum Accessibilities +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +enum AccessibilityMode { /// /// Indicates public accessibility. diff --git a/src/SharpSchema.Generator/DictionaryKeyMode.cs b/src/SharpSchema.Annotations/DictionaryKeyMode.cs similarity index 84% rename from src/SharpSchema.Generator/DictionaryKeyMode.cs rename to src/SharpSchema.Annotations/DictionaryKeyMode.cs index c1f482e..8ddb3df 100644 --- a/src/SharpSchema.Generator/DictionaryKeyMode.cs +++ b/src/SharpSchema.Annotations/DictionaryKeyMode.cs @@ -1,11 +1,16 @@ -using Json.Schema; - -namespace SharpSchema.Generator; + +namespace SharpSchema.Annotations; /// /// Specifies the mode for dictionary keys. /// -public enum DictionaryKeyMode +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +enum DictionaryKeyMode + { /// /// Loose mode allows any type of dictionary key, diff --git a/src/SharpSchema.Generator/EnumHandling.cs b/src/SharpSchema.Annotations/EnumMode.cs similarity index 73% rename from src/SharpSchema.Generator/EnumHandling.cs rename to src/SharpSchema.Annotations/EnumMode.cs index 8782410..f284800 100644 --- a/src/SharpSchema.Generator/EnumHandling.cs +++ b/src/SharpSchema.Annotations/EnumMode.cs @@ -1,9 +1,14 @@ -namespace SharpSchema.Generator; +namespace SharpSchema.Annotations; /// /// Specifies how enums are handled in the generated code. /// -public enum EnumHandling +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +enum EnumMode { /// /// Enum values are represented as strings. diff --git a/src/SharpSchema.Annotations/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs new file mode 100644 index 0000000..188e71e --- /dev/null +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -0,0 +1,42 @@ +using SharpSchema.Annotations; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace SharpSchema.Generator; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Represents the options for the generator. +/// +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +record GeneratorOptions( + AccessibilityMode AccessibilityMode = AccessibilityMode.Public, + TraversalMode TraversalMode = TraversalMode.Bases, + DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, + EnumMode EnumMode = EnumMode.String, + NumberMode NumberMode = NumberMode.StrictDefs) +{ + /// + /// Initializes a new instance of the class by copying the values from another instance. + /// + /// The instance to copy values from. + public GeneratorOptions(GeneratorOptions options) + { + if (options is null) + throw new System.ArgumentNullException(nameof(options)); + + AccessibilityMode = options.AccessibilityMode; + TraversalMode = options.TraversalMode; + DictionaryKeyMode = options.DictionaryKeyMode; + EnumMode = options.EnumMode; + NumberMode = options.NumberMode; + } + + /// + /// Gets the default generator options. + /// + public static GeneratorOptions Default { get; } = new GeneratorOptions(); +} diff --git a/src/SharpSchema.Annotations/NumberMode.cs b/src/SharpSchema.Annotations/NumberMode.cs new file mode 100644 index 0000000..0c1d0ce --- /dev/null +++ b/src/SharpSchema.Annotations/NumberMode.cs @@ -0,0 +1,29 @@ +namespace SharpSchema.Annotations; + +/// +/// Specifies the mode for handling numeric types in the schema generation. +/// +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +enum NumberMode +{ + /// + /// Uses the best matching type for numbers, adding range validation based on .NET type. + /// Numeric subschema placed in $defs, and referenced inline. + /// + StrictDefs, + + /// + /// Uses the best matching type for numbers, adding range validation based on .NET type. + /// Numeric subschema are always placed inline. + /// + StrictInline, + + /// + /// Uses the best matching json type for numbers, without adding range validation. + /// + JsonNative, +} diff --git a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs new file mode 100644 index 0000000..f068991 --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default traversal option for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedTypes)] +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute +{ + /// + /// Gets the accessibility option for the type. + /// + public AccessibilityMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaConstAttribute.cs b/src/SharpSchema.Annotations/SchemaConstAttribute.cs index b522bbd..1c7d178 100644 --- a/src/SharpSchema.Annotations/SchemaConstAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaConstAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaConstAttribute(object value) : SchemaAttribute +public #else -internal class SchemaConstAttribute(object value) : SchemaAttribute +internal #endif +class SchemaConstAttribute(object value) : SchemaAttribute { /// /// Gets the constant value. diff --git a/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs new file mode 100644 index 0000000..3740bfc --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default dictionary key mode for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedMembers)] +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute +{ + /// + /// Gets the dictionary key mode. + /// + public DictionaryKeyMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs new file mode 100644 index 0000000..87e8b8f --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default enum mode for a given type. +/// +[AttributeUsage(AttributeTargets.Enum)] +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute +{ + /// + /// Gets the enum mode. + /// + public EnumMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaEnumValueAttribute.cs b/src/SharpSchema.Annotations/SchemaEnumValueAttribute.cs index 308ef9c..a965168 100644 --- a/src/SharpSchema.Annotations/SchemaEnumValueAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaEnumValueAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(AttributeTargets.Field)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaEnumValueAttribute(string value) : SchemaAttribute +public #else -internal class SchemaEnumValueAttribute(string value) : SchemaAttribute +internal #endif +class SchemaEnumValueAttribute(string value) : SchemaAttribute { /// /// Gets the value of the enum member. diff --git a/src/SharpSchema.Annotations/SchemaFormatAttribute.cs b/src/SharpSchema.Annotations/SchemaFormatAttribute.cs index 179ccc5..5e488d6 100644 --- a/src/SharpSchema.Annotations/SchemaFormatAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaFormatAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaFormatAttribute(string format) : SchemaAttribute +public #else -internal class SchemaFormatAttribute(string format) : SchemaAttribute +internal #endif +class SchemaFormatAttribute(string format) : SchemaAttribute { /// /// Gets the format of the schema. diff --git a/src/SharpSchema.Annotations/SchemaIgnoreAttribute.cs b/src/SharpSchema.Annotations/SchemaIgnoreAttribute.cs index b4b65f6..7243e7a 100644 --- a/src/SharpSchema.Annotations/SchemaIgnoreAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaIgnoreAttribute.cs @@ -9,9 +9,10 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedTypes | SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaIgnoreAttribute : SchemaAttribute +public #else -internal class SchemaIgnoreAttribute : SchemaAttribute +internal #endif +class SchemaIgnoreAttribute : SchemaAttribute { } diff --git a/src/SharpSchema.Annotations/SchemaItemsRangeAttribute.cs b/src/SharpSchema.Annotations/SchemaItemsRangeAttribute.cs index 4ac058f..3fa2c08 100644 --- a/src/SharpSchema.Annotations/SchemaItemsRangeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaItemsRangeAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaItemsRangeAttribute : SchemaAttribute +public #else -internal class SchemaItemsRangeAttribute : SchemaAttribute +internal #endif +class SchemaItemsRangeAttribute : SchemaAttribute { /// /// Gets or sets the minimum items allowed for the array. diff --git a/src/SharpSchema.Annotations/SchemaLengthRangeAttribute.cs b/src/SharpSchema.Annotations/SchemaLengthRangeAttribute.cs index 771753b..a35967e 100644 --- a/src/SharpSchema.Annotations/SchemaLengthRangeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaLengthRangeAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaLengthRangeAttribute : SchemaAttribute +public #else -internal class SchemaLengthRangeAttribute : SchemaAttribute +internal #endif +class SchemaLengthRangeAttribute : SchemaAttribute { /// /// Gets or sets the minimum length allowed for the schema. diff --git a/src/SharpSchema.Annotations/SchemaMetaAttribute.cs b/src/SharpSchema.Annotations/SchemaMetaAttribute.cs index 30abe25..7c40a77 100644 --- a/src/SharpSchema.Annotations/SchemaMetaAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaMetaAttribute.cs @@ -11,10 +11,11 @@ namespace SharpSchema.Annotations; [ExcludeFromCodeCoverage] [AttributeUsage(SupportedTypes | SupportedMembers | EnumTargets)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaMetaAttribute : SchemaAttribute +public #else -internal class SchemaMetaAttribute : SchemaAttribute +internal #endif +class SchemaMetaAttribute : SchemaAttribute { /// /// Gets or sets the title of the schema. diff --git a/src/SharpSchema.Annotations/SchemaOverrideAttribute.cs b/src/SharpSchema.Annotations/SchemaOverrideAttribute.cs index 165baa6..16ce9f6 100644 --- a/src/SharpSchema.Annotations/SchemaOverrideAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaOverrideAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedTypes | SupportedMembers | EnumTargets)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaOverrideAttribute(string value) : SchemaAttribute +public #else -internal class SchemaOverrideAttribute(string value) : SchemaAttribute +internal #endif +class SchemaOverrideAttribute(string value) : SchemaAttribute { /// /// Gets the overridden value for the schema. diff --git a/src/SharpSchema.Annotations/SchemaPropertiesRangeAttribute.cs b/src/SharpSchema.Annotations/SchemaPropertiesRangeAttribute.cs index 4468348..0c0d375 100644 --- a/src/SharpSchema.Annotations/SchemaPropertiesRangeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaPropertiesRangeAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedTypes | SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaPropertiesRangeAttribute : SchemaAttribute +public #else -internal class SchemaPropertiesRangeAttribute : SchemaAttribute +internal #endif +class SchemaPropertiesRangeAttribute : SchemaAttribute { /// /// Gets or sets the minimum number of properties allowed in a schema. diff --git a/src/SharpSchema.Annotations/SchemaRegexAttribute.cs b/src/SharpSchema.Annotations/SchemaRegexAttribute.cs index e721548..953895b 100644 --- a/src/SharpSchema.Annotations/SchemaRegexAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaRegexAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaRegexAttribute(string pattern) : SchemaAttribute +public #else -internal class SchemaRegexAttribute(string pattern) : SchemaAttribute +internal #endif +class SchemaRegexAttribute(string pattern) : SchemaAttribute { /// /// Gets the regular expression pattern. diff --git a/src/SharpSchema.Annotations/SchemaRequiredAttribute.cs b/src/SharpSchema.Annotations/SchemaRequiredAttribute.cs index d0bce15..4f8b0d3 100644 --- a/src/SharpSchema.Annotations/SchemaRequiredAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaRequiredAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaRequiredAttribute(bool isRequired = true) : SchemaAttribute +public #else -internal class SchemaRequiredAttribute(bool isRequired = true) : SchemaAttribute +internal #endif +class SchemaRequiredAttribute(bool isRequired = true) : SchemaAttribute { /// /// Gets a value indicating whether the property is required. diff --git a/src/SharpSchema.Annotations/SchemaRootAttribute.cs b/src/SharpSchema.Annotations/SchemaRootAttribute.cs index 550b1b9..fa2e4fe 100644 --- a/src/SharpSchema.Annotations/SchemaRootAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaRootAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedTypes)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaRootAttribute : SchemaAttribute +public #else -internal class SchemaRootAttribute : SchemaAttribute +internal #endif +class SchemaRootAttribute : SchemaAttribute { /// /// Gets or sets the file name of the schema. diff --git a/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs new file mode 100644 index 0000000..7474f56 --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default traversal option for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedTypes)] +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute +{ + /// + /// Gets the traversal option for the type. + /// + public TraversalMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaValueRangeAttribute.cs b/src/SharpSchema.Annotations/SchemaValueRangeAttribute.cs index 709e9f2..e1045e0 100644 --- a/src/SharpSchema.Annotations/SchemaValueRangeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaValueRangeAttribute.cs @@ -12,10 +12,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaValueRangeAttribute : SchemaAttribute +public #else -internal class SchemaValueRangeAttribute : SchemaAttribute +internal #endif +class SchemaValueRangeAttribute : SchemaAttribute { /// /// Gets or sets the minimum value allowed for the property. diff --git a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj index 1a8ef26..7235686 100644 --- a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj +++ b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj @@ -8,4 +8,10 @@ Annotations library for the SharpSchema project. SHARPSCHEMA_ASSEMBLY + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/SharpSchema.Generator/Traversal.cs b/src/SharpSchema.Annotations/TraversalMode.cs similarity index 80% rename from src/SharpSchema.Generator/Traversal.cs rename to src/SharpSchema.Annotations/TraversalMode.cs index d11bd1c..46c9c1c 100644 --- a/src/SharpSchema.Generator/Traversal.cs +++ b/src/SharpSchema.Annotations/TraversalMode.cs @@ -1,10 +1,17 @@ -namespace SharpSchema.Generator; +using System; + +namespace SharpSchema.Annotations; /// /// Specifies the traversal options for symbols. /// [Flags] -public enum Traversal +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +enum TraversalMode { /// /// Traverse only the symbol itself. diff --git a/src/SharpSchema.Generator/GeneratorOptions.cs b/src/SharpSchema.Generator/GeneratorOptions.cs deleted file mode 100644 index 8a4a249..0000000 --- a/src/SharpSchema.Generator/GeneratorOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SharpSchema.Generator; - -/// -/// The options for the generator. -/// -/// The accessibilities to consider. -/// The traversal options. -/// The mode for dictionary keys. -/// The options for handling enums. -public record GeneratorOptions( - Accessibilities Accessibilities = Accessibilities.Public, - Traversal Traversal = Traversal.SymbolOnly, - DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, - EnumHandling EnumHandling = EnumHandling.String) -{ - /// - /// Gets the default generator options. - /// - public static GeneratorOptions Default { get; } = new GeneratorOptions(); -} diff --git a/src/SharpSchema.Generator/GenericTypeSymbolVisitor.cs b/src/SharpSchema.Generator/GenericTypeSymbolVisitor.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs deleted file mode 100644 index a12120f..0000000 --- a/src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs +++ /dev/null @@ -1,355 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis; -using SharpSchema.Generator.Utilities; -using SharpSchema.Generator.Model; -using Json.Schema; -using System.Text.Json.Nodes; - -namespace SharpSchema.Generator; - -using Builder = JsonSchemaBuilder; -using Metadata = Model.Metadata; - -internal class LeafDeclaredTypeSyntaxVisitor : CSharpSyntaxVisitor -{ - private readonly Compilation _compilation; - private readonly GeneratorOptions _options; - private readonly SemanticModelCache _semanticModelCache; - private readonly Metadata.SymbolVisitor _metadataVisitor; - private readonly CollectionSymbolVisitor _collectionVisitor; - private readonly Dictionary _cachedTypeSchemas; - private readonly Dictionary _cachedBaseSchemas; - - public LeafDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions options) - { - _compilation = compilation; - _options = options; - _semanticModelCache = new(compilation); - _metadataVisitor = new(); - _collectionVisitor = new(options, compilation, this); - _cachedTypeSchemas = []; - _cachedBaseSchemas = []; - } - - internal GeneratorOptions Options => _options; - - public IReadOnlyDictionary? GetCachedSchemas() - { - if (_cachedTypeSchemas.Count == 0) - return null; - - return _cachedTypeSchemas.ToDictionary( - p => p.Key, - p => p.Value.Build()); - } - - public override Builder? Visit(SyntaxNode? node) - { - if (node is null) - return null; - - using var trace = Tracer.Enter($"[LEAF] {node.Kind()}"); - return base.Visit(node); - } - - public override Builder? VisitQualifiedName(QualifiedNameSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.ToString()); - - TypeInfo typeInfo = _semanticModelCache.GetSemanticModel(node).GetTypeInfo(node); - if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for qualified name '{node}'."); - - return this.Visit(typeSymbol.FindBaseTypeDeclaration()); - } - - public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return VisitTypeDeclaration(node); - } - - public override Builder? VisitStructDeclaration(StructDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return VisitTypeDeclaration(node); - } - - public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return VisitTypeDeclaration(node); - } - - public override Builder? VisitSimpleBaseType(SimpleBaseTypeSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Type.ToString()); - if (node.Type is not IdentifierNameSyntax nameSyntax) - return CommonSchemas.UnsupportedObject($"The base type syntax is not supported for generation '{node.Type}'."); - - if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node.Type).Type is not INamedTypeSymbol baseTypeSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for base type '{node.Type}'."); - - string cacheKey = baseTypeSymbol.GetDefCacheKey(); - if (!_cachedBaseSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) - { - cachedSchema = baseTypeSymbol.FindTypeDeclaration()?.CreateTypeSchema(this); - if (cachedSchema is not null) - { - _cachedBaseSchemas[cacheKey] = cachedSchema; - } - } - - return cachedSchema; - } - - public override Builder? VisitEnumDeclaration(EnumDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - - if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol enumSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'."); - - if (enumSymbol.GetOverrideSchema() is Builder overrideSchema) - return overrideSchema; - - string cacheKey = enumSymbol.GetDefCacheKey(); - if (!_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) - { - - Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(enumSymbol)); - - Builder builder = EnumSymbolVisitor.Instance.Visit(enumSymbol, _options) ?? - CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'."); - - _cachedTypeSchemas[cacheKey] = builder.ApplyMetadata(metadata); - } - - return CommonSchemas.DefRef(cacheKey); - } - - public override Builder? VisitPropertyDeclaration(PropertyDeclarationSyntax node) - { - Throw.IfNullArgument(node); - - using var trace = Tracer.Enter(node.Identifier.Text); - - if (node.GetDeclaredSymbol(_semanticModelCache) is not IPropertySymbol propertySymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for property '{node.Identifier}'."); - - if (!_options.ShouldProcess(propertySymbol)) - return null; - - if (propertySymbol.GetOverrideSchema() is Builder overrideSchema) - return overrideSchema; - - Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(propertySymbol)); - - return GetPropertySchema(node.Type, metadata, node.Initializer?.Value); - } - - public override Builder? VisitParameter(ParameterSyntax node) - { - Throw.IfNullArgument(node); - - using var trace = Tracer.Enter(node.Identifier.Text); - - if (node.GetDeclaredSymbol(_semanticModelCache) is not IParameterSymbol parameterSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for parameter '{node.Identifier}'."); - - if (!_options.ShouldProcess(parameterSymbol)) - return null; - - if (parameterSymbol.GetOverrideSchema() is Builder overrideSchema) - return overrideSchema; - - Metadata metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(parameterSymbol)); - - TypeSyntax? typeSyntax = node.Type; - if (typeSyntax is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for parameter type '{typeSyntax}'."); - - return GetPropertySchema(typeSyntax, metadata, node.Default?.Value); - } - - public override Builder? VisitIdentifierName(IdentifierNameSyntax node) - { - Throw.IfNullArgument(node); - - using var trace = Tracer.Enter(node.Identifier.Text); - - TypeInfo typeInfo = _semanticModelCache.GetSemanticModel(node).GetTypeInfo(node); - if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for identifier '{node.Identifier}'."); - - return this.Visit(typeSymbol.FindBaseTypeDeclaration()) - ?? CommonSchemas.UnsupportedObject($"Could not find declaration for identifier '{node.Identifier}'."); - } - - public override Builder? VisitGenericName(GenericNameSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - - if (node.IsUnboundGenericName) - return CommonSchemas.UnsupportedObject($"Failed to evaluate unbound generic type '{node.Identifier}'."); - - SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(node); - - if (semanticModel.GetTypeInfo(node).Type is not INamedTypeSymbol boundTypeSymbol) - return CommonSchemas.UnsupportedObject($"Node '{node.Identifier.Text}' does not produce a type symbol."); - - Builder? collectionBuilder = _collectionVisitor.Visit(boundTypeSymbol); - if (collectionBuilder is not null) - return collectionBuilder; - - return this.Visit(boundTypeSymbol.FindTypeDeclaration()); - } - - public override Builder? VisitNullableType(NullableTypeSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Kind().ToString()); - - return CommonSchemas.Nullable( - Throw.ForUnexpectedNull( - node.ElementType.Accept(this))); - } - - public override Builder? VisitPredefinedType(PredefinedTypeSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Keyword.Text); - - if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol) - { - return CommonSchemas.UnsupportedObject($"Failed to build schema for predefined type '{node}'."); - } - - if (typeSymbol.IsJsonDefinedType(out Builder? valueTypeSchema)) - { - return valueTypeSchema; - } - - return CommonSchemas.UnsupportedObject($"Unexpected built-in type '{node.Keyword.Text}'."); - } - - public override Builder? VisitArrayType(ArrayTypeSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Kind().ToString()); - - Builder? elementSchema = node.ElementType.Accept(this); - if (elementSchema is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{node.ElementType}'."); - - return CommonSchemas.ArrayOf(elementSchema); - } - - private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node) - { - if (_semanticModelCache.GetSemanticModel(node).GetDeclaredSymbol(node) is not INamedTypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed building schema for {node.Identifier.ValueText}"); - - string typeId = typeSymbol.GetDefCacheKey(); - if (_cachedTypeSchemas.TryGetValue(typeId, out _)) - return CommonSchemas.DefRef(typeId); - - if (typeSymbol.GetOverrideSchema() is Builder overrideSchema) - return overrideSchema; - - // Handle abstract types - if (typeSymbol.IsAbstract) - { - List oneOfBuilders = []; - foreach (INamedTypeSymbol concrete in GetConcreteSubtypes(typeSymbol)) - { - TypeDeclarationSyntax? decl = concrete.FindTypeDeclaration(); - if (decl is not null) - { - var subtypeBuilder = this.Visit(decl); - if (subtypeBuilder is not null) - oneOfBuilders.Add(subtypeBuilder); - } - } - - if (oneOfBuilders.Count > 0) - { - Builder abstractBuilder = CommonSchemas.Object; // Create a new def builder. - abstractBuilder = abstractBuilder.OneOf([.. oneOfBuilders]); - _cachedTypeSchemas[typeId] = abstractBuilder; - - return CommonSchemas.DefRef(typeId); - } - } - - // Regular concrete type. - Builder builder = node.CreateTypeSchema(this); - - _cachedTypeSchemas[typeId] = builder; - return CommonSchemas.DefRef(typeId); - } - - // Helper to get all concrete subtypes of an abstract class. - private IEnumerable GetConcreteSubtypes(INamedTypeSymbol abstractType) - { - using var trace = Tracer.Enter(abstractType.Name); - foreach (SyntaxTree tree in _compilation.SyntaxTrees) - { - SemanticModel model = _compilation.GetSemanticModel(tree); - foreach (var node in tree.GetRoot().DescendantNodes().OfType()) - { - if (model.GetDeclaredSymbol(node) is not INamedTypeSymbol symbol) - continue; - - if (!symbol.IsAbstract && symbol.InheritsFrom(abstractType)) - { - trace.WriteLine($"Found concrete subtype: {symbol.Name}"); - yield return symbol; - } - } - } - } - - private Builder GetPropertySchema(TypeSyntax typeSyntax, Metadata metadata, ExpressionSyntax? defaultExpression) - { - SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(typeSyntax); - if (semanticModel.GetTypeInfo(typeSyntax).Type is not ITypeSymbol typeSymbol || !typeSymbol.IsValidForGeneration()) - return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{typeSyntax}'."); - - Builder? propertyBuilder = null; - - // These kinds are never directly cached - if (typeSyntax.Kind() is SyntaxKind.NullableType or SyntaxKind.ArrayType or SyntaxKind.PredefinedType) - propertyBuilder = this.Visit(typeSyntax); - - string typeId = typeSymbol.GetDefCacheKey(); - if (propertyBuilder is null && _cachedTypeSchemas.TryGetValue(typeId, out _)) - propertyBuilder = CommonSchemas.DefRef(typeId); - - if (propertyBuilder is null && this.Visit(typeSyntax) is Builder builder) - propertyBuilder = builder; - - if (propertyBuilder is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{typeSymbol.Name}'."); - - propertyBuilder.ApplyMetadata(metadata); - - JsonNode? defaultValue = null; - if (defaultExpression is not null && semanticModel.GetConstantValue(defaultExpression) is { HasValue: true } constantValue) - { - defaultValue = JsonValue.Create(constantValue.Value); - } - - return defaultValue is not null - ? propertyBuilder.Default(defaultValue) - : propertyBuilder; - } -} diff --git a/src/SharpSchema.Generator/LeafSyntaxVisitor.Unsupported.cs b/src/SharpSchema.Generator/LeafSyntaxVisitor.Unsupported.cs new file mode 100644 index 0000000..ef0ec9a --- /dev/null +++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.Unsupported.cs @@ -0,0 +1,21 @@ +namespace SharpSchema.Generator; + +internal partial class LeafSyntaxVisitor +{ + internal static class Unsupported + { + public const string EnumMessage = "Failed to build schema for enum '{0}'."; + public const string IdentifierMessage = "Failed to build schema for identifier '{0}'."; + public const string DeclarationMessage = "Could not find declaration for identifier '{0}'."; + public const string UnboundGenericMessage = "Failed to evaluate unbound generic type '{0}'."; + public const string TypeSymbolMessage = "Node '{0}' does not produce a type symbol."; + public const string DictionaryElementMessage = "Failed to build schema for dictionary element type '{0}'."; + public const string KeyTypeMessage = "Key type '{0}' is not supported."; + public const string ArrayElementMessage = "Failed to build schema for array element type '{0}'."; + public const string GenericTypeMessage = "Failed to build schema for generic type '{0}'."; + public const string NullableElementMessage = "Failed to build schema for nullable element type '{0}'."; + public const string PredefinedTypeMessage = "Failed to build schema for predefined type '{0}'."; + public const string TypeMessage = "Failed to build schema for type '{0}'."; + public const string SymbolMessage = "Failed building schema for {0}"; + } +} diff --git a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs new file mode 100644 index 0000000..f5d0c41 --- /dev/null +++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs @@ -0,0 +1,346 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; +using Json.Schema; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Annotations; +using SharpSchema.Generator.Model; +using SharpSchema.Generator.Resolvers; +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator; + +using Builder = JsonSchemaBuilder; + +internal partial class LeafSyntaxVisitor : CSharpSyntaxVisitor +{ + private readonly RootContext _context; + private readonly GeneratorOptions _options; + + public LeafSyntaxVisitor(RootContext context, GeneratorOptions options) + { + _context = context; + _options = options; + } + + [ExcludeFromCodeCoverage] + public override Builder? DefaultVisit(SyntaxNode node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope scope = Tracer.Enter($"{node.Kind()} {node.GetLocation().GetLineSpan().StartLinePosition.Line}"); + return null; + } + + public override Builder? VisitQualifiedName(QualifiedNameSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Right.Identifier.Text); + return this.Visit(node.Right); + } + + public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node, trace); + } + + public override Builder? VisitStructDeclaration(StructDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node, trace); + } + + public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node, trace); + } + + public override Builder? VisitEnumDeclaration(EnumDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + if (node.GetDeclaredSymbol(_context.SemanticModelCache) is not INamedTypeSymbol enumSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); + + if (enumSymbol.GetOverrideSchema() is Builder overrideSchema) + return overrideSchema; + + string cacheKey = enumSymbol.GetDefCacheKey(); + if (_context.CachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + return CommonSchemas.DefRef(cacheKey); + + MemberMeta metadata = Throw.IfUnexpectedNull(MemberMeta.SymbolVisitor.Default.Visit(enumSymbol)); + + Builder builder = EnumResolver.Resolve(enumSymbol, _options) ?? + CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); + + _context.CachedTypeSchemas[cacheKey] = builder.ApplyMemberMeta(metadata); + + return CommonSchemas.DefRef(cacheKey); + } + + public override Builder? VisitIdentifierName(IdentifierNameSyntax node) + { + Throw.IfNullArgument(node); + + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + TypeInfo typeInfo = node.GetTypeInfo(_context.SemanticModelCache); + if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.IdentifierMessage, node.Identifier); + + if (typeSymbol.FindDeclaringSyntax() is SyntaxNode syntaxNode) + return this.Visit(syntaxNode); + + return null; + } + + public override Builder? VisitGenericName(GenericNameSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + if (node.IsUnboundGenericName) + return CommonSchemas.UnsupportedObject(Unsupported.UnboundGenericMessage, node.Identifier); + + if (node.GetTypeInfo(_context.SemanticModelCache).Type is not INamedTypeSymbol boundTypeSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.TypeSymbolMessage, node.Identifier.Text); + + if (boundTypeSymbol.HasUnresolvedTypeArguments()) + { + trace.WriteLine("Unresolved type arguments found."); + return null; + } + + var (kind, keyType, elementSymbol) = _context.CollectionResolver.Resolve(boundTypeSymbol); + + return kind switch + { + CollectionKind.Dictionary => HandleDictionaryType(node, keyType, elementSymbol), + CollectionKind.Array => HandleArrayType(node, elementSymbol), + _ => HandleGenericType(node, boundTypeSymbol) + }; + + // -- Local functions -- + + Builder? HandleDictionaryType(GenericNameSyntax node, SchemaValueType keyType, ITypeSymbol elementSymbol) + { + if (node.TypeArgumentList.Arguments[^1].Accept(this) is not Builder elementSchema) + return CommonSchemas.UnsupportedObject(Unsupported.DictionaryElementMessage, elementSymbol.Name); + + if (keyType is SchemaValueType.String) + return CommonSchemas.Object.AdditionalProperties(elementSchema); + + return _options.DictionaryKeyMode switch + { + DictionaryKeyMode.Skip => null, + DictionaryKeyMode.Strict => CommonSchemas.UnsupportedObject(Unsupported.KeyTypeMessage, keyType), + DictionaryKeyMode.Loose => CommonSchemas.Object + .Comment($"Key type '{keyType}' must be convertible to string") + .AdditionalProperties(elementSchema), + DictionaryKeyMode.Silent => CommonSchemas.Object + .AdditionalProperties(elementSchema), + _ => Throw.UnknownEnumValue(_options.DictionaryKeyMode) + }; + } + + Builder? HandleArrayType(GenericNameSyntax node, ITypeSymbol elementSymbol) + { + Builder? elementSchema = node.TypeArgumentList.Arguments[^1].Accept(this); + if (elementSchema is null) + return CommonSchemas.UnsupportedObject(Unsupported.ArrayElementMessage, elementSymbol); + + return CommonSchemas.ArrayOf(elementSchema); + } + + Builder? HandleGenericType(GenericNameSyntax node, INamedTypeSymbol boundTypeSymbol) + { + Builder? boundTypeBuilder = Visit(boundTypeSymbol.FindDeclaringSyntax()); + if (boundTypeBuilder is null) + return CommonSchemas.UnsupportedObject(Unsupported.GenericTypeMessage, node.Identifier); + + // Add to the oneOf for the unbound generic type + INamedTypeSymbol unboundGeneric = boundTypeSymbol.ConstructUnboundGenericType(); + string cacheKey = unboundGeneric.GetDefCacheKey(); + + if (_context.CachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema) && + cachedSchema.Get()?.Schemas is IReadOnlyList currentOneOf) + { + _context.CachedTypeSchemas[cacheKey] = cachedSchema.OneOf(currentOneOf.Append(boundTypeBuilder)); + } + else if (_context.CachedTypeSchemas.TryGetValue(cacheKey, out cachedSchema)) + { + _context.CachedTypeSchemas[cacheKey] = CommonSchemas.Object.OneOf(cachedSchema, boundTypeBuilder); + } + + return boundTypeBuilder; + } + } + + public override Builder? VisitNullableType(NullableTypeSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString()); + + if (node.ElementType.Accept(this) is Builder elementSchema) + return CommonSchemas.Nullable(elementSchema); + + return null; + } + + public override Builder? VisitPredefinedType(PredefinedTypeSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Keyword.Text); + + if (node.Keyword.IsKind(SyntaxKind.StringKeyword)) + return CommonSchemas.String; + + if (node.Keyword.IsKind(SyntaxKind.BoolKeyword)) + return CommonSchemas.Boolean; + + if (_context.SemanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.PredefinedTypeMessage, node); + + bool shouldCache = _options.NumberMode is NumberMode.StrictDefs; + string cacheKey = typeSymbol.GetDefCacheKey(); + + if (shouldCache && _context.CachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + return CommonSchemas.DefRef(cacheKey); + + if (!typeSymbol.IsJsonDefinedType(_options.NumberMode, out Builder? valueTypeSchema)) + valueTypeSchema = CommonSchemas.UnsupportedObject(Unsupported.PredefinedTypeMessage, node.Keyword.Text); + + if (shouldCache) + { + _context.CachedTypeSchemas[cacheKey] = valueTypeSchema; + return CommonSchemas.DefRef(cacheKey); + } + + return valueTypeSchema; + } + + public override Builder? VisitArrayType(ArrayTypeSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString()); + + if (node.ElementType.Accept(this) is Builder elementSchema) + return CommonSchemas.ArrayOf(elementSchema); + + return null; + } + + public override Builder? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + Builder? typeBuilder = node.Type.Accept(this); + if (typeBuilder is null) + return null; + + if (node.ExpressionBody is ArrowExpressionClauseSyntax aec + && GetConstantValue(aec.Expression) is JsonNode constantValue) + { + typeBuilder = CommonSchemas.Const(constantValue); + } + else if (ExtractDefaultValue(node) is JsonNode defaultValue) + { + typeBuilder = typeBuilder.Default(defaultValue); + } + + return typeBuilder; + + // -- Local functions -- + + JsonNode? ExtractDefaultValue(PropertyDeclarationSyntax propertyDeclaration) + { + return propertyDeclaration.Initializer is EqualsValueClauseSyntax evc + ? GetConstantValue(evc.Value) + : null; + } + + JsonNode? GetConstantValue(SyntaxNode node) + { + SemanticModel sm = _context.SemanticModelCache.GetSemanticModel(node); + return sm.GetConstantValue(node) is Optional optValue + && optValue.HasValue + ? JsonValue.Create(optValue.Value) + : (JsonNode?)null; + } + } + + /// + /// Creates a type schema for the specified type declaration syntax. + /// + /// The type declaration syntax node. + /// A JSON schema builder for the type. + /// Thrown when node is null. + public Builder CreateTypeSchema(TypeDeclarationSyntax node) + { + Throw.IfNullArgument(node); + + if (node.GetDeclaredSymbol(_context.SemanticModelCache) is not INamedTypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.TypeMessage, node.Identifier); + + return this.CreateTypeSchema(typeSymbol, node); + } + + /// + /// Creates a type schema for the specified symbol and declaration syntax. + /// + /// The named type symbol. + /// The type declaration syntax node. + /// Optional traversal mode override. + /// A JSON schema builder for the type. + /// Thrown when node is null. + public Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax node, TraversalMode? traversalMode = null) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + NamedTypeResolver visitor = new(_context); + return visitor.Resolve(symbol, _options) ?? CommonSchemas.UnsupportedObject(symbol.Name); + } + + private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node, Tracer.TraceScope trace) + { + if (node.GetDeclaredSymbol(_context.SemanticModelCache) is not INamedTypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject(Unsupported.SymbolMessage, node.Identifier.ValueText); + + string typeId = typeSymbol.GetDefCacheKey(); + + if (_context.CachedTypeSchemas.TryGetValue(typeId, out _)) + return CommonSchemas.DefRef(typeId); + + if (typeSymbol.GetOverrideSchema() is Builder overrideSchema) + return overrideSchema; + + if (typeSymbol.IsAbstract) + { + trace.WriteLine($"Found abstract type '{typeSymbol.Name}'."); + if (!_context.CachedAbstractSymbols.ContainsKey(typeId)) + { + _context.CachedAbstractSymbols.Add(typeId, typeSymbol); + } + + return CommonSchemas.DefRef(typeId); + } + + AttributeHandler schemaTraversal = typeSymbol.GetAttributeHandler(); + TraversalMode effectiveTraversalMode = schemaTraversal.Get(0) ?? _options.TraversalMode; + + // Regular concrete type. + Builder builder = CreateTypeSchema(typeSymbol, node, effectiveTraversalMode); + + _context.CachedTypeSchemas[typeId] = builder; + return CommonSchemas.DefRef(typeId); + } +} diff --git a/src/SharpSchema.Generator/Model/CommonSchemas.cs b/src/SharpSchema.Generator/Model/CommonSchemas.cs index ac4fcac..e2995a9 100644 --- a/src/SharpSchema.Generator/Model/CommonSchemas.cs +++ b/src/SharpSchema.Generator/Model/CommonSchemas.cs @@ -1,4 +1,5 @@ -using Json.Schema; +using System.Text.Json.Nodes; +using Json.Schema; using SharpSchema.Generator.Utilities; namespace SharpSchema.Generator.Model; @@ -31,83 +32,78 @@ static CommonSchemas() public static Builder System_Byte => new Builder() .Type(SchemaValueType.Integer) .Minimum(byte.MinValue) - .Maximum(byte.MaxValue) - .Comment("System.Byte"); + .Maximum(byte.MaxValue); public static Builder System_SByte => new Builder() .Type(SchemaValueType.Integer) .Minimum(sbyte.MinValue) - .Maximum(sbyte.MaxValue) - .Comment("System.SByte"); + .Maximum(sbyte.MaxValue); public static Builder System_Int16 => new Builder() .Type(SchemaValueType.Integer) .Minimum(short.MinValue) - .Maximum(short.MaxValue) - .Comment("System.Int16"); + .Maximum(short.MaxValue); public static Builder System_UInt16 => new Builder() .Type(SchemaValueType.Integer) .Minimum(ushort.MinValue) - .Maximum(ushort.MaxValue) - .Comment("System.UInt16"); + .Maximum(ushort.MaxValue); public static Builder System_Int32 => new Builder() .Type(SchemaValueType.Integer) .Minimum(int.MinValue) - .Maximum(int.MaxValue) - .Comment("System.Int32"); + .Maximum(int.MaxValue); public static Builder System_UInt32 => new Builder() .Type(SchemaValueType.Integer) .Minimum(uint.MinValue) - .Maximum(uint.MaxValue) - .Comment("System.UInt32"); + .Maximum(uint.MaxValue); public static Builder System_Int64 => new Builder() .Type(SchemaValueType.Integer) .Minimum(long.MinValue) - .Maximum(long.MaxValue) - .Comment("System.Int64"); + .Maximum(long.MaxValue); public static Builder System_UInt64 => new Builder() .Type(SchemaValueType.Integer) .Minimum(ulong.MinValue) - .Maximum(ulong.MaxValue) - .Comment("System.UInt64"); + .Maximum(ulong.MaxValue); public static Builder System_Single => new Builder() .Type(SchemaValueType.Number) .Minimum(decimal.MinValue) - .Maximum(decimal.MaxValue) - .Comment("System.Single"); + .Maximum(decimal.MaxValue); public static Builder System_Double => new Builder() .Type(SchemaValueType.Number) .Minimum(decimal.MinValue) - .Maximum(decimal.MaxValue) - .Comment("System.Double"); + .Maximum(decimal.MaxValue); public static Builder System_Decimal => new Builder() .Type(SchemaValueType.Number) .Minimum(decimal.MinValue) - .Maximum(decimal.MaxValue) - .Comment("System.Decimal"); + .Maximum(decimal.MaxValue); public static Builder System_Char => new Builder() .Type(SchemaValueType.String) .MinLength(1) - .MaxLength(1) - .Comment("System.Char"); + .MaxLength(1); public static Builder System_DateTime => new Builder() .Type(SchemaValueType.String) - .Format(Formats.DateTime) - .Comment("System.DateTime"); + .Format(Formats.DateTime); public static Builder UnsupportedObject(string value) => new Builder() .UnsupportedObject(value); + public static Builder UnsupportedObject(string format, params object[] args) + { + string formatted = string.Format(format, args); + using var trace = Tracer.Enter(formatted); + return new Builder() + .UnsupportedObject(formatted); + } + public static Builder DefRef(string key) => new Builder() .Ref(string.Format(DefUriFormat, key)); @@ -117,4 +113,7 @@ static CommonSchemas() public static Builder ArrayOf(Builder schema) => new Builder() .Type(SchemaValueType.Array) .Items(schema); + + public static Builder Const(JsonNode value) => new Builder() + .Const(value); } diff --git a/src/SharpSchema.Generator/Model/DeclaredTypePair.cs b/src/SharpSchema.Generator/Model/DeclaredTypePair.cs new file mode 100644 index 0000000..28b8a4f --- /dev/null +++ b/src/SharpSchema.Generator/Model/DeclaredTypePair.cs @@ -0,0 +1,6 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SharpSchema.Generator.Model; + +internal readonly record struct DeclaredTypePair(TypeDeclarationSyntax SyntaxNode, INamedTypeSymbol Symbol); diff --git a/src/SharpSchema.Generator/Model/Metadata.cs b/src/SharpSchema.Generator/Model/Metadata.cs index 0de76ce..cd3ddd5 100644 --- a/src/SharpSchema.Generator/Model/Metadata.cs +++ b/src/SharpSchema.Generator/Model/Metadata.cs @@ -14,7 +14,7 @@ namespace SharpSchema.Generator.Model; /// Examples for the schema member. /// Additional comments for the schema member. /// Indicates if the schema member is deprecated. -public record Metadata( +public record MemberMeta( string Title, string? Description, List? Examples, @@ -31,7 +31,7 @@ public override string ToString() /// /// A visitor that extracts member metadata from symbols. /// - internal class SymbolVisitor : SymbolVisitor + internal class SymbolVisitor : SymbolVisitor { private const string JsonSchemaTag = "jsonschema"; private const string TitleElement = "title"; @@ -40,37 +40,43 @@ internal class SymbolVisitor : SymbolVisitor private const string ExampleElement = "example"; private const string DeprecatedElement = "deprecated"; + public static SymbolVisitor Default { get; } = new(); + + private SymbolVisitor() + { + } + /// - /// Visits a named type symbol and extracts . + /// Visits a named type symbol and extracts . /// /// The named type symbol to visit. - /// The extracted . - public override Metadata VisitNamedType(INamedTypeSymbol symbol) => CreateMetadata(symbol); + /// The extracted . + public override MemberMeta VisitNamedType(INamedTypeSymbol symbol) => CreateMetadata(symbol); /// - /// Visits a property symbol and extracts . + /// Visits a property symbol and extracts . /// /// The property symbol to visit. - /// The extracted . - public override Metadata VisitProperty(IPropertySymbol symbol) => CreateMetadata(symbol); + /// The extracted . + public override MemberMeta VisitProperty(IPropertySymbol symbol) => CreateMetadata(symbol); /// - /// Visits a parameter symbol and extracts . + /// Visits a parameter symbol and extracts . /// /// The parameter symbol to visit. - /// The extracted . - public override Metadata VisitParameter(IParameterSymbol symbol) => CreateMetadata(symbol); + /// The extracted . + public override MemberMeta VisitParameter(IParameterSymbol symbol) => CreateMetadata(symbol); /// - /// Creates a instance from the given symbol. + /// Creates a instance from the given symbol. /// /// The symbol to extract metadata from. - /// The created instance. - private static Metadata CreateMetadata(ISymbol symbol) + /// The created instance. + private static MemberMeta CreateMetadata(ISymbol symbol) { using var trace = Tracer.Enter(symbol.Name); - string title = symbol.Name.Titleize(); + string title = symbol.Name.Replace("_", " ").Humanize(); string? description = null; List? examples = null; string? comment = null; @@ -129,7 +135,7 @@ private static Metadata CreateMetadata(ISymbol symbol) deprecated = data.GetNamedArgument(nameof(SchemaMetaAttribute.Deprecated)) || deprecated; } - return new Metadata(title, description, examples, comment, deprecated); + return new MemberMeta(title, description, examples, comment, deprecated); } } } diff --git a/src/SharpSchema.Generator/Model/ObjectAttributes.cs b/src/SharpSchema.Generator/Model/ObjectAttributes.cs new file mode 100644 index 0000000..5c8b007 --- /dev/null +++ b/src/SharpSchema.Generator/Model/ObjectAttributes.cs @@ -0,0 +1,28 @@ +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator.Model; + +internal record ObjectAttributes( + AttributeHandler AccessibilityMode, + AttributeHandler EnumMode, + AttributeHandler Meta, + AttributeHandler Override, + AttributeHandler PropertiesRange, + AttributeHandler Root, + AttributeHandler TraversalMode +); + +internal record PropertyAttributes( + AttributeHandler Const, + AttributeHandler DictionaryKeyMode, + AttributeHandler EnumMode, + AttributeHandler Ignore, + AttributeHandler ItemsRange, + AttributeHandler Format, + AttributeHandler LengthRange, + AttributeHandler Meta, + AttributeHandler Override, + AttributeHandler Regex, + AttributeHandler Required, + AttributeHandler ValueRange +); diff --git a/src/SharpSchema.Generator/Model/SharedContext.cs b/src/SharpSchema.Generator/Model/SharedContext.cs new file mode 100644 index 0000000..5f6dd1d --- /dev/null +++ b/src/SharpSchema.Generator/Model/SharedContext.cs @@ -0,0 +1,24 @@ +using Json.Schema; +using Microsoft.CodeAnalysis; +using SharpSchema.Generator.Resolvers; + +namespace SharpSchema.Generator.Model; + +internal record RootContext( + Compilation Compilation, + SemanticModelCache SemanticModelCache, + CollectionResolver CollectionResolver, + Dictionary CachedTypeSchemas, + Dictionary CachedAbstractSymbols) +{ + public RootContext( + Compilation compilation, + SemanticModelCache semanticModelCache) : this( + compilation, + semanticModelCache, + new(compilation), + new(StringComparer.OrdinalIgnoreCase), + new(StringComparer.OrdinalIgnoreCase)) + { + } +} diff --git a/src/SharpSchema.Generator/NamedTypeResolver.cs b/src/SharpSchema.Generator/NamedTypeResolver.cs new file mode 100644 index 0000000..ff6da04 --- /dev/null +++ b/src/SharpSchema.Generator/NamedTypeResolver.cs @@ -0,0 +1,206 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using SharpSchema.Generator.Utilities; +using SharpSchema.Generator.Model; +using Json.Schema; +using Humanizer; +using SharpSchema.Annotations; +using System.Text.Json.Nodes; + +namespace SharpSchema.Generator; + +using Builder = JsonSchemaBuilder; +using PropertyResult = (JsonSchemaBuilder? Builder, bool Required); + +internal class NamedTypeResolver +{ + private readonly RootContext _context; + + public NamedTypeResolver( + RootContext rootContext) + { + _context = rootContext; + } + + public Builder? Resolve(INamedTypeSymbol symbol, GeneratorOptions options) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + ObjectAttributes attributes = symbol.GetObjectAttributes(options.TraversalMode); + GeneratorOptions originalOptions = new GeneratorOptions(options); + options = options.Override(attributes); + + scope.WriteLine(options.ToString(), "Options"); + + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); + HashSet _requiredProperties = new(StringComparer.OrdinalIgnoreCase); + + if (symbol.IsRecord) + { + IMethodSymbol primaryCtor = symbol.Constructors.First(); + for (int i = 0; i < primaryCtor.Parameters.Length; i++) + { + IParameterSymbol param = primaryCtor.Parameters[i]; + if (!options.ShouldProcess(param) || !param.IsValidForGeneration()) + continue; + + string propertyName = param.Name.Camelize(); + + (Builder? typeBuilder, bool isRequired) = this.VisitParameter(param, originalOptions); + if (typeBuilder is null) + continue; + + properties.Add(propertyName, typeBuilder); + + if (isRequired) + _requiredProperties.Add(propertyName); + } + } + + foreach (IPropertySymbol prop in symbol.GetMembers().OfType()) + { + if (!options.ShouldProcess(prop) || !prop.IsValidForGeneration()) + continue; + + string propertyName = prop.Name.Camelize(); + + (Builder? valueBuilder, bool isRequired) = this.VisitProperty(prop, originalOptions); + if (valueBuilder is null) + continue; + + properties.Add(propertyName, valueBuilder); + + if (isRequired) + _requiredProperties.Add(propertyName); + } + + Builder builder = CommonSchemas.Object; + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + builder = builder.ApplyMemberMeta(meta); + + if (properties.Count > 0) + builder = builder.Properties(properties); + + if (_requiredProperties.Count > 0) + builder = builder.Required(_requiredProperties); + + if (options.TraversalMode.CheckFlag(TraversalMode.Bases)) + { + INamedTypeSymbol? @base = symbol.BaseType; + while (@base is not null && @base.SpecialType is not SpecialType.System_Object) + { + NamedTypeResolver baseSymbolVisitor = new(_context); + if (baseSymbolVisitor.Resolve(@base, options) is Builder baseBuilder) + { + builder = builder.MergeProperties(baseBuilder); + } + + @base = @base.BaseType; + } + } + + if (options.TraversalMode.CheckFlag(TraversalMode.Interfaces)) + { + symbol.AllInterfaces.ForEach(@interface => + { + NamedTypeResolver interfaceSymbolVisitor = new(_context); + if (interfaceSymbolVisitor.Resolve(@interface, options) is Builder interfaceBuilder) + { + builder = builder.MergeProperties(interfaceBuilder); + } + }); + } + + return builder; + } + + private PropertyResult VisitProperty(IPropertySymbol symbol, GeneratorOptions options) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + PropertyAttributes attributes = symbol.GetPropertyAttributes(options.TraversalMode); + options = options.Override(attributes); + + scope.WriteLine(options.ToString(), "Options"); + + bool isRequired = symbol.IsRequired || !IsNullable(symbol.NullableAnnotation); + + if (symbol.GetOverrideSchema() is Builder overrideBuilder) + return (overrideBuilder, EvaluateSchemaRequired(symbol, isRequired)); + + if (symbol.FindDeclaringSyntax() is not PropertyDeclarationSyntax pdx) + return (null, false); + + if (pdx.Accept(_context.LeafSyntaxVisitor(options)) is not Builder typeBuilder) + return (null, false); + + if (typeBuilder.Get() is not null) + { + isRequired = true; + } + else if (typeBuilder.Get() is not null) + { + isRequired = false; + } + + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); + + return (typeBuilder, EvaluateSchemaRequired(symbol, isRequired)); + } + + private PropertyResult VisitParameter(IParameterSymbol symbol, GeneratorOptions options) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + PropertyAttributes attributes = symbol.GetPropertyAttributes(options.TraversalMode); + options = options.Override(attributes); + + scope.WriteLine(options.ToString(), "Options"); + + bool isRequired = !IsNullable(symbol.NullableAnnotation); + + if (symbol.GetOverrideSchema() is Builder overrideBuilder) + return (overrideBuilder, EvaluateSchemaRequired(symbol, isRequired)); + + // Excludes implicitly-typed parameters. + if (symbol.FindDeclaringSyntax() is not ParameterSyntax px || px.Type is null) + return (null, false); + + if (px.Type.Accept(_context.LeafSyntaxVisitor(options)) is not Builder typeBuilder) + return (null, false); + + if (symbol.HasExplicitDefaultValue + && symbol.ExplicitDefaultValue is object edv + && JsonValue.Create(edv) is JsonNode defaultValue) + { + typeBuilder = typeBuilder.Default(defaultValue); + isRequired = false; + } + + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); + + return (typeBuilder, EvaluateSchemaRequired(symbol, isRequired)); + } + + private static bool IsNullable(NullableAnnotation annotation) + { + return annotation switch + { + NullableAnnotation.NotAnnotated => false, + NullableAnnotation.Annotated => true, + NullableAnnotation.None => false, // TODO: Make Configurable + _ => throw new NotSupportedException() + }; + } + + private static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) + { + AttributeHandler schemaRequired = prop.GetAttributeHandler(); + if (schemaRequired[0] is bool overrideRequired) + return overrideRequired; + + return isRequired; + } +} diff --git a/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs b/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs new file mode 100644 index 0000000..91ad436 --- /dev/null +++ b/src/SharpSchema.Generator/Resolvers/CollectionResolver.cs @@ -0,0 +1,72 @@ +using Json.Schema; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator.Resolvers; + +internal enum CollectionKind +{ + None, + Array, + Dictionary +} + +internal class CollectionResolver +{ + internal readonly record struct Result(CollectionKind Kind, SchemaValueType KeyType, ITypeSymbol ElementSymbol); + + private const string IEnumerableMetadataName = "System.Collections.Generic.IEnumerable`1"; + private const string IDictionaryMetadataName = "System.Collections.Generic.IDictionary`2"; + private const string IReadOnlyDictionaryMetadataName = "System.Collections.Generic.IReadOnlyDictionary`2"; + + private readonly INamedTypeSymbol _dictionaryOfKVSymbol; + private readonly INamedTypeSymbol _readOnlyDictionaryOfKVSymbol; + private readonly INamedTypeSymbol _enumerableOfTSymbol; + + public CollectionResolver(Compilation compilation) + { + _enumerableOfTSymbol = compilation.GetTypeByMetadataName(IEnumerableMetadataName) + ?? throw new InvalidOperationException($"Could not find symbol for '{IEnumerableMetadataName}'."); + + _readOnlyDictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IReadOnlyDictionaryMetadataName) + ?? throw new InvalidOperationException($"Could not find symbol for '{IReadOnlyDictionaryMetadataName}'."); + + _dictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IDictionaryMetadataName) ?? + throw new InvalidOperationException($"Could not find symbol for '{IDictionaryMetadataName}'."); + } + + public Result Resolve(INamedTypeSymbol boundTypeSymbol) + { + using var trace = Tracer.Enter(boundTypeSymbol.Name); + + // Try dictionary resolution + if (boundTypeSymbol.ImplementsGenericInterface(_dictionaryOfKVSymbol, _readOnlyDictionaryOfKVSymbol) + is INamedTypeSymbol boundDictionarySymbol) + { + trace.WriteLine("Dictionary type found."); + + ITypeSymbol keyTypeSymbol = boundDictionarySymbol.TypeArguments.First(); + ITypeSymbol valueTypeSymbol = boundDictionarySymbol.TypeArguments.Last(); + + return new( + CollectionKind.Dictionary, + keyTypeSymbol.GetSchemaValueType(), + valueTypeSymbol); + } + + // Try enumerable resolution + if (boundTypeSymbol.ImplementsGenericInterface(_enumerableOfTSymbol) is INamedTypeSymbol boundEnumerableSymbol) + { + trace.WriteLine("Enumerable type found."); + ITypeSymbol elementTypeSymbol = boundEnumerableSymbol.TypeArguments.First(); + return new( + CollectionKind.Array, + SchemaValueType.Null, + elementTypeSymbol); + } + + trace.WriteLine("Not a recognized collection."); + return new(CollectionKind.None, SchemaValueType.Null, null!); + } +} diff --git a/src/SharpSchema.Generator/Resolvers/EnumResolver.cs b/src/SharpSchema.Generator/Resolvers/EnumResolver.cs new file mode 100644 index 0000000..9bcbae1 --- /dev/null +++ b/src/SharpSchema.Generator/Resolvers/EnumResolver.cs @@ -0,0 +1,46 @@ +using Humanizer; +using Json.Schema; +using Microsoft.CodeAnalysis; +using SharpSchema.Annotations; +using SharpSchema.Generator.Model; +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator.Resolvers; + +using Builder = JsonSchemaBuilder; + +internal class EnumResolver +{ + public static Builder? Resolve(INamedTypeSymbol symbol, GeneratorOptions options) + { + using var trace = Tracer.Enter(symbol.Name); + + if (symbol.TypeKind != TypeKind.Enum) + return null; + + trace.WriteLine($"{options.EnumMode}"); + + if (options.EnumMode == EnumMode.String) + { + var names = symbol.GetMembers() + .OfType() + .Select(fieldSymbol => fieldSymbol.GetAttributeHandler(TraversalMode.SymbolOnly)[0] as string + ?? fieldSymbol.Name.Camelize()) + .ToList(); + + return CommonSchemas.String.Enum(names); + } + else if (options.EnumMode == EnumMode.UnderlyingType) + { + trace.WriteLine("Underlying type enum handling."); + + if (symbol.EnumUnderlyingType is INamedTypeSymbol underlyingSymbol + && underlyingSymbol.IsJsonDefinedType(NumberMode.JsonNative, out Builder? underlyingBuilder)) + { + return underlyingBuilder; + } + } + + return null; + } +} diff --git a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs b/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs deleted file mode 100644 index 0cf88b6..0000000 --- a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis; -using SharpSchema.Generator.Utilities; -using Json.Schema; - -namespace SharpSchema.Generator; - -using Builder = JsonSchemaBuilder; - -/// -/// Visits C# syntax nodes to generate JSON schema builders. -/// -public class RootDeclaredTypeSyntaxVisitor : CSharpSyntaxVisitor -{ - private readonly LeafDeclaredTypeSyntaxVisitor _cachingVisitor; - - /// - /// Initializes a new instance of the class. - /// - /// The compilation context. - /// The generator options. - public RootDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions options) - { - _cachingVisitor = new(compilation, options); - } - - /// - /// - /// Visits a syntax node. - /// - /// The syntax node to visit. - /// A JSON schema builder or null. - public override Builder? Visit(SyntaxNode? node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter($"[ROOT] {node.Kind()}"); - Builder? builder = base.Visit(node); - - Builder result = new Builder() - .Schema("http://json-schema.org/draft-07/schema#"); - - if (builder is not null) - { - result.ApplySchema(builder); - } - - return result; - } - - /// - /// - /// Visits a class declaration syntax node. - /// - /// The class declaration syntax node to visit. - /// A JSON schema builder or null. - public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return this.VisitTypeDeclaration(node); - } - - /// - /// - /// Visits a struct declaration syntax node. - /// - /// The struct declaration syntax node to visit. - /// A JSON schema builder or null. - public override Builder? VisitStructDeclaration(StructDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return this.VisitTypeDeclaration(node); - } - - /// - /// - /// Visits a record declaration syntax node. - /// - /// The record declaration syntax node to visit. - /// A JSON schema builder or null. - public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node) - { - Throw.IfNullArgument(node); - using var trace = Tracer.Enter(node.Identifier.Text); - return this.VisitTypeDeclaration(node); - } - - /// - /// Visits a type declaration syntax node. - /// - /// The type declaration syntax node to visit. - /// A JSON schema builder or null. - private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node) - { - Builder builder = node.CreateTypeSchema(_cachingVisitor); - - if (_cachingVisitor.GetCachedSchemas() is IReadOnlyDictionary defs) - { - builder.Defs(defs); - } - - return builder; - } -} diff --git a/src/SharpSchema.Generator/RootSyntaxVisitor.cs b/src/SharpSchema.Generator/RootSyntaxVisitor.cs new file mode 100644 index 0000000..c0ad316 --- /dev/null +++ b/src/SharpSchema.Generator/RootSyntaxVisitor.cs @@ -0,0 +1,166 @@ +using System.Collections.Immutable; +using Json.Schema; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Generator.Model; +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator; + +using Builder = JsonSchemaBuilder; + +/// +/// Visits C# syntax nodes to generate JSON schema builders. +/// +public class RootSyntaxVisitor : CSharpSyntaxVisitor +{ + private readonly LeafSyntaxVisitor _leafSyntaxVisitor; + private readonly RootContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The compilation context. + /// The generator options. + public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) + { + Throw.IfNullArgument(compilation); + Throw.IfNullArgument(options); + + _context = new RootContext(compilation, new SemanticModelCache(compilation)); + _leafSyntaxVisitor = new(_context, options); + } + + /// + /// + /// Visits a syntax node. + /// + /// The syntax node to visit. + /// A JSON schema builder or null. + public override Builder? Visit(SyntaxNode? node) + { + Throw.IfNullArgument(node); + using var trace = Tracer.Enter($"[ROOT] {node.Kind()}"); + Builder? builder = base.Visit(node); + + Builder result = new Builder() + .Schema("http://json-schema.org/draft-07/schema#"); + + if (builder is not null) + { + result.ApplySchema(builder); + } + + return result; + } + + /// + /// + /// Visits a class declaration syntax node. + /// + /// The class declaration syntax node to visit. + /// A JSON schema builder or null. + public override Builder? VisitClassDeclaration(ClassDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using var trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node); + } + + /// + /// + /// Visits a struct declaration syntax node. + /// + /// The struct declaration syntax node to visit. + /// A JSON schema builder or null. + public override Builder? VisitStructDeclaration(StructDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using var trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node); + } + + /// + /// + /// Visits a record declaration syntax node. + /// + /// The record declaration syntax node to visit. + /// A JSON schema builder or null. + public override Builder? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + Throw.IfNullArgument(node); + using var trace = Tracer.Enter(node.Identifier.Text); + return this.VisitTypeDeclaration(node); + } + + /// + /// Visits a type declaration syntax node. + /// + /// The type declaration syntax node to visit. + /// A JSON schema builder or null. + private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node) + { + Builder builder = _leafSyntaxVisitor.CreateTypeSchema(node); + + Dictionary cachedAbstractSymbols = _context.CachedAbstractSymbols; + Dictionary cachedTypeSchemas = _context.CachedTypeSchemas; + + if (cachedAbstractSymbols.Count > 0) + { + using var trace = Tracer.Enter("Building abstract type schemas."); + ImmutableArray namedTypes = [.. _context.Compilation.GetAllNamedTypes(_context.SemanticModelCache)]; + + foreach ((string key, INamedTypeSymbol abstractSymbol) in cachedAbstractSymbols) + { + trace.WriteLine($"Building schema for abstract type '{abstractSymbol.Name}'."); + + IEnumerable subTypes = namedTypes + .Where(t => t.Symbol.InheritsFrom(abstractSymbol)); + + List subSchemas = []; + + foreach (DeclaredTypePair subType in subTypes) + { + // Check if the sub-type is already cached + string cacheKey = subType.Symbol.GetDefCacheKey(); + if (cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + { + trace.WriteLine($"Using cached schema for '{subType.Symbol.Name}'."); + subSchemas.Add(CommonSchemas.DefRef(cacheKey)); + } + else + { + trace.WriteLine($"Building schema for '{subType.Symbol.Name}'."); + Builder? subSchema = _leafSyntaxVisitor.CreateTypeSchema(subType.Symbol, subType.SyntaxNode); + if (subSchema is not null) + { + cachedTypeSchemas[cacheKey] = subSchema; + subSchemas.Add(CommonSchemas.DefRef(cacheKey)); + } + } + } + + if (subSchemas.Count > 0) + { + trace.WriteLine($"Found {subSchemas.Count} sub-types for '{abstractSymbol.Name}'."); + Builder abstractSchema = CommonSchemas.Object.OneOf(subSchemas); + cachedTypeSchemas[key] = abstractSchema; + } + else + { + trace.WriteLine($"No sub-types found for '{abstractSymbol.Name}'."); + } + } + } + + if (cachedTypeSchemas.Count > 0) + { + builder.Defs(cachedTypeSchemas.ToImmutableSortedDictionary( + p => p.Key, + p => p.Value.Build())); + } + + return builder; + } +} diff --git a/src/SharpSchema.Generator/SharpSchema.Generator.csproj b/src/SharpSchema.Generator/SharpSchema.Generator.csproj index cc4699a..a5c9ace 100644 --- a/src/SharpSchema.Generator/SharpSchema.Generator.csproj +++ b/src/SharpSchema.Generator/SharpSchema.Generator.csproj @@ -21,10 +21,6 @@ - - - - diff --git a/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs b/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs index 155ff07..d11d9a8 100644 --- a/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.CodeAnalysis; using SharpSchema.Annotations; using SharpSchema.Generator.Model; -using System.Text.Json.Serialization; namespace SharpSchema.Generator.Utilities; @@ -17,26 +16,12 @@ internal static class AnnotationExtensions /// True if the symbol is ignored for generation; otherwise, false. public static bool IsIgnoredForGeneration(this ISymbol symbol) { - if (symbol.GetAttributeData() is not null) return true; - - if (symbol.GetAttributeData() is AttributeData jsonIgnoreAttribute) - { - // Filter out properties that have JsonIgnoreAttribute without any named arguments - if (jsonIgnoreAttribute.NamedArguments.Length == 0) - return true; - - // Filter out properties that have JsonIgnoreAttribute with named argument "Condition" and value "Always" - if (jsonIgnoreAttribute.GetNamedArgument("Condition") == JsonIgnoreCondition.Always) - return true; - } - - return false; + return symbol.GetAttributeData() is not null; } public static Builder? GetOverrideSchema(this ISymbol symbol) { - if (symbol.GetAttributeData() is AttributeData data - && data.GetConstructorArgument(0) is string schemaString) + if (symbol.GetAttributeHandler().Get(0) is string schemaString) { try { diff --git a/src/SharpSchema.Generator/Utilities/AttributeDataExtensions.cs b/src/SharpSchema.Generator/Utilities/AttributeDataExtensions.cs index 4714fd6..6610342 100644 --- a/src/SharpSchema.Generator/Utilities/AttributeDataExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/AttributeDataExtensions.cs @@ -65,29 +65,4 @@ public static List GetNamedArgumentArray( _ => default }; } - - public static List GetConstructorArgumentArray(this AttributeData attributeData, int argumentIndex) - where T : notnull - { - if (argumentIndex >= attributeData.ConstructorArguments.Length) - { - return []; - } - - TypedConstant constructorArgument = attributeData.ConstructorArguments[argumentIndex]; - - List result = []; - if (constructorArgument.Kind == TypedConstantKind.Array) - { - foreach (TypedConstant value in constructorArgument.Values) - { - if (value.Value is T item) - { - result.Add(item); - } - } - } - - return result; - } } diff --git a/src/SharpSchema.Generator/Utilities/AttributeHandler.cs b/src/SharpSchema.Generator/Utilities/AttributeHandler.cs index f4b15d6..b11cf2c 100644 --- a/src/SharpSchema.Generator/Utilities/AttributeHandler.cs +++ b/src/SharpSchema.Generator/Utilities/AttributeHandler.cs @@ -7,6 +7,8 @@ namespace SharpSchema.Generator.Utilities; [ExcludeFromCodeCoverage] internal readonly struct AttributeHandler(AttributeData? data) { + public bool Present => data is not null; + public object? this[int index] { get @@ -45,6 +47,69 @@ public object? this[string name] } } + public T? Get(int index) + where T : struct + { + if (data is null || index >= data.ConstructorArguments.Length) + { + return default; + } + + TypedConstant argument = data.ConstructorArguments[index]; + return argument.Kind switch + { + TypedConstantKind.Primitive => argument.Value is T value ? value : default, + TypedConstantKind.Enum => (T)argument.Value!, + _ => default + }; + } + + public string? Get(int index) + { + if (data is null || index >= data.ConstructorArguments.Length) + { + return default; + } + + TypedConstant argument = data.ConstructorArguments[index]; + return argument.Kind switch + { + TypedConstantKind.Primitive => argument.Value is string value ? value : default, + _ => default + }; + } + + public T? Get(string name) + where T : struct + { + if (data is null) + { + return default; + } + TypedConstant argument = data.NamedArguments.FirstOrDefault(a => a.Key == name).Value; + return argument.Kind switch + { + TypedConstantKind.Primitive => argument.Value is T value ? value : default, + TypedConstantKind.Enum => (T)argument.Value!, + _ => default + }; + } + + public string? Get(string name) + { + if (data is null) + { + return default; + } + + TypedConstant argument = data.NamedArguments.FirstOrDefault(a => a.Key == name).Value; + return argument.Kind switch + { + TypedConstantKind.Primitive => argument.Value is string value ? value : default, + _ => default + }; + } + public ImmutableArray? GetArray(int index) where T : notnull { diff --git a/src/SharpSchema.Generator/Utilities/CollectionExtensions.cs b/src/SharpSchema.Generator/Utilities/CollectionExtensions.cs new file mode 100644 index 0000000..145b223 --- /dev/null +++ b/src/SharpSchema.Generator/Utilities/CollectionExtensions.cs @@ -0,0 +1,16 @@ +namespace SharpSchema.Generator.Utilities; + +internal static class CollectionExtensions +{ + public static void ForEach(this IEnumerable source, Action action) + { + foreach (var item in source) + action(item); + } + + public static void Deconstruct(this KeyValuePairpair, out TKey key, out TValue value) + { + key = pair.Key; + value = pair.Value; + } +} diff --git a/src/SharpSchema.Generator/Utilities/CollectionSymbolVisitor.cs b/src/SharpSchema.Generator/Utilities/CollectionSymbolVisitor.cs deleted file mode 100644 index e91d267..0000000 --- a/src/SharpSchema.Generator/Utilities/CollectionSymbolVisitor.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Json.Schema; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using SharpSchema.Generator.Model; - -namespace SharpSchema.Generator.Utilities; - -using Builder = JsonSchemaBuilder; - -internal class CollectionSymbolVisitor : SymbolVisitor -{ - private const string IEnumerableMetadataName = "System.Collections.Generic.IEnumerable`1"; - private const string IDictionaryMetadataName = "System.Collections.Generic.IDictionary`2"; - private const string IReadOnlyDictionaryMetadataName = "System.Collections.Generic.IReadOnlyDictionary`2"; - - private readonly GeneratorOptions _options; - private readonly CSharpSyntaxVisitor _visitor; - private readonly INamedTypeSymbol _dictionaryOfKVSymbol; - private readonly INamedTypeSymbol _readOnlyDictionaryOfKVSymbol; - private readonly INamedTypeSymbol _enumerableOfTSymbol; - - public CollectionSymbolVisitor( - GeneratorOptions options, - Compilation compilation, - CSharpSyntaxVisitor declaredTypeSyntaxVisitor) - { - _enumerableOfTSymbol = compilation.GetTypeByMetadataName(IEnumerableMetadataName) - ?? throw new InvalidOperationException($"Could not find symbol for '{IEnumerableMetadataName}'."); - _readOnlyDictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IReadOnlyDictionaryMetadataName) - ?? throw new InvalidOperationException($"Could not find symbol for '{IReadOnlyDictionaryMetadataName}'."); - _dictionaryOfKVSymbol = compilation.GetTypeByMetadataName(IDictionaryMetadataName) ?? - throw new InvalidOperationException($"Could not find symbol for '{IDictionaryMetadataName}'."); - _options = options; - _visitor = declaredTypeSyntaxVisitor; - } - - public override Builder? VisitNamedType(INamedTypeSymbol symbol) - { - return ResolveCollectionType(symbol); - } - - private Builder? ResolveCollectionType(INamedTypeSymbol boundTypeSymbol) - { - using var trace = Tracer.Enter(boundTypeSymbol.Name); - - // Try dictionary resolution - if (boundTypeSymbol.ImplementsGenericInterface(_dictionaryOfKVSymbol, _readOnlyDictionaryOfKVSymbol) - is INamedTypeSymbol boundDictionarySymbol) - { - trace.WriteLine("Dictionary type found."); - Builder builder = CommonSchemas.Object; - - ITypeSymbol keyTypeSymbol = boundDictionarySymbol.TypeArguments.First(); - if (keyTypeSymbol.GetSchemaValueType() != SchemaValueType.String) - { - trace.WriteLine($"Key type is not string. Using DictionaryKeyMode: {_options.DictionaryKeyMode}"); - switch (_options.DictionaryKeyMode) - { - case DictionaryKeyMode.Loose: - builder.Comment($"Ensure key type '{keyTypeSymbol.Name}' is convertible to string."); - break; - case DictionaryKeyMode.Strict: - return CommonSchemas.UnsupportedObject($"Key type '{keyTypeSymbol.Name}' must be string."); - case DictionaryKeyMode.Skip: - return new Builder(); - } - } - - INamedTypeSymbol valueTypeSymbol = (INamedTypeSymbol)boundDictionarySymbol.TypeArguments.Last(); - if (!valueTypeSymbol.IsJsonDefinedType(out Builder? valueSchema)) - { - valueSchema = _visitor.Visit(valueTypeSymbol.FindTypeDeclaration()); - if (valueSchema is null) - return CommonSchemas.UnsupportedObject($"Could not find schema for value type '{valueTypeSymbol.Name}'."); - } - - return builder.AdditionalProperties(valueSchema); - } - - // Try enumerable resolution - if (boundTypeSymbol.ImplementsGenericInterface(_enumerableOfTSymbol) - is INamedTypeSymbol boundEnumerableSymbol) - { - trace.WriteLine("Enumerable type found."); - INamedTypeSymbol elementTypeSymbol = (INamedTypeSymbol)boundEnumerableSymbol.TypeArguments.First(); - if (!elementTypeSymbol.IsJsonDefinedType(out Builder? elementSchema)) - { - elementSchema = _visitor.Visit(elementTypeSymbol.FindTypeDeclaration()); - if (elementSchema is null) - return CommonSchemas.UnsupportedObject($"Could not find schema for element type '{elementTypeSymbol.Name}'."); - } - - return CommonSchemas.ArrayOf(elementSchema); - } - - return null; - } -} diff --git a/src/SharpSchema.Generator/Utilities/CommonSchemas.cs b/src/SharpSchema.Generator/Utilities/CommonSchemas.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs new file mode 100644 index 0000000..e9a3b36 --- /dev/null +++ b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Generator.Model; + +namespace SharpSchema.Generator.Utilities; + +internal static class CompilationExtensions +{ + internal static IEnumerable GetAllNamedTypes(this Compilation compilation, SemanticModelCache semanticModelCache) + { + foreach (SyntaxTree tree in compilation.SyntaxTrees) + { + SemanticModel semanticModel = semanticModelCache.GetSemanticModel(tree); + foreach (TypeDeclarationSyntax type in tree + .GetRoot() + .DescendantNodes() + .OfType()) + { + ISymbol? symbol = type.GetDeclaredSymbol(semanticModel); + if (symbol is INamedTypeSymbol namedTypeSymbol) + { + yield return new(type, namedTypeSymbol); + } + } + } + } +} diff --git a/src/SharpSchema.Generator/Utilities/EnumExtensions.cs b/src/SharpSchema.Generator/Utilities/EnumExtensions.cs index 3feb20e..433e201 100644 --- a/src/SharpSchema.Generator/Utilities/EnumExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/EnumExtensions.cs @@ -1,11 +1,12 @@ using System.Runtime.CompilerServices; +using SharpSchema.Annotations; namespace SharpSchema.Generator.Utilities; internal static class EnumExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CheckFlag(this Accessibilities value, Accessibilities flag) => (value & flag) == flag; + public static bool CheckFlag(this AccessibilityMode value, AccessibilityMode flag) => (value & flag) == flag; - public static bool CheckFlag(this Traversal value, Traversal flag) => (value & flag) == flag; + public static bool CheckFlag(this TraversalMode value, TraversalMode flag) => (value & flag) == flag; } diff --git a/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs deleted file mode 100644 index a518e0d..0000000 --- a/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Humanizer; -using Json.Schema; -using Microsoft.CodeAnalysis; -using SharpSchema.Annotations; -using SharpSchema.Generator.Model; - -namespace SharpSchema.Generator.Utilities; - -using Builder = JsonSchemaBuilder; - -internal class EnumSymbolVisitor : SymbolVisitor -{ - public static EnumSymbolVisitor Instance { get; } = new EnumSymbolVisitor(); - - private EnumSymbolVisitor() { } - - protected override Builder? DefaultResult => null; - - public override Builder? VisitNamedType(INamedTypeSymbol symbol, GeneratorOptions options) - { - using var trace = Tracer.Enter(symbol.Name); - - if (symbol.TypeKind != TypeKind.Enum) - return DefaultResult; - - trace.WriteLine($"{options.EnumHandling}"); - - if (options.EnumHandling == EnumHandling.String) - { - var names = symbol.GetMembers() - .OfType() - .Select(fieldSymbol => (fieldSymbol.GetAttributeHandler()[0] as string) - ?? fieldSymbol.Name.Camelize()) - .ToList(); - - return CommonSchemas.String.Enum(names); - } - else if (options.EnumHandling == EnumHandling.UnderlyingType) - { - trace.WriteLine("Underlying type enum handling."); - - if (symbol.EnumUnderlyingType is INamedTypeSymbol underlyingSymbol - && underlyingSymbol.IsJsonDefinedType(out Builder? underlyingBuilder)) - { - return underlyingBuilder; - } - } - - return DefaultResult; - } -} diff --git a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs index 806ede5..add70be 100644 --- a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis; +using SharpSchema.Annotations; +using SharpSchema.Generator.Model; namespace SharpSchema.Generator.Utilities; @@ -17,18 +19,38 @@ bool ShouldProcessParameter(IParameterSymbol symbol) => symbol.IsValidForGenerat && !symbol.IsIgnoredForGeneration(); bool ShouldProcessProperty(IPropertySymbol symbol) => symbol.IsValidForGeneration() - && ShouldProcessAccessibility(symbol.DeclaredAccessibility, options.Accessibilities) + && ShouldProcessAccessibility(symbol.DeclaredAccessibility, options.AccessibilityMode) && !symbol.IsIgnoredForGeneration(); - static bool ShouldProcessAccessibility(Accessibility accessibility, Accessibilities allowedAccessibilities) + static bool ShouldProcessAccessibility(Accessibility accessibility, AccessibilityMode allowedAccessibilities) { return accessibility switch { - Accessibility.Public => allowedAccessibilities.CheckFlag(Accessibilities.Public), - Accessibility.Internal => allowedAccessibilities.CheckFlag(Accessibilities.Internal), - Accessibility.Private => allowedAccessibilities.CheckFlag(Accessibilities.Private), + Accessibility.Public => allowedAccessibilities.CheckFlag(AccessibilityMode.Public), + Accessibility.Internal => allowedAccessibilities.CheckFlag(AccessibilityMode.Internal), + Accessibility.Private => allowedAccessibilities.CheckFlag(AccessibilityMode.Private), _ => false, }; } } + + public static GeneratorOptions Override(this GeneratorOptions options, ObjectAttributes attributes) + { + return new GeneratorOptions( + AccessibilityMode: attributes.AccessibilityMode.Get(0) ?? options.AccessibilityMode, + TraversalMode: attributes.TraversalMode.Get(0) ?? (options.TraversalMode), + DictionaryKeyMode: options.DictionaryKeyMode, + EnumMode: attributes.EnumMode.Get(0) ?? options.EnumMode, + NumberMode: options.NumberMode); + } + + public static GeneratorOptions Override(this GeneratorOptions options, PropertyAttributes attributes) + { + return new GeneratorOptions( + AccessibilityMode: options.AccessibilityMode, + TraversalMode: options.TraversalMode, + DictionaryKeyMode: attributes.DictionaryKeyMode.Get(0) ?? options.DictionaryKeyMode, + EnumMode: attributes.EnumMode.Get(0) ?? options.EnumMode, + NumberMode: options.NumberMode); + } } diff --git a/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs b/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs index dffe249..4112db2 100644 --- a/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs @@ -6,7 +6,7 @@ namespace SharpSchema.Generator.Utilities; internal static class JsonSchemaBuilderExtensions { - public static JsonSchemaBuilder ApplyMetadata(this JsonSchemaBuilder builder, Metadata? data) + public static JsonSchemaBuilder ApplyMemberMeta(this JsonSchemaBuilder builder, MemberMeta? data) { using var scope = Tracer.Enter($"{data}"); @@ -51,33 +51,35 @@ public static JsonSchemaBuilder ApplySchema(this JsonSchema @base, JsonSchema ap return builder; } - public static JsonSchemaBuilder MergeBaseProperties(this JsonSchemaBuilder builder, JsonSchema baseSchema) + public static JsonSchemaBuilder MergeProperties(this JsonSchemaBuilder @base, JsonSchema apply) { - using var trace = Tracer.Enter($"{baseSchema.BaseUri}"); + using var trace = Tracer.Enter($"{apply.BaseUri}"); + + IReadOnlyDictionary? baseProperties = @base.Get()?.Properties; + IReadOnlyDictionary? applyProperties = apply.GetProperties(); - IReadOnlyDictionary baseProperties = builder.Get()?.Properties ?? new Dictionary(); - IReadOnlyDictionary otherProperties = baseSchema.GetProperties() ?? new Dictionary(); + if (applyProperties is null) + return @base; - Dictionary mergedProperties = new((IDictionary)otherProperties, StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair pair in baseProperties) + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); + if (baseProperties is not null) { - if (mergedProperties.TryGetValue(pair.Key, out JsonSchema? value)) - mergedProperties[pair.Key] = pair.Value.ApplySchema(value); - else - mergedProperties.Add(pair.Key, pair.Value); + foreach ((string name, JsonSchema value) in baseProperties) + properties.Add(name, value); } - // Merge required properties - IReadOnlyList baseRequiredProperties = builder.Get()?.Properties ?? []; - IReadOnlyList otherRequiredProperties = baseSchema.GetRequired() ?? []; - - HashSet mergedRequiredProperties = new(baseRequiredProperties, StringComparer.OrdinalIgnoreCase); - foreach (string requiredProperty in otherRequiredProperties) - mergedRequiredProperties.Add(requiredProperty); + if (apply.GetProperties() is IReadOnlyDictionary props) + { + foreach ((string name, JsonSchema applyValue) in props) + { + if (properties.TryGetValue(name, out JsonSchema? baseValue)) + properties[name] = baseValue.ApplySchema(applyValue); + else + properties.Add(name, applyValue); + } + } - return builder - .Properties(mergedProperties) - .Required(mergedRequiredProperties); + return properties.Count > 0 ? @base.Properties(properties) : @base; } public static JsonSchemaBuilder UnsupportedObject(this JsonSchemaBuilder builder, string value) @@ -85,9 +87,4 @@ public static JsonSchemaBuilder UnsupportedObject(this JsonSchemaBuilder builder builder.Add(new UnsupportedObjectKeyword(value)); return builder; } - - public static UnsupportedObjectKeyword? GetUnsupportedObject(this JsonSchemaBuilder builder) - { - return builder.Get(); - } } diff --git a/src/SharpSchema.Generator/Utilities/RootContextExtensions.cs b/src/SharpSchema.Generator/Utilities/RootContextExtensions.cs new file mode 100644 index 0000000..aadae78 --- /dev/null +++ b/src/SharpSchema.Generator/Utilities/RootContextExtensions.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using SharpSchema.Generator.Model; + +namespace SharpSchema.Generator.Utilities; + +internal static class RootContextExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LeafSyntaxVisitor LeafSyntaxVisitor(this RootContext rootContext, GeneratorOptions options) + { + return new LeafSyntaxVisitor(rootContext, options); + } +} diff --git a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs index 1624be4..3c6f659 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -2,6 +2,7 @@ using Json.Schema; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Annotations; using SharpSchema.Generator.Model; namespace SharpSchema.Generator.Utilities; @@ -11,12 +12,59 @@ namespace SharpSchema.Generator.Utilities; /// internal static class SymbolExtensions { - public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Traversal traversal = Traversal.SymbolOnly) + public static AttributeHandler GetAttributeHandler(this ISymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) where T : Attribute { return new AttributeHandler(GetAttributeData(symbol, traversal)); } + public static AttributeHandler GetAttributeHandler(this IPropertySymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute + { + return new AttributeHandler(GetAttributeData(symbol, traversal)); + } + + public static AttributeHandler GetAttributeHandler(this IFieldSymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute + { + return new AttributeHandler(GetAttributeData(symbol, traversal)); + } + + public static AttributeHandler GetAttributeHandler(this IParameterSymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute + { + return new AttributeHandler(GetAttributeData(symbol, traversal)); + } + + public static ObjectAttributes GetObjectAttributes(this ISymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + { + return new ObjectAttributes( + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal)); + } + + public static PropertyAttributes GetPropertyAttributes(this ISymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + { + return new PropertyAttributes( + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal), + GetAttributeHandler(symbol, traversal)); + } + /// /// Gets the attribute data of the specified type from the symbol. /// @@ -24,9 +72,11 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave /// The symbol. /// Indicates whether to search for attributes on base classes and interfaces. /// The attribute data if found; otherwise, null. - public static AttributeData? GetAttributeData(this ISymbol symbol, Traversal traversal = Traversal.SymbolOnly) + public static AttributeData? GetAttributeData(this ISymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) where T : Attribute { + AttributeData? mostDerivedAttribute = null; + // Search for the attribute on the symbol itself foreach (AttributeData attribute in symbol.GetAttributes()) { @@ -36,7 +86,7 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave if (symbol is INamedTypeSymbol namedTypeSymbol) { - if (traversal.CheckFlag(Traversal.Bases)) + if (traversal.CheckFlag(TraversalMode.Bases)) { // Search on base classes INamedTypeSymbol? baseType = namedTypeSymbol.BaseType; @@ -45,14 +95,17 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave foreach (AttributeData attribute in baseType.GetAttributes()) { if (attribute.AttributeClass?.MatchesType() ?? false) - return attribute; + { + mostDerivedAttribute = attribute; + break; + } } baseType = baseType.BaseType; } } - if (traversal.CheckFlag(Traversal.Interfaces)) + if (traversal.CheckFlag(TraversalMode.Interfaces)) { // Search on interfaces foreach (INamedTypeSymbol interfaceType in namedTypeSymbol.AllInterfaces) @@ -60,24 +113,168 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave foreach (AttributeData attribute in interfaceType.GetAttributes()) { if (attribute.AttributeClass?.MatchesType() ?? false) - return attribute; + { + mostDerivedAttribute = attribute; + break; + } } } } } - return null; + return mostDerivedAttribute; } - public static TValue? GetAttributeConstructorArgument(this ISymbol symbol, int argumentIndex, Traversal traversal = Traversal.SymbolOnly) - where TAttribute : Attribute - where TValue : notnull + public static AttributeData? GetAttributeData(this IPropertySymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute { - AttributeData? attributeData = symbol.GetAttributeData(traversal); - if (attributeData is null) - return default; + AttributeData? mostDerivedAttribute = null; + + // Search for the attribute on the symbol itself + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + return attribute; + } - return attributeData.GetConstructorArgument(argumentIndex); + if (symbol.ContainingType is INamedTypeSymbol containingType) + { + if (traversal.CheckFlag(TraversalMode.Bases)) + { + // Search on base classes + INamedTypeSymbol? baseType = containingType.BaseType; + while (baseType is not null) + { + var baseProperty = baseType.GetMembers(symbol.Name).OfType().FirstOrDefault(); + if (baseProperty is not null) + { + foreach (AttributeData attribute in baseProperty.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + { + mostDerivedAttribute = attribute; + break; + } + } + } + + baseType = baseType.BaseType; + } + } + + if (traversal.CheckFlag(TraversalMode.Interfaces)) + { + // Search on interfaces + foreach (INamedTypeSymbol interfaceType in containingType.AllInterfaces) + { + var interfaceProperty = interfaceType.GetMembers(symbol.Name).OfType().FirstOrDefault(); + if (interfaceProperty is not null) + { + foreach (AttributeData attribute in interfaceProperty.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + { + mostDerivedAttribute = attribute; + break; + } + } + } + } + } + } + + return mostDerivedAttribute; + } + + public static AttributeData? GetAttributeData(this IParameterSymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute + { + AttributeData? mostDerivedAttribute = null; + + // Search for the attribute on the symbol itself + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + return attribute; + } + + if (symbol.ContainingSymbol is IMethodSymbol methodSymbol && + methodSymbol.MethodKind == MethodKind.Constructor && + methodSymbol.ContainingType.IsRecord) + { + if (methodSymbol.ContainingType is INamedTypeSymbol containingType) + { + if (traversal.CheckFlag(TraversalMode.Bases)) + { + // Search on base records + INamedTypeSymbol? baseType = containingType.BaseType; + while (baseType is not null) + { + var baseConstructor = baseType.InstanceConstructors.FirstOrDefault(); + if (baseConstructor is not null) + { + var baseParameter = baseConstructor.Parameters.FirstOrDefault(p => p.Name == symbol.Name); + if (baseParameter is not null) + { + foreach (AttributeData attribute in baseParameter.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + { + mostDerivedAttribute = attribute; + break; + } + } + } + } + + baseType = baseType.BaseType; + } + } + } + } + + return mostDerivedAttribute; + } + + public static AttributeData? GetAttributeData(this IFieldSymbol symbol, TraversalMode traversal = TraversalMode.SymbolOnly) + where T : Attribute + { + AttributeData? mostDerivedAttribute = null; + + // Search for the attribute on the symbol itself + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + return attribute; + } + + if (symbol.ContainingType is INamedTypeSymbol containingType) + { + if (traversal.CheckFlag(TraversalMode.Bases)) + { + // Search on base classes + INamedTypeSymbol? baseType = containingType.BaseType; + while (baseType is not null) + { + var baseField = baseType.GetMembers(symbol.Name).OfType().FirstOrDefault(); + if (baseField is not null) + { + foreach (AttributeData attribute in baseField.GetAttributes()) + { + if (attribute.AttributeClass?.MatchesType() ?? false) + { + mostDerivedAttribute = attribute; + break; + } + } + } + + baseType = baseType.BaseType; + } + } + } + + return mostDerivedAttribute; } public static bool MatchesType(this INamedTypeSymbol typeSymbol) @@ -106,6 +303,14 @@ public static bool MatchesType(this INamedTypeSymbol typeSymbol) return normalizedSymbolName.SequenceEqual(runtimeTypeName.AsSpan()); } + public static bool HasUnresolvedTypeArguments(this INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.IsGenericType) + return namedTypeSymbol.TypeArguments.Any(arg => arg.TypeKind == TypeKind.TypeParameter); + + return false; + } + /// /// Determines if the symbol is valid for generation. /// @@ -114,7 +319,6 @@ public static bool MatchesType(this INamedTypeSymbol typeSymbol) public static bool IsValidForGeneration(this ISymbol symbol) { return !symbol.IsStatic - && !symbol.IsVirtual && !symbol.IsImplicitlyDeclared && symbol switch { @@ -125,12 +329,11 @@ public static bool IsValidForGeneration(this ISymbol symbol) static bool IsValidNamedTypeSymbol(INamedTypeSymbol symbol) { - return !symbol.IsStatic + return !symbol.IsVirtual && !symbol.IsAnonymousType && !symbol.IsComImport && !symbol.IsImplicitClass - && !symbol.IsExtern - && !symbol.IsImplicitlyDeclared; + && !symbol.IsExtern; } static bool IsValidPropertySymbol(IPropertySymbol symbol) @@ -141,36 +344,31 @@ static bool IsValidPropertySymbol(IPropertySymbol symbol) } /// - /// Finds the type declaration syntax for the symbol. + /// Finds the declaring syntax node of the specified type for the given symbol. /// - /// The symbol. - /// The type declaration syntax if found; otherwise, null. - public static TypeDeclarationSyntax? FindTypeDeclaration(this INamedTypeSymbol symbol) + /// The type of the syntax node to find. + /// The symbol whose declaring syntax node is to be found. + /// The declaring syntax node of the specified type if found; otherwise, null. + public static T? FindDeclaringSyntax(this ISymbol symbol) + where T : SyntaxNode { foreach (SyntaxReference reference in symbol.DeclaringSyntaxReferences) { SyntaxNode syntax = reference.GetSyntax(); - if (syntax is TypeDeclarationSyntax typeDeclarationSyntax) + if (syntax is T node) { - return typeDeclarationSyntax; + return node; } } return null; } - public static BaseTypeDeclarationSyntax? FindBaseTypeDeclaration(this ITypeSymbol symbol) + public static SyntaxNode? FindDeclaringSyntax(this ISymbol symbol) { - foreach (SyntaxReference reference in symbol.DeclaringSyntaxReferences) - { - SyntaxNode syntax = reference.GetSyntax(); - if (syntax is BaseTypeDeclarationSyntax baseTypeDeclarationSyntax) - { - return baseTypeDeclarationSyntax; - } - } - - return null; + return symbol.DeclaringSyntaxReferences + .FirstOrDefault()? + .GetSyntax(); } /// @@ -196,22 +394,38 @@ static bool IsValidPropertySymbol(IPropertySymbol symbol) public static bool InheritsFrom(this INamedTypeSymbol symbol, INamedTypeSymbol baseType) { - INamedTypeSymbol? current = symbol.BaseType; - while (current is not null) + // Traverse base types + for (INamedTypeSymbol? current = symbol.BaseType; current is not null; current = current.BaseType) { - if (SymbolEqualityComparer.Default.Equals(current, baseType)) + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, baseType.OriginalDefinition)) + { return true; - current = current.BaseType; + } } + + // Check interfaces as well + if (baseType.TypeKind == TypeKind.Interface) + { + foreach (var iface in symbol.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, baseType.OriginalDefinition)) + { + return true; + } + } + } + return false; } public static string GetDefCacheKey(this ITypeSymbol symbol) => symbol.GetDocumentationCommentId() ?? symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - public static bool IsJsonDefinedType(this ITypeSymbol symbol, [NotNullWhen(true)] out JsonSchemaBuilder? schema) + public static bool IsJsonDefinedType(this ITypeSymbol symbol, NumberMode numberMode, [NotNullWhen(true)] out JsonSchemaBuilder? schema) { + using var trace = Tracer.Enter(symbol.Name); if (symbol.SpecialType == SpecialType.None) { + trace.WriteLine("Symbol is not a special type."); schema = null; return false; } @@ -219,21 +433,21 @@ public static bool IsJsonDefinedType(this ITypeSymbol symbol, [NotNullWhen(true) schema = symbol.SpecialType switch { SpecialType.System_Boolean => CommonSchemas.Boolean, - SpecialType.System_Byte => CommonSchemas.System_Byte, + SpecialType.System_Byte => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_Byte, SpecialType.System_Char => CommonSchemas.System_Char, SpecialType.System_DateTime => CommonSchemas.System_DateTime, - SpecialType.System_Decimal => CommonSchemas.System_Decimal, - SpecialType.System_Double => CommonSchemas.System_Double, - SpecialType.System_Int16 => CommonSchemas.System_Int16, - SpecialType.System_Int32 => CommonSchemas.System_Int32, - SpecialType.System_Int64 => CommonSchemas.System_Int64, + SpecialType.System_Decimal => numberMode is NumberMode.JsonNative ? CommonSchemas.Number : CommonSchemas.System_Decimal, + SpecialType.System_Double => numberMode is NumberMode.JsonNative ? CommonSchemas.Number : CommonSchemas.System_Double, + SpecialType.System_Int16 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_Int16, + SpecialType.System_Int32 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_Int32, + SpecialType.System_Int64 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_Int64, SpecialType.System_Object => throw new InvalidOperationException("System.Object does not map to a json defined type."), - SpecialType.System_SByte => CommonSchemas.System_SByte, - SpecialType.System_Single => CommonSchemas.System_Single, + SpecialType.System_SByte => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_SByte, + SpecialType.System_Single => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_Single, SpecialType.System_String => CommonSchemas.String, - SpecialType.System_UInt16 => CommonSchemas.System_UInt16, - SpecialType.System_UInt32 => CommonSchemas.System_UInt32, - SpecialType.System_UInt64 => CommonSchemas.System_UInt64, + SpecialType.System_UInt16 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_UInt16, + SpecialType.System_UInt32 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_UInt32, + SpecialType.System_UInt64 => numberMode is NumberMode.JsonNative ? CommonSchemas.Integer : CommonSchemas.System_UInt64, _ => null }; diff --git a/src/SharpSchema.Generator/Utilities/SyntaxExtensions.cs b/src/SharpSchema.Generator/Utilities/SyntaxExtensions.cs index ca6c5bf..e903a21 100644 --- a/src/SharpSchema.Generator/Utilities/SyntaxExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SyntaxExtensions.cs @@ -1,16 +1,11 @@ using System.Runtime.CompilerServices; -using Humanizer; -using Json.Schema; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using SharpSchema.Generator.Model; -using SharpSchema.Annotations; namespace SharpSchema.Generator.Utilities; -using Builder = JsonSchemaBuilder; - internal static class SyntaxExtensions { public static bool IsNestedInSystemNamespace(this TypeDeclarationSyntax node) @@ -23,7 +18,7 @@ public static bool IsNestedInSystemNamespace(this TypeDeclarationSyntax node) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ISymbol? GetDeclaredSymbol(this SyntaxNode node, SemanticModelCache semanticCache) { - return GetDeclaredSymbol(node, semanticCache.GetSemanticModel(node)); + return node.GetDeclaredSymbol(semanticCache.GetSemanticModel(node)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -32,103 +27,15 @@ public static bool IsNestedInSystemNamespace(this TypeDeclarationSyntax node) return semanticModel.GetDeclaredSymbol(node); } - public static Builder CreateTypeSchema(this TypeDeclarationSyntax node, LeafDeclaredTypeSyntaxVisitor typeVisitor) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TypeInfo GetTypeInfo(this ExpressionSyntax node, SemanticModelCache semanticCache) { - Throw.IfNullArgument(node); - - Builder builder = CommonSchemas.Object; - - var properties = new Dictionary(StringComparer.OrdinalIgnoreCase); - var requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Collect primary-constructor parameters - if (node.ParameterList is not null) - { - foreach (ParameterSyntax parameter in node.ParameterList.Parameters) - { - ProcessParameter(parameter); - } - } - - foreach (MemberDeclarationSyntax member in node.Members) - { - ProcessMember(member); - } - - // Apply collected properties and required properties - if (properties.Count > 0) - { - builder = builder.Properties(properties); - } - - if (requiredProperties.Count > 0) - { - builder = builder.Required(requiredProperties); - } - - if (typeVisitor.Options.Traversal.CheckFlag(Traversal.Bases)) - { - // Apply base types - if (node.BaseList is not null) - { - foreach (BaseTypeSyntax baseType in node.BaseList.Types) - { - if (typeVisitor.Visit(baseType) is Builder baseTypeBuilder) - { - builder.MergeBaseProperties(baseTypeBuilder); - } - } - } - } - - return builder; - - void ProcessMember(MemberDeclarationSyntax member) - { - if (member is PropertyDeclarationSyntax property && typeVisitor.Visit(property) is Builder propertyBuilder) - { - properties[property.Identifier.Text.Camelize()] = propertyBuilder; - - // Check for nullability annotation - bool isNullable = property.Type is NullableTypeSyntax; - - // Check for SchemaRequired attribute - bool hasSchemaRequiredAttribute = property.AttributeLists - .SelectMany(attrList => attrList.Attributes) - .Any(attr => attr.Name.ToString() == nameof(SchemaRequiredAttribute)); - - // Check for required keyword - bool hasRequiredKeyword = property.Modifiers.Any(SyntaxKind.RequiredKeyword); - - if (!isNullable || hasSchemaRequiredAttribute || hasRequiredKeyword) - { - requiredProperties.Add(property.Identifier.Text.Camelize()); - } - } - } - - void ProcessParameter(ParameterSyntax parameter) - { - if (typeVisitor.Visit(parameter) is Builder paramBuilder) - { - properties[parameter.Identifier.Text.Camelize()] = paramBuilder; - - // Check for default value - bool hasDefaultValue = parameter.Default is not null; - - // Check for nullability annotation - bool isNullable = parameter.Type is NullableTypeSyntax; - - // Check for SchemaRequired attribute - bool hasSchemaRequiredAttribute = parameter.AttributeLists - .SelectMany(attrList => attrList.Attributes) - .Any(attr => attr.Name.ToString() == nameof(SchemaRequiredAttribute)); + return node.GetTypeInfo(semanticCache.GetSemanticModel(node)); + } - if (!hasDefaultValue && !isNullable && !hasSchemaRequiredAttribute) - { - requiredProperties.Add(parameter.Identifier.Text.Camelize()); - } - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TypeInfo GetTypeInfo(this ExpressionSyntax node, SemanticModel semanticModel) + { + return semanticModel.GetTypeInfo(node); } } diff --git a/src/SharpSchema.Generator/Utilities/Throw.cs b/src/SharpSchema.Generator/Utilities/Throw.cs index a83facd..cd2565f 100644 --- a/src/SharpSchema.Generator/Utilities/Throw.cs +++ b/src/SharpSchema.Generator/Utilities/Throw.cs @@ -15,10 +15,15 @@ public static void IfNullArgument([NotNull] T? value, [CallerArgumentExpressi [MethodImpl(MethodImplOptions.AggressiveInlining)] [return: NotNullIfNotNull(nameof(value))] - public static T ForUnexpectedNull([NotNull] T? value, [CallerArgumentExpression(nameof(value))] string? paramName = null, [CallerMemberName] string? caller = null) + public static T IfUnexpectedNull([NotNull] T? value, [CallerArgumentExpression(nameof(value))] string? paramName = null, [CallerMemberName] string? caller = null) where T : class { if (value is null) throw new InvalidOperationException($"Unexpected null value {paramName} in {caller}"); return value; } + + public static TResult UnknownEnumValue(object value, [CallerArgumentExpression("value")] string? paramName = null) + { + throw new InvalidOperationException($"Unknown enum value {value} for {paramName}"); + } } diff --git a/src/SharpSchema.Generator/Utilities/Tracer.cs b/src/SharpSchema.Generator/Utilities/Tracer.cs index 278c28f..2d06eac 100644 --- a/src/SharpSchema.Generator/Utilities/Tracer.cs +++ b/src/SharpSchema.Generator/Utilities/Tracer.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Diagnostics; namespace SharpSchema.Generator.Utilities; @@ -18,6 +19,7 @@ public static class Tracer private static int s_indentLevel = 0; private static LineWriter? s_writer; private static int s_indentWidth = 2; + private static bool s_enableTiming = false; /// /// Sets the writer to use for writing trace messages. @@ -29,6 +31,15 @@ public static class Tracer /// public static int IndentWidth { set => s_indentWidth = value; } + /// + /// Gets or sets whether timing is enabled. + /// + public static bool EnableTiming + { + get => s_enableTiming; + set => s_enableTiming = value; + } + /// /// Writes a trace message followed by a newline. /// @@ -72,6 +83,8 @@ private static string GetIndent() /// public readonly struct TraceScope() : IDisposable { + private readonly Stopwatch? _stopwatch = Tracer.EnableTiming ? Stopwatch.StartNew() : null; + /// /// Writes a trace message followed by a newline. /// @@ -84,6 +97,16 @@ public void WriteLine(string message, [CallerMemberName] string? caller = null) /// /// Decreases the indent level when the scope is disposed. /// - public void Dispose() => Tracer.s_indentLevel--; + public void Dispose() + { + if (Tracer.EnableTiming) + { + _stopwatch!.Stop(); + double seconds = _stopwatch.Elapsed.TotalSeconds; + WriteLine($"Done [{seconds:0.00}s]", "Scope"); + } + + Tracer.s_indentLevel--; + } } } diff --git a/src/SharpSchema.Generator/_docs/visitor-classes.md b/src/SharpSchema.Generator/_docs/visitor-classes.md new file mode 100644 index 0000000..638dee1 --- /dev/null +++ b/src/SharpSchema.Generator/_docs/visitor-classes.md @@ -0,0 +1,48 @@ +# Visitor Classes in SharpSchema.Generator + +## Class Responsibilities and Boundaries + +This document outlines the responsibilities of `RootSyntaxVisitor.cs`, `LeafSyntaxVisitor.cs`, and `NamedTypeResolver.cs`, which form the core of SharpSchema's JSON schema generation process. + +### 1. RootSyntaxVisitor + +- Entry point for schema generation +- Manages the root schema document structure, setting up the schema version +- Handles top-level type declarations (classes, structs, records) +- Coordinates management of declarations for abstract types and their implementations +- Manages schema references via the `$defs` section +- Assembles the final complete schema with all required references + +### 2. LeafSyntaxVisitor + +- Handles the detailed syntax traversal of C# code +- Processes individual language elements like arrays, enums, nullable types, etc. +- Manages schema caching for defined types +- Creates references to abstract types for polymorphic schemas +- Translates various C# syntax nodes into JSON schema components +- Connects to NamedTypeResolver when processing types with members + +### 3. NamedTypeResolver + +- Focuses on symbol-based traversal rather than syntax +- Handles type member resolution (properties, record parameters) +- Manages property requirements based on nullability and attributes +- Implements base type and interface traversal based on TraversalMode +- Processes property metadata from attributes +- Manages property naming conventions and schema customization + +## Class Collaboration Flow + +1. `RootSyntaxVisitor` receives a type declaration and sets up the root schema +2. It calls through to `LeafSyntaxVisitor` to handle the details of the type +3. When a type with members is encountered, `LeafSyntaxVisitor` instantiates `NamedTypeResolver` +4. `NamedTypeResolver` processes the members, referring back to `LeafSyntaxVisitor` to resolve their types +5. `LeafSyntaxVisitor` caches completed schemas +6. `RootSyntaxVisitor` assembles everything into a complete schema with references + +## Design Principles + +- Single Responsibility: Each class has a focused area of concern +- Separation of Concerns: Syntax traversal vs. symbol traversal vs. high-level document structure +- Caching: Types are processed once and referenced thereafter +- Visitor Pattern: Standard Roslyn visitor pattern used throughout diff --git a/test/Generator/AllFeaturesProject/AllFeaturesProjectTests.cs b/test/Generator/AllFeaturesProject/AllFeaturesProjectTests.cs index 07485fd..209ec5c 100644 --- a/test/Generator/AllFeaturesProject/AllFeaturesProjectTests.cs +++ b/test/Generator/AllFeaturesProject/AllFeaturesProjectTests.cs @@ -7,6 +7,7 @@ using VerifyTests; using Xunit; using Xunit.Abstractions; +using SharpSchema.Annotations; namespace SharpSchema.Test.Generator.AllFeaturesProject; @@ -123,12 +124,12 @@ public AllFeaturesProjectTests(ITestOutputHelper outputHelper, AllFeaturesProjec // return schemaRootInfos; //} - public static TheoryData VerifyOptions() + public static TheoryData VerifyOptions() { return new() { - { Accessibilities.Public, Accessibilities.Public}, - { Accessibilities.Any, Accessibilities.Any } + { AccessibilityMode.Public, AccessibilityMode.Public}, + { AccessibilityMode.Any, AccessibilityMode.Any } }; } } diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt deleted file mode 100644 index 95c99cb..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt deleted file mode 100644 index 95c99cb..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt deleted file mode 100644 index 95c99cb..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt deleted file mode 100644 index b92a62e..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "type": "object", - "$comment": "Ensure key type 'Address' is convertible to string.", - "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Data" - } - }, - "required": [ - "data" - ] -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt deleted file mode 100644 index 96fc2e8..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "card": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card", - "title": "Card" - }, - "deck": { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" - }, - "title": "Deck" - } - }, - "required": [ - "card", - "deck" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Card.FaceKind": { - "type": "string", - "enum": [ - "NotFaceCard", - "jack", - "queen", - "king", - "ace" - ], - "title": "Face Kind" - }, - "T:SharpSchema.Generator.TestData.Card.AceOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.KingOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.QueenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.JackOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.TenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card": { - "type": "object", - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.AceOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.KingOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.QueenOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.JackOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.TenOfSpades" - } - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt deleted file mode 100644 index 9b06269..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age" - }, - "address": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address", - "title": "Address" - } - }, - "required": [ - "name", - "age", - "address" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt deleted file mode 100644 index 284ac24..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "address": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address", - "title": "Address Title", - "description": "Address Description", - "$comment": "This is just a test" - }, - "nullableAddress": { - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" - }, - { - "type": "null" - } - ], - "title": "Nullable Address", - "deprecated": true - }, - "addresses": { - "oneOf": [ - { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" - } - }, - { - "type": "null" - } - ], - "title": "Addresses Title" - } - }, - "required": [ - "address" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt deleted file mode 100644 index 29c3c1e..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age" - }, - "person": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Person", - "title": "Person" - } - }, - "required": [ - "name", - "age", - "person" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ] - }, - "T:SharpSchema.Generator.TestData.Office": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "address": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address", - "title": "Address" - } - }, - "required": [ - "name", - "address" - ] - }, - "T:SharpSchema.Generator.TestData.Person": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age" - }, - "office": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Office", - "title": "Office" - } - }, - "required": [ - "name", - "age", - "office" - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt deleted file mode 100644 index 3f36192..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "abstract": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.AbstractClass", - "title": "Abstract" - }, - "concrete": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Class_ExtendsAbstractClass", - "title": "Concrete" - } - }, - "required": [ - "abstract", - "concrete" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Class_ExtendsAbstractClass": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - } - }, - "required": [ - "name" - ] - }, - "T:SharpSchema.Generator.TestData.AbstractClass": { - "type": "object", - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Class_ExtendsAbstractClass" - } - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt deleted file mode 100644 index 808498b..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt +++ /dev/null @@ -1,185 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "string": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "String" - }, - "int": { - "oneOf": [ - { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - { - "type": "null" - } - ], - "title": "Int" - }, - "byte": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 255, - "$comment": "System.Byte" - }, - { - "type": "null" - } - ], - "title": "Byte" - }, - "sByte": { - "oneOf": [ - { - "type": "integer", - "minimum": -128, - "maximum": 127, - "$comment": "System.SByte" - }, - { - "type": "null" - } - ], - "title": "S Byte" - }, - "short": { - "oneOf": [ - { - "type": "integer", - "minimum": -32768, - "maximum": 32767, - "$comment": "System.Int16" - }, - { - "type": "null" - } - ], - "title": "Short" - }, - "uShort": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 65535, - "$comment": "System.UInt16" - }, - { - "type": "null" - } - ], - "title": "U Short" - }, - "uInt": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 4294967295, - "$comment": "System.UInt32" - }, - { - "type": "null" - } - ], - "title": "U Int" - }, - "long": { - "oneOf": [ - { - "type": "integer", - "minimum": -9223372036854775808, - "maximum": 9223372036854775807, - "$comment": "System.Int64" - }, - { - "type": "null" - } - ], - "title": "Long" - }, - "uLong": { - "oneOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 18446744073709551615, - "$comment": "System.UInt64" - }, - { - "type": "null" - } - ], - "title": "U Long" - }, - "float": { - "oneOf": [ - { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335, - "$comment": "System.Single" - }, - { - "type": "null" - } - ], - "title": "Float" - }, - "double": { - "oneOf": [ - { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335, - "$comment": "System.Double" - }, - { - "type": "null" - } - ], - "title": "Double" - }, - "decimal": { - "oneOf": [ - { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335, - "$comment": "System.Decimal" - }, - { - "type": "null" - } - ], - "title": "Decimal" - }, - "char": { - "oneOf": [ - { - "type": "string", - "minLength": 1, - "maxLength": 1, - "$comment": "System.Char" - }, - { - "type": "null" - } - ], - "title": "Char" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt deleted file mode 100644 index 10242cd..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Data" - } - }, - "required": [ - "data" - ] -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt deleted file mode 100644 index 10802ee..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "title": "Data" - } - }, - "required": [ - "data" - ] -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt deleted file mode 100644 index f00504c..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "$unsupportedObject": "Key type 'Address' must be string.", - "title": "Data" - } - }, - "required": [ - "data" - ] -} diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt deleted file mode 100644 index 96fc2e8..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "card": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card", - "title": "Card" - }, - "deck": { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" - }, - "title": "Deck" - } - }, - "required": [ - "card", - "deck" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Card.FaceKind": { - "type": "string", - "enum": [ - "NotFaceCard", - "jack", - "queen", - "king", - "ace" - ], - "title": "Face Kind" - }, - "T:SharpSchema.Generator.TestData.Card.AceOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.KingOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.QueenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.JackOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.TenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card": { - "type": "object", - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.AceOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.KingOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.QueenOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.JackOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.TenOfSpades" - } - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt deleted file mode 100644 index 30a03fd..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt +++ /dev/null @@ -1,110 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "card": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card", - "title": "Card" - }, - "deck": { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" - }, - "title": "Deck" - } - }, - "required": [ - "card", - "deck" - ], - "$defs": { - "T:SharpSchema.Generator.TestData.Card.FaceKind": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Face Kind" - }, - "T:SharpSchema.Generator.TestData.Card.AceOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.KingOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.QueenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.JackOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card.TenOfSpades": { - "type": "object", - "properties": { - "face": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.FaceKind", - "title": "Face" - } - }, - "required": [ - "face" - ] - }, - "T:SharpSchema.Generator.TestData.Card": { - "type": "object", - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.AceOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.KingOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.QueenOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.JackOfSpades" - }, - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.TenOfSpades" - } - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs deleted file mode 100644 index a3a4702..0000000 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Threading.Tasks; -using Json.Schema; -using SharpSchema.Generator; -using SharpSchema.Generator.TestData; -using SharpSchema.Generator.Utilities; -using SharpSchema.Test.Generator.TestUtilities; -using VerifyXunit; -using Xunit; -using Xunit.Abstractions; - -namespace SharpSchema.Test.Generator.RootDeclaredTypeSyntaxVisitorTests; - -public class VerifyTests : IDisposable, IClassFixture -{ - private readonly ITestOutputHelper _output; - private readonly TestDataFixture _fixture; - - public VerifyTests(TestDataFixture fixture, ITestOutputHelper outputHelper) - { - _fixture = fixture; - Tracer.Writer = outputHelper.WriteLine; - _output = outputHelper; - } - - public void Dispose() => Tracer.Writer = null; - - [Theory] - [InlineData(nameof(Struct_WithNullableValueTypes))] - [InlineData(nameof(Struct_WithAbstractProperties))] - [InlineData(nameof(Record_WithReferenceTypeProperties))] - [InlineData(nameof(Record_WithReferenceTypeParameters))] - [InlineData(nameof(Record_WithValueTypeParameters))] - [InlineData(nameof(Record_WithSchemaOverride))] - [InlineData(nameof(Record_WithIgnoredParameter))] - [InlineData(nameof(Record_WithParametersAndProperties))] - [InlineData(nameof(Record_WithAbstractParameters))] - [InlineData(nameof(Class_WithDictionaryProperties))] - [InlineData(nameof(Class_WithDocComments))] - [InlineData(nameof(Class_WithValueTypes))] - [InlineData(nameof(Class_WithSchemaOverride))] - [InlineData(nameof(Class_WithTypeSchemaOverride))] - [InlineData(nameof(Class_WithUnsupportedDictionaryKey))] - [InlineData(nameof(Class_WithIgnoredProperty))] - [InlineData(nameof(Class_WithInternalProperties))] - [InlineData(nameof(Class_WithArrayProperties))] - [InlineData(nameof(Class_WithInvalidProperties))] - [InlineData(nameof(Class_ExtendsAbstractClass))] - [InlineData(nameof(Record_WithGenericAbstractProperty), Skip = "Not Yet Implemented")] - public Task Verify_DefaultOptions(string testName) - { - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); - JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); - _output.WriteSeparator(); - string schemaString = builder.Build().SerializeToJson(); - _output.WriteLine(schemaString); - return Verifier.Verify(schemaString).UseParameters(testName); - } - - [InlineData(DictionaryKeyMode.Loose)] - [InlineData(DictionaryKeyMode.Strict)] - [InlineData(DictionaryKeyMode.Silent)] - [InlineData(DictionaryKeyMode.Skip)] - [Theory] - public Task Verify_DictionaryKeyMode(DictionaryKeyMode dictionaryKeyMode) - { - GeneratorOptions options = new() - { - DictionaryKeyMode = dictionaryKeyMode - }; - - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); - JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithUnsupportedDictionaryKey)); - _output.WriteSeparator(); - string schemaString = builder.Build().SerializeToJson(); - _output.WriteLine(schemaString); - return Verifier.Verify(schemaString).UseParameters(dictionaryKeyMode); - } - - [InlineData(Accessibilities.Public)] - [InlineData(Accessibilities.Internal)] - [InlineData(Accessibilities.Private)] - [InlineData(Accessibilities.PublicInternal)] - [Theory] - public Task Verify_Accessibilities(Accessibilities accessibilities) - { - GeneratorOptions options = new() - { - Accessibilities = accessibilities - }; - - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); - JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithInternalProperties)); - _output.WriteSeparator(); - string schemaString = builder.Build().SerializeToJson(); - _output.WriteLine(schemaString); - return Verifier.Verify(schemaString).UseParameters(accessibilities); - } - - [InlineData(EnumHandling.String)] - [InlineData(EnumHandling.UnderlyingType)] - [Theory] - public Task Verify_EnumHandling(EnumHandling enumHandling) - { - GeneratorOptions options = new() - { - EnumHandling = enumHandling - }; - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); - JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Record_WithAbstractParameters)); - _output.WriteSeparator(); - string schemaString = builder.Build().SerializeToJson(); - _output.WriteLine(schemaString); - return Verifier.Verify(schemaString).UseParameters(enumHandling); - } - - [InlineData(Traversal.SymbolOnly)] - [InlineData(Traversal.Bases)] - [InlineData(Traversal.Interfaces)] - [InlineData(Traversal.Full)] - [Theory] - public Task Verify_Traversal(Traversal traversal) - { - GeneratorOptions options = new() - { - Traversal = traversal - }; - - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); - JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_ExtendsAbstractClass)); - _output.WriteSeparator(); - string schemaString = builder.Build().SerializeToJson(); - _output.WriteLine(schemaString); - return Verifier.Verify(schemaString).UseParameters(traversal); - } -} diff --git a/test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs b/test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs new file mode 100644 index 0000000..16bd577 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs @@ -0,0 +1,78 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +namespace SharpSchema.Generator.TestData; + +using System.Collections.Generic; +using SharpSchema.Annotations; + +public class DictionaryKey_Default +{ + public Dictionary StringKey { get; set; } + + public Dictionary IntKey { get; set; } + + public Dictionary ClassKey { get; set; } +} + +public class DictionaryKey_PropertyOverride +{ + public Dictionary StringKey { get; set; } + + [SchemaDictionaryKeyMode(DictionaryKeyMode.Silent)] + public Dictionary IntKey { get; set; } + + [SchemaDictionaryKeyMode(DictionaryKeyMode.Loose)] + public Dictionary BoolKey { get; set; } + + [SchemaDictionaryKeyMode(DictionaryKeyMode.Strict)] + public Dictionary ClassKey { get; set; } +} + +public class DictionaryKey_NestedOverride +{ + public DictionaryKey_Default Default { get; set; } + + public DictionaryKey_PropertyOverride PropertyOverride { get; set; } +} + +public class Accessibility_Default +{ + public string Public { get; set; } + + internal string Internal { get; set; } + + protected string Protected { get; set; } + + protected internal string ProtectedInternal { get; set; } + + private string Private { get; set; } +} + +[SchemaAccessibilityMode(AccessibilityMode.Internal | AccessibilityMode.Private)] +public class Accessibility_ClassOverride +{ + public string Public { get; set; } + + internal string Internal { get; set; } + + protected string Protected { get; set; } + + protected internal string ProtectedInternal { get; set; } + + private string Private { get; set; } +} + +public class Accessibility_NestedDefault +{ + public Accessibility_Default Default { get; set; } + + public Accessibility_ClassOverride ClassOverride { get; set; } +} + +[SchemaAccessibilityMode(AccessibilityMode.Any)] +public class Accessibility_NestedOverride +{ + private Accessibility_Default Default { get; set; } + + internal Accessibility_ClassOverride ClassOverride { get; set; } +} diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs similarity index 65% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestData.cs rename to test/Generator/RootSyntaxVisitorTests/TestData.cs index 8e07764..b24e852 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -1,13 +1,58 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Text.Json.Serialization; using SharpSchema.Annotations; #pragma warning disable IDE0130 // Namespace does not match folder structure namespace SharpSchema.Generator.TestData; +using Test.Generator.RootSyntaxVisitorTests; + + #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +public record Record_WithValueParameters(string Name, int Age); + +public record Record_WithDefaultValueParameter(string Name, int Age = 42); + +public record Record_WithNullableParameters(string? Name, int? Age); + +public record Record_WithValueParametersAndProperty(string Name, int Age) +{ + public string Title { get; set; } +} + +public record Record_WithValueParametersAndPropertyInitializer(string Name, int Age) +{ + public string Title { get; set; } = "How to make a record"; +} + +public record Record_WithDefaultValueParametersAndConstantProperty(string Name, int Age = 42) +{ + public string Title => "How to make a record"; +} + +/// +/// Demonstrates param-based XML metadata. +/// +/// +/// +/// NameOfRecord +/// The record's name. +/// John Doe +/// +/// +/// +/// +/// AgeOfRecord +/// The record's age. +/// 42 +/// +/// +public record Record_WithDocComments(string Name, int Age = 42); + + public class Class_WithDocComments { /// @@ -69,11 +114,13 @@ public class Class_WithArrayProperties public class Class_WithInvalidProperties { + public int Mutable { get; set; } + + public int Immutable { get; } + public string Name { set { } } public static string Static { get; set; } - - public virtual string Virtual { get; set; } } public class Class_WithDictionaryProperties @@ -84,29 +131,14 @@ public class Class_WithDictionaryProperties } +/// +/// +/// public class Class_WithUnsupportedDictionaryKey { public Dictionary Data { get; set; } } -/// -/// Demonstrates param-based XML metadata. -/// -/// -/// -/// NameOfRecord -/// The record's name. -/// John Doe -/// -/// -/// -/// -/// AgeOfRecord -/// The record's age. -/// 42 -/// -/// -public record Record_WithValueTypeParameters(string Name, int Age = 42); public record Record_WithReferenceTypeParameters( [SchemaMeta( @@ -120,10 +152,6 @@ public record Record_WithReferenceTypeParameters( Title = "Addresses Title")] List
? Addresses); -public record Record_WithParametersAndProperties(string Name, int Age) -{ - public required Address Address { get; set; } -} public struct Struct_WithNullableValueTypes { @@ -155,15 +183,6 @@ public struct Struct_WithNullableValueTypes } -public record Record_WithReferenceTypeProperties -{ - public string Name { get; set; } - - public int Age { get; set; } - - public Person Person { get; set; } -} - public class Class_WithSchemaOverride { [SchemaOverride("{\"type\":\"string\",\"maxLength\":50}")] @@ -191,7 +210,7 @@ public class Class_WithIgnoredProperty [SchemaIgnore] public string Ignored { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [SchemaIgnore] public string AlsoIgnored { get; set; } public string NotIgnored { get; set; } @@ -202,8 +221,13 @@ public record Record_WithIgnoredParameter( string NotIgnored ); +/// +/// +/// public record Class_WithInternalProperties { + public string Public { get; set; } + internal string Internal { get; set; } protected string Protected { get; set; } @@ -220,9 +244,14 @@ public class Class_WithRequiredProperties public string? Optional { get; set; } [SchemaRequired] + public string? DefaultRequired { get; set; } = "default"; + public string? Default { get; set; } = "default"; } +// -- Not Tested -- // + + public class Class_ExtendsAbstractClass : AbstractClass { public override string Name { get; set; } @@ -240,10 +269,10 @@ public record Record_WithAbstractParameters(Card Card, ImmutableArray Deck public record Record_WithGenericAbstractProperty { public MagicStack Stack { get; set; } - - public CardStack CardStack { get; set; } } +public record MagicStack(List Cards); + [SchemaOverride("{invalidJson}")] public record BadOverride(); @@ -289,67 +318,115 @@ public abstract class AbstractClass public int Age { get; set; } } -public class MagicStack : CardStack +public class GameHall { - public List Stack = []; + public string Name { get; set; } + + public Dictionary Rooms { get; set; } +} + +public struct GameRoom +{ + public string Name { get; set; } - public override Card.AceOfSpades Peek() => this.Stack[^1]; + public Table WildCardTable { get; set; } - public override void Push(Card.AceOfSpades card) => this.Stack.Add(card); + public PokerTable PokerTable { get; set; } + + public BlackjackTable BlackjackTable { get; set; } + + public BridgeTable BridgeTable { get; set; } } -public abstract class CardStack - where TCard : Card +public abstract record Table(int PlayerCount) where T : BaseHand { - public abstract TCard Peek(); + public abstract T? DealerHand { get; } - public abstract void Push(TCard card); + public IReadOnlyCollection Hands { get; } } -public abstract record Card(Card.SuitKind Suit, int Value) +public record PokerTable(int PlayerCount) : Table(PlayerCount) { - public enum SuitKind : byte - { - Spades, - Hearts, - Clubs, - Diamonds - } + public override BaseHand.Poker DealerHand { get; } +} - public enum FaceKind - { - [SchemaEnumValue("NotFaceCard")] - None, - Jack, - Queen, - King, - Ace - } +public record BlackjackTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Blackjack DealerHand { get; } +} + +public record BridgeTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Bridge? DealerHand => null; +} + +public record WhistTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Bridge? DealerHand => null; +} - public abstract FaceKind Face { get; } +public abstract record BaseHand(int Size) +{ + public abstract string Game { get; } + + public List Cards { get; set; } - public record AceOfSpades() : Card(SuitKind.Spades, 1) + public record Poker() : BaseHand(5) { - public override FaceKind Face => FaceKind.Ace; + public override string Game => "Poker"; + + public bool IsRoyalFlush => Cards.Count == Size && Cards.All(c => c.IsFaceCard); } - public record KingOfSpades() : Card(SuitKind.Spades, 13) + public record Blackjack() : BaseHand(2) { - public override FaceKind Face => FaceKind.King; + public override string Game => "Blackjack"; + + public int Value => Cards.Sum(c => c.Rank switch + { + Card.RankKind.Ace => 11, + Card.RankKind.Jack => 10, + Card.RankKind.Queen => 10, + Card.RankKind.King => 10, + _ => (int)c.Rank + }); } - public record QueenOfSpades() : Card(SuitKind.Spades, 12) + public record Bridge() : BaseHand(13) { - public override FaceKind Face => FaceKind.Queen; + public override string Game => "Bridge"; + + public bool IsNoTrump => Cards.All(c => c.Suit == Card.SuitKind.Spades); } +} + + +public record Card(Card.SuitKind Suit, Card.RankKind Rank) +{ + public bool IsFaceCard => Rank >= RankKind.Jack; - public record JackOfSpades() : Card(SuitKind.Spades, 11) + public enum SuitKind { - public override FaceKind Face => FaceKind.Jack; + Spades, + Hearts, + Clubs, + Diamonds } - public record TenOfSpades() : Card(SuitKind.Spades, 10) + public enum RankKind { - public override FaceKind Face => FaceKind.None; + Ace, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Jack, + Queen, + King } } diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestDataFixture.cs b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs similarity index 73% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestDataFixture.cs rename to test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs index 98571b0..09b7918 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestDataFixture.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs @@ -8,13 +8,12 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.IO; using Xunit; -using DiffEngine; using SharpSchema.Generator; using SharpSchema.Test.Generator.TestUtilities; using System.Collections.Immutable; using System.Text.Json.Serialization; -namespace SharpSchema.Test.Generator.RootDeclaredTypeSyntaxVisitorTests; +namespace SharpSchema.Test.Generator.RootSyntaxVisitorTests; public class TestDataFixture { @@ -23,20 +22,30 @@ public class TestDataFixture public TestDataFixture() { - DiffRunner.Disabled = true; string pathToTestData = PathHelper.GetRepoPath( "test", "Generator", - "RootDeclaredTypeSyntaxVisitorTests", + "RootSyntaxVisitorTests", "TestData.cs"); + string pathToAccessibility = PathHelper.GetRepoPath( + "test", + "Generator", + "RootSyntaxVisitorTests", + "TestData.Attributes.cs"); + // Create an array of syntax tree from all cs files in src/SharpSchema.Annotations/ string[] annotationFiles = Directory.GetFiles( PathHelper.GetRepoPath("src", "SharpSchema.Annotations"), "*.cs", SearchOption.AllDirectories); List annotationSyntaxTrees = [.. annotationFiles.Select(file => CSharpSyntaxTree.ParseText(File.ReadAllText(file)))]; - _syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(pathToTestData)); + _syntaxTree = CSharpSyntaxTree.ParseText( + string.Join( + Environment.NewLine, + File.ReadAllText(pathToTestData), + File.ReadAllText(pathToAccessibility))); + _compilation = CSharpCompilation.Create("TestDataCompilation") .AddReferences( MetadataReference.CreateFromFile(typeof(object).Assembly.Location), @@ -45,9 +54,9 @@ public TestDataFixture() .AddSyntaxTrees([.. annotationSyntaxTrees, _syntaxTree]); } - public RootDeclaredTypeSyntaxVisitor GetVisitor(GeneratorOptions options) => new(_compilation, options); + public RootSyntaxVisitor GetVisitor(GeneratorOptions options) => new(_compilation, options); - public JsonSchemaBuilder GetJsonSchemaBuilder(RootDeclaredTypeSyntaxVisitor visitor, [CallerMemberName] string? testName = null) + public JsonSchemaBuilder GetJsonSchemaBuilder(RootSyntaxVisitor visitor, [CallerMemberName] string? testName = null) { ArgumentNullException.ThrowIfNull(visitor); ArgumentNullException.ThrowIfNull(testName); diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt similarity index 82% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt index 68fb175..a57cef1 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with internal properties", "properties": { "internal": { "type": "string", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt similarity index 82% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt index c84de0f..b65799f 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with internal properties", "properties": { "private": { "type": "string", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt similarity index 61% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt index 03fd3c2..546e0be 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt @@ -1,13 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with internal properties", "properties": { - "name": { + "public": { "type": "string", - "title": "Name" + "title": "Public" } }, "required": [ - "name" + "public" ] } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_PublicInternal.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_PublicInternal.verified.txt new file mode 100644 index 0000000..a6e0c5d --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_PublicInternal.verified.txt @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with internal properties", + "properties": { + "public": { + "type": "string", + "title": "Public" + }, + "internal": { + "type": "string", + "title": "Internal" + } + }, + "required": [ + "public", + "internal" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_ClassOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_ClassOverride.verified.txt new file mode 100644 index 0000000..1b2f82d --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_ClassOverride.verified.txt @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Accessibility class override", + "properties": { + "internal": { + "type": "string", + "title": "Internal" + }, + "private": { + "type": "string", + "title": "Private" + } + }, + "required": [ + "internal", + "private" + ] +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt similarity index 63% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt index 68fb175..e5b3128 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt @@ -1,13 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Accessibility default", "properties": { - "internal": { + "public": { "type": "string", - "title": "Internal" + "title": "Public" } }, "required": [ - "internal" + "public" ] } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedDefault.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedDefault.verified.txt new file mode 100644 index 0000000..f735ff0 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedDefault.verified.txt @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Accessibility nested default", + "properties": { + "default": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_Default", + "title": "Default" + }, + "classOverride": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_ClassOverride", + "title": "Class override" + } + }, + "required": [ + "default", + "classOverride" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_ClassOverride": { + "type": "object", + "title": "Accessibility class override", + "properties": { + "internal": { + "type": "string", + "title": "Internal" + }, + "private": { + "type": "string", + "title": "Private" + } + }, + "required": [ + "internal", + "private" + ] + }, + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_Default": { + "type": "object", + "title": "Accessibility default", + "properties": { + "public": { + "type": "string", + "title": "Public" + } + }, + "required": [ + "public" + ] + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedOverride.verified.txt new file mode 100644 index 0000000..31f9692 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedOverride.verified.txt @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Accessibility nested override", + "properties": { + "default": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_Default", + "title": "Default" + }, + "classOverride": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_ClassOverride", + "title": "Class override" + } + }, + "required": [ + "default", + "classOverride" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_ClassOverride": { + "type": "object", + "title": "Accessibility class override", + "properties": { + "internal": { + "type": "string", + "title": "Internal" + }, + "private": { + "type": "string", + "title": "Private" + } + }, + "required": [ + "internal", + "private" + ] + }, + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.Accessibility_Default": { + "type": "object", + "title": "Accessibility default", + "properties": { + "public": { + "type": "string", + "title": "Public" + } + }, + "required": [ + "public" + ] + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt similarity index 61% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt index 8354a29..8a61578 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt @@ -1,21 +1,25 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class extends abstract class", "properties": { - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age" - }, "name": { "type": "string", "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" } }, "required": [ - "name", - "age" - ] + "name" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt similarity index 79% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt index 663796d..aa1a33a 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt @@ -1,16 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with array properties", "properties": { "numbersArray": { "type": "array", "items": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" + "$ref": "#/$defs/T:System.Int32" }, - "title": "Numbers Array" + "title": "Numbers array" }, "names": { "type": "array", @@ -43,12 +41,9 @@ "numbersList": { "type": "array", "items": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" + "$ref": "#/$defs/T:System.Int32" }, - "title": "Numbers List" + "title": "Numbers list" }, "stringsEnumerable": { "oneOf": [ @@ -62,14 +57,14 @@ "type": "null" } ], - "title": "Strings Enumerable" + "title": "Strings enumerable" }, "stringImmutableArray": { "type": "array", "items": { "type": "string" }, - "title": "String Immutable Array" + "title": "String immutable array" } }, "required": [ @@ -81,6 +76,7 @@ "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", + "title": "Address", "properties": { "street": { "type": "string", @@ -95,6 +91,11 @@ "street", "city" ] + }, + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt similarity index 72% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt index 10275ce..e8ff5bd 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt @@ -1,23 +1,21 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with dictionary properties", "properties": { "valueTypes": { "type": "object", "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" + "$ref": "#/$defs/T:System.Int32" }, - "title": "Value Types" + "title": "Value types" }, "referenceTypes": { "type": "object", "additionalProperties": { "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" }, - "title": "Reference Types" + "title": "Reference types" } }, "required": [ @@ -27,6 +25,7 @@ "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", + "title": "Address", "properties": { "street": { "type": "string", @@ -41,6 +40,11 @@ "street", "city" ] + }, + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt similarity index 70% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt index fdb32bb..d498641 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt @@ -1,16 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with doc comments", "properties": { "name": { "type": "string", "title": "The name of the person." }, "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", + "$ref": "#/$defs/T:System.Int32", "title": "Age", "description": "The age of the person." } @@ -18,5 +16,12 @@ "required": [ "name", "age" - ] + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt similarity index 73% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt index 8de934f..2140eac 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with ignored property", "properties": { "notIgnored": { "type": "string", - "title": "Not Ignored" + "title": "Not ignored" } }, "required": [ diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithInvalidProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithInvalidProperties.verified.txt new file mode 100644 index 0000000..24d522b --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithInvalidProperties.verified.txt @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with invalid properties", + "properties": { + "mutable": { + "$ref": "#/$defs/T:System.Int32", + "title": "Mutable" + }, + "immutable": { + "$ref": "#/$defs/T:System.Int32", + "title": "Immutable" + } + }, + "required": [ + "mutable", + "immutable" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt new file mode 100644 index 0000000..2d7d24e --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with required properties", + "properties": { + "requiredInt": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Int32" + }, + { + "type": "null" + } + ], + "title": "Required int" + }, + "required": { + "type": "string", + "title": "Required" + }, + "optional": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Optional" + }, + "defaultRequired": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "default", + "title": "Default required" + }, + "default": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "default", + "title": "Default" + } + }, + "required": [ + "requiredInt", + "required", + "defaultRequired" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt similarity index 87% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt index 95c6637..7989690 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with schema override", "properties": { "name": { "type": "string", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt similarity index 82% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt index 505a1e8..5c02023 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with type schema override", "properties": { "badOverride": { "$unsupportedObject": "Failed to parse schema override: 'i' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.", - "title": "Bad Override" + "title": "Bad override" }, "goodOverride": { "type": "object", @@ -13,7 +14,7 @@ "type": "string" } }, - "title": "Good Override" + "title": "Good override" } }, "required": [ diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt new file mode 100644 index 0000000..638f365 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt @@ -0,0 +1,305 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Game hall", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "rooms": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.GameRoom" + }, + "title": "Rooms" + } + }, + "required": [ + "name", + "rooms" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.BaseHand.Blackjack": { + "type": "object", + "title": "Blackjack", + "properties": { + "game": { + "const": "Blackjack", + "title": "Game", + "type": "string" + }, + "value": { + "$ref": "#/$defs/T:System.Int32", + "title": "Value" + }, + "size": { + "$ref": "#/$defs/T:System.Int32", + "title": "Size" + }, + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" + } + }, + "required": [ + "game", + "value" + ] + }, + "T:SharpSchema.Generator.TestData.BaseHand.Bridge": { + "type": "object", + "title": "Bridge", + "properties": { + "game": { + "const": "Bridge", + "title": "Game", + "type": "string" + }, + "isNoTrump": { + "type": "boolean", + "title": "Is no trump" + }, + "size": { + "$ref": "#/$defs/T:System.Int32", + "title": "Size" + }, + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" + } + }, + "required": [ + "game", + "isNoTrump" + ] + }, + "T:SharpSchema.Generator.TestData.BaseHand.Poker": { + "type": "object", + "title": "Poker", + "properties": { + "game": { + "const": "Poker", + "title": "Game", + "type": "string" + }, + "isRoyalFlush": { + "type": "boolean", + "title": "Is royal flush" + }, + "size": { + "$ref": "#/$defs/T:System.Int32", + "title": "Size" + }, + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" + } + }, + "required": [ + "game", + "isRoyalFlush" + ] + }, + "T:SharpSchema.Generator.TestData.BlackjackTable": { + "type": "object", + "title": "Blackjack table", + "properties": { + "playerCount": { + "$ref": "#/$defs/T:System.Int32", + "title": "Player count" + }, + "dealerHand": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Blackjack", + "title": "Dealer hand" + } + }, + "required": [ + "playerCount", + "dealerHand" + ] + }, + "T:SharpSchema.Generator.TestData.BridgeTable": { + "type": "object", + "title": "Bridge table", + "properties": { + "playerCount": { + "$ref": "#/$defs/T:System.Int32", + "title": "Player count" + }, + "dealerHand": { + "oneOf": [ + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" + }, + { + "type": "null" + } + ], + "title": "Dealer hand" + } + }, + "required": [ + "playerCount" + ] + }, + "T:SharpSchema.Generator.TestData.Card": { + "type": "object", + "title": "Card", + "properties": { + "suit": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.SuitKind", + "title": "Suit" + }, + "rank": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.RankKind", + "title": "Rank" + }, + "isFaceCard": { + "type": "boolean", + "title": "Is face card" + } + }, + "required": [ + "suit", + "rank", + "isFaceCard" + ] + }, + "T:SharpSchema.Generator.TestData.Card.RankKind": { + "type": "string", + "enum": [ + "ace", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "jack", + "queen", + "king" + ], + "title": "Rank kind" + }, + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit kind" + }, + "T:SharpSchema.Generator.TestData.GameRoom": { + "type": "object", + "title": "Game room", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "wildCardTable": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Table`1", + "title": "Wild card table" + }, + "pokerTable": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.PokerTable", + "title": "Poker table" + }, + "blackjackTable": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BlackjackTable", + "title": "Blackjack table" + }, + "bridgeTable": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BridgeTable", + "title": "Bridge table" + } + }, + "required": [ + "name", + "wildCardTable", + "pokerTable", + "blackjackTable", + "bridgeTable" + ] + }, + "T:SharpSchema.Generator.TestData.PokerTable": { + "type": "object", + "title": "Poker table", + "properties": { + "playerCount": { + "$ref": "#/$defs/T:System.Int32", + "title": "Player count" + }, + "dealerHand": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Poker", + "title": "Dealer hand" + } + }, + "required": [ + "playerCount", + "dealerHand" + ] + }, + "T:SharpSchema.Generator.TestData.Table`1": { + "type": "object", + "oneOf": [ + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.PokerTable" + }, + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BlackjackTable" + }, + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BridgeTable" + }, + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.WhistTable" + } + ] + }, + "T:SharpSchema.Generator.TestData.WhistTable": { + "type": "object", + "title": "Whist table", + "properties": { + "playerCount": { + "$ref": "#/$defs/T:System.Int32", + "title": "Player count" + }, + "dealerHand": { + "oneOf": [ + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" + }, + { + "type": "null" + } + ], + "title": "Dealer hand" + } + }, + "required": [ + "playerCount" + ] + }, + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParameter.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParameter.verified.txt new file mode 100644 index 0000000..b77442c --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParameter.verified.txt @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with default value parameter", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "default": 42, + "title": "Age" + } + }, + "required": [ + "name" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParametersAndConstantProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParametersAndConstantProperty.verified.txt new file mode 100644 index 0000000..2173ef1 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParametersAndConstantProperty.verified.txt @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with default value parameters and constant property", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "default": 42, + "title": "Age" + }, + "title": { + "const": "How to make a record", + "title": "Title" + } + }, + "required": [ + "name", + "title" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt similarity index 65% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt index f905db0..6f6b566 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt @@ -1,6 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "NameOfRecord", + "description": "The record's name.", + "examples": [ + "John Doe" + ], "properties": { "name": { "type": "string", @@ -11,19 +16,23 @@ ] }, "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", + "$ref": "#/$defs/T:System.Int32", + "default": 42, "title": "NameOfRecord", "description": "The record's name.", "examples": [ "42" - ], - "default": 42 + ] } }, "required": [ "name" - ] + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt similarity index 72% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt index 8de934f..be454b0 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Record with ignored parameter", "properties": { "notIgnored": { "type": "string", - "title": "Not Ignored" + "title": "Not ignored" } }, "required": [ diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt similarity index 86% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt index 95c6637..25a8fe0 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Record with schema override", "properties": { "name": { "type": "string", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt similarity index 65% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt index 8354a29..8c81e3a 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt @@ -1,21 +1,26 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Record with value parameters", "properties": { - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age" - }, "name": { "type": "string", "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" } }, "required": [ "name", "age" - ] + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndProperty.verified.txt new file mode 100644 index 0000000..c26b0d4 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndProperty.verified.txt @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with value parameters and property", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" + }, + "title": { + "type": "string", + "title": "Title" + } + }, + "required": [ + "name", + "age", + "title" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndPropertyInitializer.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndPropertyInitializer.verified.txt new file mode 100644 index 0000000..ca00a31 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndPropertyInitializer.verified.txt @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with value parameters and property initializer", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" + }, + "title": { + "type": "string", + "default": "How to make a record", + "title": "Title" + } + }, + "required": [ + "name", + "age" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt new file mode 100644 index 0000000..26c6c93 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt @@ -0,0 +1,212 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Struct with nullable value types", + "properties": { + "string": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "String" + }, + "int": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Int32" + }, + { + "type": "null" + } + ], + "title": "Int" + }, + "byte": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Byte" + }, + { + "type": "null" + } + ], + "title": "Byte" + }, + "sByte": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.SByte" + }, + { + "type": "null" + } + ], + "title": "S byte" + }, + "short": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Int16" + }, + { + "type": "null" + } + ], + "title": "Short" + }, + "uShort": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.UInt16" + }, + { + "type": "null" + } + ], + "title": "U short" + }, + "uInt": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.UInt32" + }, + { + "type": "null" + } + ], + "title": "U int" + }, + "long": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Int64" + }, + { + "type": "null" + } + ], + "title": "Long" + }, + "uLong": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.UInt64" + }, + { + "type": "null" + } + ], + "title": "U long" + }, + "float": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Single" + }, + { + "type": "null" + } + ], + "title": "Float" + }, + "double": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Double" + }, + { + "type": "null" + } + ], + "title": "Double" + }, + "decimal": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Decimal" + }, + { + "type": "null" + } + ], + "title": "Decimal" + }, + "char": { + "oneOf": [ + { + "$ref": "#/$defs/T:System.Char" + }, + { + "type": "null" + } + ], + "title": "Char" + } + }, + "$defs": { + "T:System.Byte": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "T:System.Char": { + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "T:System.Decimal": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Double": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Int16": { + "type": "integer", + "minimum": -32768, + "maximum": 32767 + }, + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:System.Int64": { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807 + }, + "T:System.SByte": { + "type": "integer", + "minimum": -128, + "maximum": 127 + }, + "T:System.Single": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Loose.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Loose.verified.txt new file mode 100644 index 0000000..81bc57f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Loose.verified.txt @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with unsupported dictionary key", + "properties": { + "data": { + "type": "object", + "$comment": "Key type 'Object' must be convertible to string", + "additionalProperties": { + "$ref": "#/$defs/T:System.Int32" + }, + "title": "Data" + } + }, + "required": [ + "data" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt similarity index 51% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt index b92a62e..46614c1 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt @@ -1,20 +1,24 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with unsupported dictionary key", "properties": { "data": { "type": "object", - "$comment": "Ensure key type 'Address' is convertible to string.", "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" + "$ref": "#/$defs/T:System.Int32" }, "title": "Data" } }, "required": [ "data" - ] + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Skip.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Skip.verified.txt new file mode 100644 index 0000000..1973378 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Skip.verified.txt @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with unsupported dictionary key", + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Strict.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Strict.verified.txt new file mode 100644 index 0000000..171433a --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Strict.verified.txt @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with unsupported dictionary key", + "properties": { + "data": { + "$unsupportedObject": "Key type 'Object' is not supported.", + "title": "Data" + } + }, + "required": [ + "data" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_Default.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_Default.verified.txt new file mode 100644 index 0000000..5264ae2 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_Default.verified.txt @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Dictionary key default", + "properties": { + "stringKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "String key" + }, + "intKey": { + "type": "object", + "$comment": "Key type 'Integer' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Int key" + }, + "classKey": { + "type": "object", + "$comment": "Key type 'Object' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Class key" + } + }, + "required": [ + "stringKey", + "intKey", + "classKey" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_NestedOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_NestedOverride.verified.txt new file mode 100644 index 0000000..3c17673 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_NestedOverride.verified.txt @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Dictionary key nested override", + "properties": { + "default": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.DictionaryKey_Default", + "title": "Default" + }, + "propertyOverride": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.DictionaryKey_PropertyOverride", + "title": "Property override" + } + }, + "required": [ + "default", + "propertyOverride" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.DictionaryKey_Default": { + "type": "object", + "title": "Dictionary key default", + "properties": { + "stringKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "String key" + }, + "intKey": { + "type": "object", + "$comment": "Key type 'Integer' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Int key" + }, + "classKey": { + "type": "object", + "$comment": "Key type 'Object' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Class key" + } + }, + "required": [ + "stringKey", + "intKey", + "classKey" + ] + }, + "T:SharpSchema.Generator.TestData.SharpSchema.Generator.TestData.DictionaryKey_PropertyOverride": { + "type": "object", + "title": "Dictionary key property override", + "properties": { + "stringKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "String key" + }, + "intKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "Int key" + }, + "boolKey": { + "type": "object", + "$comment": "Key type 'Boolean' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Bool key" + }, + "classKey": { + "$unsupportedObject": "Key type 'Object' is not supported.", + "title": "Class key" + } + }, + "required": [ + "stringKey", + "intKey", + "boolKey", + "classKey" + ] + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_PropertyOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_PropertyOverride.verified.txt new file mode 100644 index 0000000..2429a45 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_PropertyOverride.verified.txt @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Dictionary key property override", + "properties": { + "stringKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "String key" + }, + "intKey": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "Int key" + }, + "boolKey": { + "type": "object", + "$comment": "Key type 'Boolean' must be convertible to string", + "additionalProperties": { + "type": "string" + }, + "title": "Bool key" + }, + "classKey": { + "$unsupportedObject": "Key type 'Object' is not supported.", + "title": "Class key" + } + }, + "required": [ + "stringKey", + "intKey", + "boolKey", + "classKey" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt new file mode 100644 index 0000000..85d7d7f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with abstract parameters", + "properties": { + "card": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card", + "title": "Card" + }, + "deck": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Deck" + } + }, + "required": [ + "card", + "deck" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.Card": { + "type": "object", + "title": "Card", + "properties": { + "suit": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.SuitKind", + "title": "Suit" + }, + "rank": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.RankKind", + "title": "Rank" + }, + "isFaceCard": { + "type": "boolean", + "title": "Is face card" + } + }, + "required": [ + "suit", + "rank", + "isFaceCard" + ] + }, + "T:SharpSchema.Generator.TestData.Card.RankKind": { + "type": "string", + "enum": [ + "ace", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "jack", + "queen", + "king" + ], + "title": "Rank kind" + }, + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit kind" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt new file mode 100644 index 0000000..6543c17 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with abstract parameters", + "properties": { + "card": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card", + "title": "Card" + }, + "deck": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Deck" + } + }, + "required": [ + "card", + "deck" + ], + "$defs": { + "T:SharpSchema.Generator.TestData.Card": { + "type": "object", + "title": "Card", + "properties": { + "suit": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.SuitKind", + "title": "Suit" + }, + "rank": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card.RankKind", + "title": "Rank" + }, + "isFaceCard": { + "type": "boolean", + "title": "Is face card" + } + }, + "required": [ + "suit", + "rank", + "isFaceCard" + ] + }, + "T:SharpSchema.Generator.TestData.Card.RankKind": { + "type": "integer", + "title": "Rank kind" + }, + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "integer", + "title": "Suit kind" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_JsonNative.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_JsonNative.verified.txt new file mode 100644 index 0000000..6eb0385 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_JsonNative.verified.txt @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with value types", + "properties": { + "string": { + "type": "string", + "title": "String" + }, + "int": { + "type": "integer", + "title": "Int" + }, + "bool": { + "type": "boolean", + "title": "Bool" + }, + "byte": { + "type": "integer", + "title": "Byte" + }, + "sByte": { + "type": "integer", + "title": "S byte" + }, + "short": { + "type": "integer", + "title": "Short" + }, + "uShort": { + "type": "integer", + "title": "U short" + }, + "uInt": { + "type": "integer", + "title": "U int" + }, + "long": { + "type": "integer", + "title": "Long" + }, + "uLong": { + "type": "integer", + "title": "U long" + }, + "float": { + "type": "integer", + "title": "Float" + }, + "double": { + "type": "number", + "title": "Double" + }, + "decimal": { + "type": "number", + "title": "Decimal" + }, + "char": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "title": "Char" + } + }, + "required": [ + "string", + "int", + "bool", + "byte", + "sByte", + "short", + "uShort", + "uInt", + "long", + "uLong", + "float", + "double", + "decimal", + "char" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt new file mode 100644 index 0000000..5383374 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt @@ -0,0 +1,141 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with value types", + "properties": { + "string": { + "type": "string", + "title": "String" + }, + "int": { + "$ref": "#/$defs/T:System.Int32", + "title": "Int" + }, + "bool": { + "type": "boolean", + "title": "Bool" + }, + "byte": { + "$ref": "#/$defs/T:System.Byte", + "title": "Byte" + }, + "sByte": { + "$ref": "#/$defs/T:System.SByte", + "title": "S byte" + }, + "short": { + "$ref": "#/$defs/T:System.Int16", + "title": "Short" + }, + "uShort": { + "$ref": "#/$defs/T:System.UInt16", + "title": "U short" + }, + "uInt": { + "$ref": "#/$defs/T:System.UInt32", + "title": "U int" + }, + "long": { + "$ref": "#/$defs/T:System.Int64", + "title": "Long" + }, + "uLong": { + "$ref": "#/$defs/T:System.UInt64", + "title": "U long" + }, + "float": { + "$ref": "#/$defs/T:System.Single", + "title": "Float" + }, + "double": { + "$ref": "#/$defs/T:System.Double", + "title": "Double" + }, + "decimal": { + "$ref": "#/$defs/T:System.Decimal", + "title": "Decimal" + }, + "char": { + "$ref": "#/$defs/T:System.Char", + "title": "Char" + } + }, + "required": [ + "string", + "int", + "bool", + "byte", + "sByte", + "short", + "uShort", + "uInt", + "long", + "uLong", + "float", + "double", + "decimal", + "char" + ], + "$defs": { + "T:System.Byte": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "T:System.Char": { + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "T:System.Decimal": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Double": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Int16": { + "type": "integer", + "minimum": -32768, + "maximum": 32767 + }, + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:System.Int64": { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807 + }, + "T:System.SByte": { + "type": "integer", + "minimum": -128, + "maximum": 127 + }, + "T:System.Single": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt similarity index 79% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt index 02535bb..bdafc3e 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class with value types", "properties": { "string": { "type": "string", @@ -10,7 +11,6 @@ "type": "integer", "minimum": -2147483648, "maximum": 2147483647, - "$comment": "System.Int32", "title": "Int" }, "bool": { @@ -21,77 +21,66 @@ "type": "integer", "minimum": 0, "maximum": 255, - "$comment": "System.Byte", "title": "Byte" }, "sByte": { "type": "integer", "minimum": -128, "maximum": 127, - "$comment": "System.SByte", - "title": "S Byte" + "title": "S byte" }, "short": { "type": "integer", "minimum": -32768, "maximum": 32767, - "$comment": "System.Int16", "title": "Short" }, "uShort": { "type": "integer", "minimum": 0, "maximum": 65535, - "$comment": "System.UInt16", - "title": "U Short" + "title": "U short" }, "uInt": { "type": "integer", "minimum": 0, "maximum": 4294967295, - "$comment": "System.UInt32", - "title": "U Int" + "title": "U int" }, "long": { "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807, - "$comment": "System.Int64", "title": "Long" }, "uLong": { "type": "integer", "minimum": 0, "maximum": 18446744073709551615, - "$comment": "System.UInt64", - "title": "U Long" + "title": "U long" }, "float": { "type": "number", "minimum": -79228162514264337593543950335, "maximum": 79228162514264337593543950335, - "$comment": "System.Single", "title": "Float" }, "double": { "type": "number", "minimum": -79228162514264337593543950335, "maximum": 79228162514264337593543950335, - "$comment": "System.Double", "title": "Double" }, "decimal": { "type": "number", "minimum": -79228162514264337593543950335, "maximum": 79228162514264337593543950335, - "$comment": "System.Decimal", "title": "Decimal" }, "char": { "type": "string", "minLength": 1, "maxLength": 1, - "$comment": "System.Char", "title": "Char" } }, diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Bases.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Bases.verified.txt new file mode 100644 index 0000000..8a61578 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Bases.verified.txt @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class extends abstract class", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" + } + }, + "required": [ + "name" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Full.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Full.verified.txt new file mode 100644 index 0000000..8a61578 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Full.verified.txt @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class extends abstract class", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age" + } + }, + "required": [ + "name" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt similarity index 82% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt index 03fd3c2..4149796 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class extends abstract class", "properties": { "name": { "type": "string", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt similarity index 82% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt rename to test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt index 03fd3c2..4149796 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", + "title": "Class extends abstract class", "properties": { "name": { "type": "string", diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs new file mode 100644 index 0000000..259c1d7 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Threading.Tasks; +using Json.Schema; +using SharpSchema.Annotations; +using SharpSchema.Generator; +using SharpSchema.Generator.TestData; +using SharpSchema.Generator.Utilities; +using SharpSchema.Test.Generator.TestUtilities; +using VerifyXunit; +using Xunit; +using Xunit.Abstractions; + +namespace SharpSchema.Test.Generator.RootSyntaxVisitorTests; + + +public class VerifyTests : IDisposable, IClassFixture +{ + private readonly ITestOutputHelper _output; + private readonly TestDataFixture _fixture; + + public VerifyTests(TestDataFixture fixture, ITestOutputHelper outputHelper) + { + ArgumentNullException.ThrowIfNull(outputHelper); + + _fixture = fixture; + Tracer.Writer = outputHelper.WriteLine; + _output = outputHelper; + } + + public void Dispose() => Tracer.Writer = null; + + internal static Task Verify(string schemaString, string testName, string parameter) + { + return Verifier.Verify(schemaString) + .UseDirectory("Verifications") + .UseFileName($"{testName}_{parameter}"); + } + + [Theory] + [InlineData(nameof(Class_WithArrayProperties))] + [InlineData(nameof(Class_WithDictionaryProperties))] + [InlineData(nameof(Class_WithDocComments))] + [InlineData(nameof(Class_WithIgnoredProperty))] + [InlineData(nameof(Class_WithInvalidProperties))] + [InlineData(nameof(Class_WithRequiredProperties))] + [InlineData(nameof(Class_WithSchemaOverride))] + [InlineData(nameof(Class_WithTypeSchemaOverride))] + [InlineData(nameof(Class_ExtendsAbstractClass))] + [InlineData(nameof(Record_WithDefaultValueParameter))] + [InlineData(nameof(Record_WithDefaultValueParametersAndConstantProperty))] + [InlineData(nameof(Record_WithDocComments))] + [InlineData(nameof(Record_WithIgnoredParameter))] + [InlineData(nameof(Record_WithSchemaOverride))] + [InlineData(nameof(Record_WithValueParameters))] + [InlineData(nameof(Record_WithValueParametersAndProperty))] + [InlineData(nameof(Record_WithValueParametersAndPropertyInitializer))] + [InlineData(nameof(Struct_WithNullableValueTypes))] + [InlineData(nameof(GameHall))] + public Task Verify_DefaultOptions(string testName) + { + RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + _output.WriteSeparator(); + + return Verify(schemaString, "DefaultOptions", testName); + } + + [Theory] + [InlineData(nameof(Accessibility_Default))] + [InlineData(nameof(Accessibility_ClassOverride))] + [InlineData(nameof(Accessibility_NestedDefault))] + [InlineData(nameof(Accessibility_NestedOverride))] + public Task Verify_AccessibilityOverride(string testName) + { + RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + _output.WriteSeparator(); + + return Verify(schemaString, "AccessibilityOverride", testName); + + } + + [Theory] + [InlineData(nameof(DictionaryKey_Default))] + [InlineData(nameof(DictionaryKey_PropertyOverride))] + [InlineData(nameof(DictionaryKey_NestedOverride))] + public Task Verify_DictionaryKeyOverride(string testName) + { + RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + _output.WriteSeparator(); + + return Verify(schemaString, "DictionaryKeyOverride", testName); + } + + [InlineData(DictionaryKeyMode.Loose)] + [InlineData(DictionaryKeyMode.Strict)] + [InlineData(DictionaryKeyMode.Silent)] + [InlineData(DictionaryKeyMode.Skip)] + [Theory] + public Task Verify_DictionaryKeyMode(DictionaryKeyMode dictionaryKeyMode) + { + GeneratorOptions options = new() + { + DictionaryKeyMode = dictionaryKeyMode + }; + + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithUnsupportedDictionaryKey)); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + + return Verify(schemaString, "DictionaryKeyMode", dictionaryKeyMode.ToString()); + } + + [InlineData(AccessibilityMode.Public)] + [InlineData(AccessibilityMode.Internal)] + [InlineData(AccessibilityMode.Private)] + [InlineData(AccessibilityMode.PublicInternal)] + [Theory] + public Task Verify_AccessibilityMode(AccessibilityMode accessibilities) + { + GeneratorOptions options = new() + { + AccessibilityMode = accessibilities + }; + + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithInternalProperties)); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + + return Verify(schemaString, "AccessibilityMode", accessibilities.ToString()); + } + + [InlineData(EnumMode.String)] + [InlineData(EnumMode.UnderlyingType)] + [Theory] + public Task Verify_EnumMode(EnumMode enumHandling) + { + GeneratorOptions options = new() + { + EnumMode = enumHandling + }; + + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Record_WithAbstractParameters)); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + + return Verify(schemaString, "EnumMode", enumHandling.ToString()); + } + + [InlineData(TraversalMode.SymbolOnly)] + [InlineData(TraversalMode.Bases)] + [InlineData(TraversalMode.Interfaces)] + [InlineData(TraversalMode.Full)] + [Theory] + public Task Verify_Traversal(TraversalMode traversal) + { + GeneratorOptions options = new() + { + TraversalMode = traversal + }; + + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_ExtendsAbstractClass)); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + + return Verify(schemaString, "Traversal", traversal.ToString()); + } + + [InlineData(NumberMode.StrictDefs)] + [InlineData(NumberMode.StrictInline)] + [InlineData(NumberMode.JsonNative)] + [Theory] + public Task Verify_NumberMode(NumberMode numberMode) + { + GeneratorOptions options = new() + { + NumberMode = numberMode + }; + + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); + JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithValueTypes)); + _output.WriteSeparator(); + + string schemaString = builder.Build().SerializeToJson(); + _output.WriteLine(schemaString); + + return Verify(schemaString, "NumberMode", numberMode.ToString()); + } +} diff --git a/test/Generator/SharpSchema.Test.Generator.csproj b/test/Generator/SharpSchema.Test.Generator.csproj index 93fb879..d5de853 100644 --- a/test/Generator/SharpSchema.Test.Generator.csproj +++ b/test/Generator/SharpSchema.Test.Generator.csproj @@ -26,14 +26,10 @@ - + - - - - diff --git a/vscode-diff.runsettings b/vscode-diff.runsettings new file mode 100644 index 0000000..311021e --- /dev/null +++ b/vscode-diff.runsettings @@ -0,0 +1,11 @@ + + + + + VisualStudioCode + false + true + 5 + + +