From 3ef4d8c00fece3318833970d814fcefd5706989f Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:22:55 +0000 Subject: [PATCH 01/11] Generator Epic: Major Refactor --- .editorconfig | 15 +- Directory.Packages.props | 1 + .../AccessibilityMode.cs} | 11 +- .../DictionaryKeyMode.cs | 13 +- .../EnumMode.cs} | 9 +- .../GeneratorOptions.cs | 29 ++ .../SchemaAccessibilityModeAttribute.cs | 21 ++ .../SchemaDictionaryKeyModeAttribute.cs | 21 ++ .../SchemaEnumModeAttribute.cs | 21 ++ .../SchemaTraversalModeAttribute.cs | 21 ++ .../SharpSchema.Annotations.csproj | 6 + .../TraversalMode.cs} | 11 +- src/SharpSchema.Generator/GeneratorOptions.cs | 20 - .../LeafDeclaredTypeSyntaxVisitor.cs | 355 ------------------ .../LeafNodeSymbolVisitor.cs | 0 .../LeafSyntaxVisitor.cs | 349 +++++++++++++++++ src/SharpSchema.Generator/Model/Metadata.cs | 32 +- .../Resolvers/CollectionResolver.cs | 72 ++++ .../EnumSymbolVisitor.cs | 11 +- ...eSyntaxVisitor.cs => RootSyntaxVisitor.cs} | 10 +- .../SharpSchema.Generator.csproj | 4 - .../TypeSchemaSymbolVisitor.cs | 193 ++++++++++ .../Utilities/AttributeDataExtensions.cs | 25 -- .../Utilities/AttributeHandler.cs | 63 ++++ .../Utilities/CollectionExtensions.cs | 16 + .../Utilities/CollectionSymbolVisitor.cs | 98 ----- .../Utilities/CompilationExtensions.cs | 29 ++ .../Utilities/EnumExtensions.cs | 5 +- .../Utilities/GeneratorOptionsExtensions.cs | 11 +- .../Utilities/JsonSchemaBuilderExtensions.cs | 36 +- .../Utilities/SymbolExtensions.cs | 82 ++-- .../Utilities/SyntaxExtensions.cs | 111 +----- src/SharpSchema.Generator/Utilities/Tracer.cs | 25 +- .../AllFeaturesProjectTests.cs | 7 +- ...Record_WithAbstractParameters.verified.txt | 114 ------ ...Mode_dictionaryKeyMode=Strict.verified.txt | 13 - ...mHandling_enumHandling=String.verified.txt | 114 ------ ...g_enumHandling=UnderlyingType.verified.txt | 110 ------ .../TestData.cs | 139 +++++-- .../TestDataFixture.cs | 8 +- ...ties_accessibilities=Internal.verified.txt | 0 ...ities_accessibilities=Private.verified.txt | 0 ...lities_accessibilities=Public.verified.txt | 0 ...ccessibilities=PublicInternal.verified.txt | 0 ...me=Class_ExtendsAbstractClass.verified.txt | 0 ...ame=Class_WithArrayProperties.verified.txt | 4 +- ...lass_WithDictionaryProperties.verified.txt | 4 +- ...estName=Class_WithDocComments.verified.txt | 3 +- ...ame=Class_WithIgnoredProperty.verified.txt | 0 ...=Class_WithInternalProperties.verified.txt | 0 ...e=Class_WithInvalidProperties.verified.txt | 3 +- ...Name=Class_WithSchemaOverride.verified.txt | 0 ...=Class_WithTypeSchemaOverride.verified.txt | 3 +- ..._WithUnsupportedDictionaryKey.verified.txt | 5 +- ...testName=Class_WithValueTypes.verified.txt | 3 +- ...aultOptions_testName=GameHall.verified.txt | 230 ++++++++++++ ...Record_WithAbstractParameters.verified.txt | 76 ++++ ...d_WithGenericAbstractProperty.verified.txt | 84 +++++ ...e=Record_WithIgnoredParameter.verified.txt | 0 ...d_WithParametersAndProperties.verified.txt | 4 +- ...d_WithReferenceTypeParameters.verified.txt | 4 +- ...d_WithReferenceTypeProperties.verified.txt | 10 +- ...ame=Record_WithSchemaOverride.verified.txt | 0 ...ecord_WithValueTypeParameters.verified.txt | 9 +- ...Options_testName=SimpleRecord.verified.txt | 33 ++ ...Struct_WithAbstractProperties.verified.txt | 4 +- ...Struct_WithNullableValueTypes.verified.txt | 3 +- ...yMode_dictionaryKeyMode=Loose.verified.txt | 5 +- ...Mode_dictionaryKeyMode=Silent.verified.txt | 3 +- ...eyMode_dictionaryKeyMode=Skip.verified.txt | 5 + ...ode_dictionaryKeyMode=Strict.verified.txt} | 4 +- ...mHandling_enumHandling=String.verified.txt | 76 ++++ ...g_enumHandling=UnderlyingType.verified.txt | 61 +++ ...ify_Traversal_traversal=Bases.verified.txt | 8 +- ...rify_Traversal_traversal=Full.verified.txt | 8 +- ...raversal_traversal=Interfaces.verified.txt | 0 ...raversal_traversal=SymbolOnly.verified.txt | 0 .../VerifyTests.cs | 65 ++-- .../SharpSchema.Test.Generator.csproj | 6 +- 79 files changed, 1769 insertions(+), 1185 deletions(-) rename src/{SharpSchema.Generator/Accessibilities.cs => SharpSchema.Annotations/AccessibilityMode.cs} (83%) rename src/{SharpSchema.Generator => SharpSchema.Annotations}/DictionaryKeyMode.cs (84%) rename src/{SharpSchema.Generator/EnumHandling.cs => SharpSchema.Annotations/EnumMode.cs} (73%) create mode 100644 src/SharpSchema.Annotations/GeneratorOptions.cs create mode 100644 src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs create mode 100644 src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs create mode 100644 src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs create mode 100644 src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs rename src/{SharpSchema.Generator/Traversal.cs => SharpSchema.Annotations/TraversalMode.cs} (80%) delete mode 100644 src/SharpSchema.Generator/GeneratorOptions.cs delete mode 100644 src/SharpSchema.Generator/LeafDeclaredTypeSyntaxVisitor.cs create mode 100644 src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs create mode 100644 src/SharpSchema.Generator/LeafSyntaxVisitor.cs create mode 100644 src/SharpSchema.Generator/Resolvers/CollectionResolver.cs rename src/SharpSchema.Generator/{Utilities => Resolvers}/EnumSymbolVisitor.cs (78%) rename src/SharpSchema.Generator/{RootDeclaredTypeSyntaxVisitor.cs => RootSyntaxVisitor.cs} (88%) create mode 100644 src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs create mode 100644 src/SharpSchema.Generator/Utilities/CollectionExtensions.cs delete mode 100644 src/SharpSchema.Generator/Utilities/CollectionSymbolVisitor.cs create mode 100644 src/SharpSchema.Generator/Utilities/CompilationExtensions.cs delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt delete mode 100644 test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/TestData.cs (71%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/TestDataFixture.cs (85%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt (96%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt (92%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt (91%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt (50%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt (92%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt (74%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt (98%) create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt (91%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt (93%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt (92%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt (80%) create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt (90%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt (98%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt (74%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt (87%) create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt => RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt} (59%) create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Full.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt (100%) rename test/Generator/{RootDeclaredTypeSyntaxVisitorTests => RootSyntaxVisitorTests}/VerifyTests.cs (68%) 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/Directory.Packages.props b/Directory.Packages.props index 93172cb..344e1ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + 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..8094507 --- /dev/null +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -0,0 +1,29 @@ +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 + +/// +/// The generator options. +/// +/// The accessibility mode. +/// The traversal mode. +/// The dictionary key mode. +/// The enum mode. +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +record GeneratorOptions( + AccessibilityMode AccessibilityMode = AccessibilityMode.Public, + TraversalMode TraversalMode = TraversalMode.SymbolOnly, + DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, + EnumMode EnumMode = EnumMode.String) +{ + /// + /// Gets the default generator options. + /// + public static GeneratorOptions Default { get; } = new GeneratorOptions(); +} diff --git a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs new file mode 100644 index 0000000..05a2ea9 --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs @@ -0,0 +1,21 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default traversal option for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedMembers)] +#if SHARPSCHEMA_ASSEMBLY +public class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute +#else +internal class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute +#endif +{ + /// + /// Gets the accessibility option for the type. + /// + public AccessibilityMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs new file mode 100644 index 0000000..443f823 --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs @@ -0,0 +1,21 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default dictionary key mode for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedMembers)] +#if SHARPSCHEMA_ASSEMBLY +public class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute +#else +internal class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute +#endif +{ + /// + /// 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..bea6df3 --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs @@ -0,0 +1,21 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default enum mode for a given type. +/// +[AttributeUsage(AttributeTargets.Enum)] +#if SHARPSCHEMA_ASSEMBLY +public class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute +#else +internal class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute +#endif +{ + /// + /// Gets the enum mode. + /// + public EnumMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs new file mode 100644 index 0000000..7eeaa4d --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs @@ -0,0 +1,21 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default traversal option for a given type. +/// +[AttributeUsage(SchemaAttribute.SupportedTypes)] +#if SHARPSCHEMA_ASSEMBLY +public class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute +#else +internal class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute +#endif +{ + /// + /// Gets the traversal option for the type. + /// + public TraversalMode Value { get; } = value; +} diff --git a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj index 1a8ef26..e7f9f9c 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/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/LeafNodeSymbolVisitor.cs b/src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs new file mode 100644 index 0000000..95af9b8 --- /dev/null +++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs @@ -0,0 +1,349 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +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 Compilation _compilation; + private readonly GeneratorOptions _options; + private readonly SemanticModelCache _semanticModelCache; + private readonly MemberMeta.SymbolVisitor _metadataVisitor; + private readonly CollectionResolver _collectionResolver; + private readonly Dictionary _cachedTypeSchemas; + private readonly Dictionary _cachedAbstractSymbols; + + public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) + { + _compilation = compilation; + _options = options; + _semanticModelCache = new(compilation); + _metadataVisitor = new(); + _collectionResolver = new(compilation); + _cachedTypeSchemas = []; + _cachedAbstractSymbols = []; + } + + internal GeneratorOptions Options => _options; + + public IReadOnlyDictionary? GetCachedSchemas() + { + if (_cachedAbstractSymbols.Count > 0) + { + using var trace = Tracer.Enter("Building abstract type schemas."); + ImmutableArray namedTypes = [.. _compilation.GetAllNamedTypes(_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 (NamedType 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 = this.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) + return null; + + return _cachedTypeSchemas.ToDictionary( + p => p.Key, + p => p.Value.Build()); + } + + [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(_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)) + { + MemberMeta 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.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(_semanticModelCache); + if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject($"Failed to build schema for identifier '{node.Identifier}'."); + + return this.Visit(typeSymbol.FindDeclaringSyntax()) + ?? CommonSchemas.UnsupportedObject($"Could not find declaration for identifier '{node.Identifier}'."); + } + + public override Builder? VisitGenericName(GenericNameSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope 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."); + + var (kind, keyType, elementSymbol) = _collectionResolver.Resolve(boundTypeSymbol); + if (kind is CollectionKind.Dictionary) + { + if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema)) + { + if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx) + elementSchema = elx.Accept(this); + } + + if (elementSchema is null) + return CommonSchemas.UnsupportedObject($"Failed to build schema for dictionary element type '{elementSymbol}'."); + + Builder builder = CommonSchemas.Object; + + if (keyType is not SchemaValueType.String) + { + switch (_options.DictionaryKeyMode) + { + case DictionaryKeyMode.Skip: + return null; + case DictionaryKeyMode.Strict: + return CommonSchemas.UnsupportedObject($"Key type '{keyType}' is not supported."); + case DictionaryKeyMode.Loose: + builder = builder.Comment($"Key type '{keyType}' must be convertible to string"); + break; + case DictionaryKeyMode.Silent: + break; + default: + throw new InvalidOperationException($"Unexpected dictionary key mode '{_options.DictionaryKeyMode}'."); + } + } + + return builder.AdditionalProperties(elementSchema); + } + + if (kind is CollectionKind.Array) + { + if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema)) + { + if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx) + elementSchema = elx.Accept(this); + } + + if (elementSchema is null) + return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{elementSymbol}'."); + + return CommonSchemas.ArrayOf(elementSchema); + } + + Builder? boundTypeBuilder = this.Visit(boundTypeSymbol.FindDeclaringSyntax()); + if (boundTypeBuilder is null) + return CommonSchemas.UnsupportedObject($"Failed to build schema for generic type '{node.Identifier}'."); + + // Add to the oneOf for the unbound generic type + INamedTypeSymbol unboundGeneric = boundTypeSymbol.ConstructUnboundGenericType(); + string cacheKey = unboundGeneric.GetDefCacheKey(); + if (_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + { + IReadOnlyList? currentOneOf = cachedSchema.Get()?.Schemas; + cachedSchema = currentOneOf is not null + ? cachedSchema.OneOf(currentOneOf.Append(boundTypeBuilder)) + : 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()); + + Builder? elementSchema = this.Visit(node.ElementType); + return elementSchema is null + ? new Builder().Const(null) + : CommonSchemas.Nullable( + Throw.ForUnexpectedNull( + node.ElementType.Accept(this))); + } + + public override Builder? VisitPredefinedType(PredefinedTypeSyntax node) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope 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 Tracer.TraceScope 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); + } + + public Builder CreateTypeSchema(TypeDeclarationSyntax node) + { + Throw.IfNullArgument(node); + + if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol typeSymbol) + return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{node.Identifier}'."); + + return this.CreateTypeSchema(typeSymbol, node); + } + + private Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax node, TraversalMode? traversalMode = null) + { + Throw.IfNullArgument(node); + using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); + + return symbol.Accept( + new TypeSchemaSymbolVisitor( + this, + _semanticModelCache, + _options), + node) + ?? CommonSchemas.UnsupportedObject(symbol.Name); + } + + private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node, Tracer.TraceScope trace) + { + 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) + { + trace.WriteLine($"Found abstract type '{typeSymbol.Name}'."); + if (!_cachedAbstractSymbols.TryGetValue(typeId, out _)) + { + _cachedAbstractSymbols.Add(typeId, typeSymbol); + } + + return CommonSchemas.DefRef(typeId); + } + + AttributeHandler schemaTraversal = typeSymbol.GetAttributeHandler(); + + // Regular concrete type. + Builder builder = CreateTypeSchema(typeSymbol, node, schemaTraversal.Get(0) ?? _options.TraversalMode); + + _cachedTypeSchemas[typeId] = builder; + return CommonSchemas.DefRef(typeId); + } +} diff --git a/src/SharpSchema.Generator/Model/Metadata.cs b/src/SharpSchema.Generator/Model/Metadata.cs index 0de76ce..a797a55 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,33 +40,35 @@ internal class SymbolVisitor : SymbolVisitor private const string ExampleElement = "example"; private const string DeprecatedElement = "deprecated"; + public static SymbolVisitor Default { get; } = new(); + /// - /// 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); @@ -129,7 +131,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/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/Utilities/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs similarity index 78% rename from src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs rename to src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs index a518e0d..a780793 100644 --- a/src/SharpSchema.Generator/Utilities/EnumSymbolVisitor.cs +++ b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs @@ -3,8 +3,9 @@ using Microsoft.CodeAnalysis; using SharpSchema.Annotations; using SharpSchema.Generator.Model; +using SharpSchema.Generator.Utilities; -namespace SharpSchema.Generator.Utilities; +namespace SharpSchema.Generator.Resolvers; using Builder = JsonSchemaBuilder; @@ -23,19 +24,19 @@ private EnumSymbolVisitor() { } if (symbol.TypeKind != TypeKind.Enum) return DefaultResult; - trace.WriteLine($"{options.EnumHandling}"); + trace.WriteLine($"{options.EnumMode}"); - if (options.EnumHandling == EnumHandling.String) + if (options.EnumMode == EnumMode.String) { var names = symbol.GetMembers() .OfType() - .Select(fieldSymbol => (fieldSymbol.GetAttributeHandler()[0] as string) + .Select(fieldSymbol => fieldSymbol.GetAttributeHandler()[0] as string ?? fieldSymbol.Name.Camelize()) .ToList(); return CommonSchemas.String.Enum(names); } - else if (options.EnumHandling == EnumHandling.UnderlyingType) + else if (options.EnumMode == EnumMode.UnderlyingType) { trace.WriteLine("Underlying type enum handling."); diff --git a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs b/src/SharpSchema.Generator/RootSyntaxVisitor.cs similarity index 88% rename from src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs rename to src/SharpSchema.Generator/RootSyntaxVisitor.cs index 0cf88b6..ec9b4f9 100644 --- a/src/SharpSchema.Generator/RootDeclaredTypeSyntaxVisitor.cs +++ b/src/SharpSchema.Generator/RootSyntaxVisitor.cs @@ -11,16 +11,16 @@ namespace SharpSchema.Generator; /// /// Visits C# syntax nodes to generate JSON schema builders. /// -public class RootDeclaredTypeSyntaxVisitor : CSharpSyntaxVisitor +public class RootSyntaxVisitor : CSharpSyntaxVisitor { - private readonly LeafDeclaredTypeSyntaxVisitor _cachingVisitor; + private readonly LeafSyntaxVisitor _cachingVisitor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The compilation context. /// The generator options. - public RootDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions options) + public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) { _cachingVisitor = new(compilation, options); } @@ -94,7 +94,7 @@ public RootDeclaredTypeSyntaxVisitor(Compilation compilation, GeneratorOptions o /// A JSON schema builder or null. private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node) { - Builder builder = node.CreateTypeSchema(_cachingVisitor); + Builder builder = _cachingVisitor.CreateTypeSchema(node); if (_cachingVisitor.GetCachedSchemas() is IReadOnlyDictionary defs) { 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/TypeSchemaSymbolVisitor.cs b/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs new file mode 100644 index 0000000..1074e78 --- /dev/null +++ b/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs @@ -0,0 +1,193 @@ +using Microsoft.CodeAnalysis.CSharp; +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; + +internal class TypeSchemaSymbolVisitor : SymbolVisitor +{ + private readonly CSharpSyntaxVisitor _syntaxVisitor; + private readonly SemanticModelCache _semanticModelCache; + private readonly GeneratorOptions _options; + + public TypeSchemaSymbolVisitor( + CSharpSyntaxVisitor visitor, + SemanticModelCache semanticModelCache, + GeneratorOptions options) + { + _syntaxVisitor = visitor; + _semanticModelCache = semanticModelCache; + _options = options; + } + + protected override Builder? DefaultResult { get; } + + public override Builder? VisitNamedType(INamedTypeSymbol symbol, BaseTypeDeclarationSyntax argument) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); + HashSet _requiredProperties = new(StringComparer.OrdinalIgnoreCase); + + if (symbol.IsRecord) + { + IMethodSymbol primaryCtor = symbol.Constructors.First(); + primaryCtor.Parameters.ForEach(param => + { + Builder? typeBuilder = param.Accept(this, argument); + if (typeBuilder is null) + return; + + string propertyName = param.Name.Camelize(); + properties.Add(propertyName, typeBuilder); + + bool isNullable = param.NullableAnnotation switch + { + NullableAnnotation.NotAnnotated => false, + NullableAnnotation.Annotated => true, + NullableAnnotation.None => false, // TODO: Make Configurable + _ => throw new NotSupportedException() + }; + + bool isRequired = !isNullable; + if (param.HasExplicitDefaultValue) + isRequired = false; + + if (EvaluateSchemaRequired(param, isRequired)) + _requiredProperties.Add(propertyName); + }); + } + + symbol.GetMembers().OfType().ForEach(prop => + { + Builder? valueBuilder = prop.Accept(this, argument); + if (valueBuilder is null) + return; + + string propertyName = prop.Name.Camelize(); + properties.Add(propertyName, valueBuilder); + + bool isNullable = prop.NullableAnnotation switch + { + NullableAnnotation.NotAnnotated => false, + NullableAnnotation.Annotated => true, + NullableAnnotation.None => false, // TODO: Make Configurable + _ => throw new NotSupportedException() + }; + + bool isRequired = !isNullable; + if (EvaluateSchemaRequired(prop, isRequired)) + _requiredProperties.Add(propertyName); + }); + + Builder builder = CommonSchemas.Object; + if (properties.Count > 0) + builder = builder.Properties(properties); + + if (_requiredProperties.Count > 0) + builder = builder.Required(_requiredProperties); + + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + builder = builder.ApplyMemberMeta(meta); + + return builder; + + static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) + { + AttributeHandler schemaRequired = prop.GetAttributeHandler(); + if (schemaRequired[0] is bool overrideRequired) + return overrideRequired; + + return isRequired; + } + } + + public override Builder? VisitProperty(IPropertySymbol symbol, BaseTypeDeclarationSyntax argument) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) + return null; + + // Excludes generated properties. + if (symbol.FindDeclaringSyntax() is PropertyDeclarationSyntax pdx) + { + Builder? typeBuilder = pdx.Type.Accept(_syntaxVisitor); + if (typeBuilder is null) + return null; + + if (pdx.ExpressionBody is ArrowExpressionClauseSyntax aec + && GetConstantValue(aec.Expression) is JsonNode constantValue) + { + typeBuilder = CommonSchemas.Object.Const(constantValue); + } + else if (ExtractDefaultValue(pdx) is JsonNode defaultValue) + { + typeBuilder = typeBuilder.Default(defaultValue); + } + + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); + + return typeBuilder; + } + + return null; + + // -- Local functions -- + + JsonNode? ExtractDefaultValue(PropertyDeclarationSyntax propertyDeclaration) + { + return propertyDeclaration.Initializer is EqualsValueClauseSyntax evc + ? GetConstantValue(evc.Value) + : null; + } + + JsonNode? GetConstantValue(SyntaxNode node) + { + SemanticModel sm = _semanticModelCache.GetSemanticModel(node); + return sm.GetConstantValue(node) is Optional optValue + && optValue.HasValue + ? JsonValue.Create(optValue.Value) + : (JsonNode?)null; + } + } + + public override Builder? VisitParameter(IParameterSymbol symbol, BaseTypeDeclarationSyntax argument) + { + using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + + if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) + return null; + + // Excludes implicitly-typed parameters. + if (symbol.FindDeclaringSyntax() is ParameterSyntax px && px.Type is TypeSyntax tx) + { + Builder? typeBuilder = px.Type.Accept(_syntaxVisitor); + if (typeBuilder is null) + return null; + + if (symbol.HasExplicitDefaultValue + && symbol.ExplicitDefaultValue is object edv + && JsonValue.Create(edv) is JsonNode defaultValue) + { + typeBuilder = typeBuilder.Default(defaultValue); + } + + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); + + return typeBuilder; + } + + return null; + } +} 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..a305005 100644 --- a/src/SharpSchema.Generator/Utilities/AttributeHandler.cs +++ b/src/SharpSchema.Generator/Utilities/AttributeHandler.cs @@ -45,6 +45,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/CompilationExtensions.cs b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs new file mode 100644 index 0000000..a4acebb --- /dev/null +++ b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Generator.Model; + +namespace SharpSchema.Generator.Utilities; + +internal readonly record struct NamedType(TypeDeclarationSyntax SyntaxNode, INamedTypeSymbol Symbol); + +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/GeneratorOptionsExtensions.cs b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs index 806ede5..01b1dc5 100644 --- a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using SharpSchema.Annotations; namespace SharpSchema.Generator.Utilities; @@ -17,16 +18,16 @@ 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, }; } diff --git a/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs b/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs index dffe249..82064c5 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,43 +51,9 @@ public static JsonSchemaBuilder ApplySchema(this JsonSchema @base, JsonSchema ap return builder; } - public static JsonSchemaBuilder MergeBaseProperties(this JsonSchemaBuilder builder, JsonSchema baseSchema) - { - using var trace = Tracer.Enter($"{baseSchema.BaseUri}"); - - IReadOnlyDictionary baseProperties = builder.Get()?.Properties ?? new Dictionary(); - IReadOnlyDictionary otherProperties = baseSchema.GetProperties() ?? new Dictionary(); - - Dictionary mergedProperties = new((IDictionary)otherProperties, StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair pair in baseProperties) - { - if (mergedProperties.TryGetValue(pair.Key, out JsonSchema? value)) - mergedProperties[pair.Key] = pair.Value.ApplySchema(value); - else - mergedProperties.Add(pair.Key, pair.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); - - return builder - .Properties(mergedProperties) - .Required(mergedRequiredProperties); - } - public static JsonSchemaBuilder UnsupportedObject(this JsonSchemaBuilder builder, string value) { builder.Add(new UnsupportedObjectKeyword(value)); return builder; } - - public static UnsupportedObjectKeyword? GetUnsupportedObject(this JsonSchemaBuilder builder) - { - return builder.Get(); - } } diff --git a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs index 1624be4..f5d5ed5 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Json.Schema; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using SharpSchema.Annotations; using SharpSchema.Generator.Model; namespace SharpSchema.Generator.Utilities; @@ -11,7 +11,7 @@ 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)); @@ -24,7 +24,7 @@ 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 { // Search for the attribute on the symbol itself @@ -36,7 +36,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; @@ -52,7 +52,7 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave } } - if (traversal.CheckFlag(Traversal.Interfaces)) + if (traversal.CheckFlag(TraversalMode.Interfaces)) { // Search on interfaces foreach (INamedTypeSymbol interfaceType in namedTypeSymbol.AllInterfaces) @@ -69,17 +69,6 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave return null; } - public static TValue? GetAttributeConstructorArgument(this ISymbol symbol, int argumentIndex, Traversal traversal = Traversal.SymbolOnly) - where TAttribute : Attribute - where TValue : notnull - { - AttributeData? attributeData = symbol.GetAttributeData(traversal); - if (attributeData is null) - return default; - - return attributeData.GetConstructorArgument(argumentIndex); - } - public static bool MatchesType(this INamedTypeSymbol typeSymbol) { const string globalPrefix = "global::"; @@ -114,7 +103,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 +113,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 +128,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 +178,40 @@ 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) { + using var trace = Tracer.Enter(symbol.Name); if (symbol.SpecialType == SpecialType.None) { + trace.WriteLine("Symbol is not a special type."); schema = null; return false; } 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/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/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_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_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/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs similarity index 71% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestData.cs rename to test/Generator/RootSyntaxVisitorTests/TestData.cs index 8e07764..7b9be5f 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Text.Json.Serialization; using SharpSchema.Annotations; @@ -8,6 +9,11 @@ namespace SharpSchema.Generator.TestData; #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 SimpleRecord(string Name, int Age = 42) +{ + public string? Description { get; set; } +} + public class Class_WithDocComments { /// @@ -72,8 +78,6 @@ public class Class_WithInvalidProperties public string Name { set { } } public static string Static { get; set; } - - public virtual string Virtual { get; set; } } public class Class_WithDictionaryProperties @@ -240,10 +244,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 +293,122 @@ 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 +[SchemaTraversalMode(TraversalMode.Bases)] +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) +[SchemaTraversalMode(TraversalMode.Bases)] +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 - } +[SchemaTraversalMode(TraversalMode.Bases)] +public record BlackjackTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Blackjack DealerHand { get; } +} + +[SchemaTraversalMode(TraversalMode.Bases)] +public record BridgeTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Bridge? DealerHand => null; +} - public abstract FaceKind Face { get; } +public record WhistTable(int PlayerCount) : Table(PlayerCount) +{ + public override BaseHand.Bridge? DealerHand => null; +} + +public abstract record BaseHand(int Size) +{ + public abstract string Game { get; } + + public List Cards { get; set; } - public record AceOfSpades() : Card(SuitKind.Spades, 1) + [SchemaTraversalMode(TraversalMode.Bases)] + 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) + [SchemaTraversalMode(TraversalMode.Bases)] + 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) + [SchemaTraversalMode(TraversalMode.Bases)] + 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 JackOfSpades() : Card(SuitKind.Spades, 11) +public record Card(Card.SuitKind Suit, Card.RankKind Rank) +{ + public bool IsFaceCard => Rank >= RankKind.Jack; + + 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 85% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestDataFixture.cs rename to test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs index 98571b0..d0b703a 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/TestDataFixture.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs @@ -14,7 +14,7 @@ using System.Collections.Immutable; using System.Text.Json.Serialization; -namespace SharpSchema.Test.Generator.RootDeclaredTypeSyntaxVisitorTests; +namespace SharpSchema.Test.Generator.RootSyntaxVisitorTests; public class TestDataFixture { @@ -27,7 +27,7 @@ public TestDataFixture() string pathToTestData = PathHelper.GetRepoPath( "test", "Generator", - "RootDeclaredTypeSyntaxVisitorTests", + "RootSyntaxVisitorTests", "TestData.cs"); // Create an array of syntax tree from all cs files in src/SharpSchema.Annotations/ @@ -45,9 +45,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/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt similarity index 96% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt index 663796d..aa8510c 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt @@ -78,6 +78,7 @@ "numbersList", "stringImmutableArray" ], + "title": "Class Witharrayproperties", "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", @@ -94,7 +95,8 @@ "required": [ "street", "city" - ] + ], + "title": "Address" } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt similarity index 92% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt index 10275ce..e47a808 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt @@ -24,6 +24,7 @@ "valueTypes", "referenceTypes" ], + "title": "Class Withdictionaryproperties", "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", @@ -40,7 +41,8 @@ "required": [ "street", "city" - ] + ], + "title": "Address" } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt similarity index 91% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt index fdb32bb..2f95fe2 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt @@ -18,5 +18,6 @@ "required": [ "name", "age" - ] + ], + "title": "Class Withdoccomments" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt similarity index 50% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt index 95c99cb..d1faa8c 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt @@ -1,4 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object" + "type": "object", + "title": "Class Withinvalidproperties" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt similarity index 92% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt index 505a1e8..b704747 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt @@ -19,5 +19,6 @@ "required": [ "badOverride", "goodOverride" - ] + ], + "title": "Class Withtypeschemaoverride" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt similarity index 74% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt index b92a62e..4171c73 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt @@ -4,7 +4,7 @@ "properties": { "data": { "type": "object", - "$comment": "Ensure key type 'Address' is convertible to string.", + "$comment": "Key type 'Object' must be convertible to string", "additionalProperties": { "type": "integer", "minimum": -2147483648, @@ -16,5 +16,6 @@ }, "required": [ "data" - ] + ], + "title": "Class Withunsupporteddictionarykey" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt similarity index 98% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt index 02535bb..09f0930 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt @@ -110,5 +110,6 @@ "double", "decimal", "char" - ] + ], + "title": "Class Withvaluetypes" } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt new file mode 100644 index 0000000..164d76f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt @@ -0,0 +1,230 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "rooms": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.GameRoom" + }, + "title": "Rooms" + } + }, + "required": [ + "name", + "rooms" + ], + "title": "Game Hall", + "$defs": { + "T:SharpSchema.Generator.TestData.BaseHand.Poker": { + "type": "object", + "properties": { + "game": { + "type": "object", + "const": "Poker", + "title": "Game" + }, + "isRoyalFlush": { + "type": "boolean", + "title": "Is Royal Flush" + } + }, + "required": [ + "game", + "isRoyalFlush" + ], + "title": "Poker" + }, + "T:SharpSchema.Generator.TestData.PokerTable": { + "type": "object", + "properties": { + "playerCount": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Player Count" + }, + "dealerHand": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Poker", + "title": "Dealer Hand" + } + }, + "required": [ + "playerCount", + "dealerHand" + ], + "title": "Poker Table" + }, + "T:SharpSchema.Generator.TestData.BaseHand.Blackjack": { + "type": "object", + "properties": { + "game": { + "type": "object", + "const": "Blackjack", + "title": "Game" + }, + "value": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Value" + } + }, + "required": [ + "game", + "value" + ], + "title": "Blackjack" + }, + "T:SharpSchema.Generator.TestData.BlackjackTable": { + "type": "object", + "properties": { + "playerCount": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Player Count" + }, + "dealerHand": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Blackjack", + "title": "Dealer Hand" + } + }, + "required": [ + "playerCount", + "dealerHand" + ], + "title": "Blackjack Table" + }, + "T:SharpSchema.Generator.TestData.BaseHand.Bridge": { + "type": "object", + "properties": { + "game": { + "type": "object", + "const": "Bridge", + "title": "Game" + }, + "isNoTrump": { + "type": "boolean", + "title": "Is No Trump" + } + }, + "required": [ + "game", + "isNoTrump" + ], + "title": "Bridge" + }, + "T:SharpSchema.Generator.TestData.BridgeTable": { + "type": "object", + "properties": { + "playerCount": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Player Count" + }, + "dealerHand": { + "oneOf": [ + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" + }, + { + "type": "null" + } + ], + "title": "Dealer Hand" + } + }, + "required": [ + "playerCount" + ], + "title": "Bridge Table" + }, + "T:SharpSchema.Generator.TestData.GameRoom": { + "type": "object", + "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" + ], + "title": "Game Room" + }, + "T:SharpSchema.Generator.TestData.WhistTable": { + "type": "object", + "properties": { + "playerCount": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Player Count" + }, + "dealerHand": { + "oneOf": [ + { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" + }, + { + "type": "null" + } + ], + "title": "Dealer Hand" + } + }, + "required": [ + "playerCount" + ], + "title": "Whist Table" + }, + "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" + } + ] + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt new file mode 100644 index 0000000..9cbde1b --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt @@ -0,0 +1,76 @@ +{ + "$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" + ], + "title": "Record Withabstractparameters", + "$defs": { + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit Kind" + }, + "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": { + "type": "object", + "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" + ], + "title": "Card" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt new file mode 100644 index 0000000..507e0fc --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "stack": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.MagicStack", + "title": "Stack" + } + }, + "required": [ + "stack" + ], + "title": "Record Withgenericabstractproperty", + "$defs": { + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit Kind" + }, + "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": { + "type": "object", + "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" + ], + "title": "Card" + }, + "T:SharpSchema.Generator.TestData.MagicStack": { + "type": "object", + "properties": { + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" + } + }, + "required": [ + "cards" + ], + "title": "Magic Stack" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt similarity index 91% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt index 9b06269..c8a3405 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt @@ -23,6 +23,7 @@ "age", "address" ], + "title": "Record Withparametersandproperties", "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", @@ -39,7 +40,8 @@ "required": [ "street", "city" - ] + ], + "title": "Address" } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt similarity index 93% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt index 284ac24..d7f2c73 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt @@ -38,6 +38,7 @@ "required": [ "address" ], + "title": "Record Withreferencetypeparameters", "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", @@ -54,7 +55,8 @@ "required": [ "street", "city" - ] + ], + "title": "Address" } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt similarity index 92% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt index 29c3c1e..65943bb 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt @@ -23,6 +23,7 @@ "age", "person" ], + "title": "Record Withreferencetypeproperties", "$defs": { "T:SharpSchema.Generator.TestData.Address": { "type": "object", @@ -39,7 +40,8 @@ "required": [ "street", "city" - ] + ], + "title": "Address" }, "T:SharpSchema.Generator.TestData.Office": { "type": "object", @@ -56,7 +58,8 @@ "required": [ "name", "address" - ] + ], + "title": "Office" }, "T:SharpSchema.Generator.TestData.Person": { "type": "object", @@ -81,7 +84,8 @@ "name", "age", "office" - ] + ], + "title": "Person" } } } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt similarity index 80% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt index f905db0..75e1650 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt @@ -15,15 +15,20 @@ "minimum": -2147483648, "maximum": 2147483647, "$comment": "System.Int32", + "default": 42, "title": "NameOfRecord", "description": "The record's name.", "examples": [ "42" - ], - "default": 42 + ] } }, "required": [ "name" + ], + "title": "NameOfRecord", + "description": "The record's name.", + "examples": [ + "John Doe" ] } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt new file mode 100644 index 0000000..1157b93 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt @@ -0,0 +1,33 @@ +{ + "$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", + "default": 42, + "title": "Age" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "required": [ + "name" + ], + "title": "Simple Record" +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt similarity index 90% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt index 3f36192..0e42cbf 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt @@ -15,6 +15,7 @@ "abstract", "concrete" ], + "title": "Struct Withabstractproperties", "$defs": { "T:SharpSchema.Generator.TestData.Class_ExtendsAbstractClass": { "type": "object", @@ -26,7 +27,8 @@ }, "required": [ "name" - ] + ], + "title": "Class Extendsabstractclass" }, "T:SharpSchema.Generator.TestData.AbstractClass": { "type": "object", diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt similarity index 98% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt index 808498b..c2ff213 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt @@ -181,5 +181,6 @@ ], "title": "Char" } - } + }, + "title": "Struct Withnullablevaluetypes" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt similarity index 74% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt index b92a62e..4171c73 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt @@ -4,7 +4,7 @@ "properties": { "data": { "type": "object", - "$comment": "Ensure key type 'Address' is convertible to string.", + "$comment": "Key type 'Object' must be convertible to string", "additionalProperties": { "type": "integer", "minimum": -2147483648, @@ -16,5 +16,6 @@ }, "required": [ "data" - ] + ], + "title": "Class Withunsupporteddictionarykey" } \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt similarity index 87% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt index 10242cd..bfc4ea4 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt @@ -15,5 +15,6 @@ }, "required": [ "data" - ] + ], + "title": "Class Withunsupporteddictionarykey" } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt new file mode 100644 index 0000000..6b119f4 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class Withunsupporteddictionarykey" +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt similarity index 59% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt index 10802ee..a86427b 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt @@ -3,10 +3,12 @@ "type": "object", "properties": { "data": { + "$unsupportedObject": "Key type 'Object' is not supported.", "title": "Data" } }, "required": [ "data" - ] + ], + "title": "Class Withunsupporteddictionarykey" } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt new file mode 100644 index 0000000..9cbde1b --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt @@ -0,0 +1,76 @@ +{ + "$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" + ], + "title": "Record Withabstractparameters", + "$defs": { + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit Kind" + }, + "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": { + "type": "object", + "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" + ], + "title": "Card" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt new file mode 100644 index 0000000..7a6e231 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt @@ -0,0 +1,61 @@ +{ + "$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" + ], + "title": "Record Withabstractparameters", + "$defs": { + "T:SharpSchema.Generator.TestData.Card.SuitKind": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Suit Kind" + }, + "T:SharpSchema.Generator.TestData.Card.RankKind": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "$comment": "System.Int32", + "title": "Rank Kind" + }, + "T:SharpSchema.Generator.TestData.Card": { + "type": "object", + "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" + ], + "title": "Card" + } + } +} \ No newline at end of file diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt index 8354a29..1b5fd4c 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt @@ -2,16 +2,16 @@ "$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" - }, - "name": { - "type": "string", - "title": "Name" } }, "required": [ diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt index 8354a29..1b5fd4c 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt @@ -2,16 +2,16 @@ "$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" - }, - "name": { - "type": "string", - "title": "Name" } }, "required": [ diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt similarity index 100% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt diff --git a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs similarity index 68% rename from test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs rename to test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index a3a4702..5350be1 100644 --- a/test/Generator/RootDeclaredTypeSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -1,6 +1,10 @@ using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Schema; using System.Threading.Tasks; using Json.Schema; +using SharpSchema.Annotations; using SharpSchema.Generator; using SharpSchema.Generator.TestData; using SharpSchema.Generator.Utilities; @@ -9,7 +13,7 @@ using Xunit; using Xunit.Abstractions; -namespace SharpSchema.Test.Generator.RootDeclaredTypeSyntaxVisitorTests; +namespace SharpSchema.Test.Generator.RootSyntaxVisitorTests; public class VerifyTests : IDisposable, IClassFixture { @@ -18,14 +22,18 @@ public class VerifyTests : IDisposable, IClassFixture public VerifyTests(TestDataFixture fixture, ITestOutputHelper outputHelper) { + ArgumentNullException.ThrowIfNull(outputHelper); + _fixture = fixture; Tracer.Writer = outputHelper.WriteLine; + Tracer.EnableTiming = true; _output = outputHelper; } public void Dispose() => Tracer.Writer = null; [Theory] + [InlineData(nameof(SimpleRecord))] [InlineData(nameof(Struct_WithNullableValueTypes))] [InlineData(nameof(Struct_WithAbstractProperties))] [InlineData(nameof(Record_WithReferenceTypeProperties))] @@ -46,14 +54,25 @@ public VerifyTests(TestDataFixture fixture, ITestOutputHelper outputHelper) [InlineData(nameof(Class_WithArrayProperties))] [InlineData(nameof(Class_WithInvalidProperties))] [InlineData(nameof(Class_ExtendsAbstractClass))] - [InlineData(nameof(Record_WithGenericAbstractProperty), Skip = "Not Yet Implemented")] + [InlineData(nameof(Record_WithGenericAbstractProperty))] + [InlineData(nameof(GameHall))] public Task Verify_DefaultOptions(string testName) { - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); + RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); _output.WriteSeparator(); string schemaString = builder.Build().SerializeToJson(); _output.WriteLine(schemaString); + _output.WriteSeparator(); + + // Get type instance from test name + Type? type = Assembly.GetExecutingAssembly().GetType($"SharpSchema.Generator.TestData.{testName}"); + if (type is not null) + { + System.Text.Json.Nodes.JsonNode n = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, type, JsonSchemaExporterOptions.Default); + _output.WriteLine(n.ToString()); + } + return Verifier.Verify(schemaString).UseParameters(testName); } @@ -69,7 +88,7 @@ public Task Verify_DictionaryKeyMode(DictionaryKeyMode dictionaryKeyMode) DictionaryKeyMode = dictionaryKeyMode }; - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithUnsupportedDictionaryKey)); _output.WriteSeparator(); string schemaString = builder.Build().SerializeToJson(); @@ -77,19 +96,19 @@ public Task Verify_DictionaryKeyMode(DictionaryKeyMode dictionaryKeyMode) return Verifier.Verify(schemaString).UseParameters(dictionaryKeyMode); } - [InlineData(Accessibilities.Public)] - [InlineData(Accessibilities.Internal)] - [InlineData(Accessibilities.Private)] - [InlineData(Accessibilities.PublicInternal)] + [InlineData(AccessibilityMode.Public)] + [InlineData(AccessibilityMode.Internal)] + [InlineData(AccessibilityMode.Private)] + [InlineData(AccessibilityMode.PublicInternal)] [Theory] - public Task Verify_Accessibilities(Accessibilities accessibilities) + public Task Verify_Accessibilities(AccessibilityMode accessibilities) { GeneratorOptions options = new() { - Accessibilities = accessibilities + AccessibilityMode = accessibilities }; - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_WithInternalProperties)); _output.WriteSeparator(); string schemaString = builder.Build().SerializeToJson(); @@ -97,16 +116,16 @@ public Task Verify_Accessibilities(Accessibilities accessibilities) return Verifier.Verify(schemaString).UseParameters(accessibilities); } - [InlineData(EnumHandling.String)] - [InlineData(EnumHandling.UnderlyingType)] + [InlineData(EnumMode.String)] + [InlineData(EnumMode.UnderlyingType)] [Theory] - public Task Verify_EnumHandling(EnumHandling enumHandling) + public Task Verify_EnumHandling(EnumMode enumHandling) { GeneratorOptions options = new() { - EnumHandling = enumHandling + EnumMode = enumHandling }; - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Record_WithAbstractParameters)); _output.WriteSeparator(); string schemaString = builder.Build().SerializeToJson(); @@ -114,19 +133,19 @@ public Task Verify_EnumHandling(EnumHandling enumHandling) return Verifier.Verify(schemaString).UseParameters(enumHandling); } - [InlineData(Traversal.SymbolOnly)] - [InlineData(Traversal.Bases)] - [InlineData(Traversal.Interfaces)] - [InlineData(Traversal.Full)] + [InlineData(TraversalMode.SymbolOnly)] + [InlineData(TraversalMode.Bases)] + [InlineData(TraversalMode.Interfaces)] + [InlineData(TraversalMode.Full)] [Theory] - public Task Verify_Traversal(Traversal traversal) + public Task Verify_Traversal(TraversalMode traversal) { GeneratorOptions options = new() { - Traversal = traversal + TraversalMode = traversal }; - RootDeclaredTypeSyntaxVisitor visitor = _fixture.GetVisitor(options); + RootSyntaxVisitor visitor = _fixture.GetVisitor(options); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, nameof(Class_ExtendsAbstractClass)); _output.WriteSeparator(); string schemaString = builder.Build().SerializeToJson(); 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 @@ - + - - - - From ab317482154ea39fe6a1f47a07a1282e71a112ec Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:11:36 +0000 Subject: [PATCH 02/11] Enhance GeneratorOptions and update attribute access Improved documentation for parameters in `GeneratorOptions.cs` and added a new `NumberMode` parameter with default value `StrictDefs`. Introduced `NumberMode.cs` with an enumeration defining `StrictDefs`, `StrictInline`, and `JsonNative`. Updated access modifiers from `public` to `internal` for various attributes in the `SharpSchema.Annotations` namespace, along with enhanced documentation for clarity. --- .../GeneratorOptions.cs | 14 +++++---- src/SharpSchema.Annotations/NumberMode.cs | 29 +++++++++++++++++++ .../SchemaAccessibilityModeAttribute.cs | 5 ++-- .../SchemaConstAttribute.cs | 5 ++-- .../SchemaDictionaryKeyModeAttribute.cs | 5 ++-- .../SchemaEnumModeAttribute.cs | 5 ++-- .../SchemaEnumValueAttribute.cs | 5 ++-- .../SchemaFormatAttribute.cs | 5 ++-- .../SchemaIgnoreAttribute.cs | 5 ++-- .../SchemaItemsRangeAttribute.cs | 5 ++-- .../SchemaLengthRangeAttribute.cs | 5 ++-- .../SchemaMetaAttribute.cs | 5 ++-- .../SchemaNumberModeAttribute.cs | 22 ++++++++++++++ .../SchemaOverrideAttribute.cs | 5 ++-- .../SchemaPropertiesRangeAttribute.cs | 5 ++-- .../SchemaRegexAttribute.cs | 5 ++-- .../SchemaRequiredAttribute.cs | 5 ++-- .../SchemaRootAttribute.cs | 5 ++-- .../SchemaTraversalModeAttribute.cs | 5 ++-- .../SchemaValueRangeAttribute.cs | 5 ++-- 20 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 src/SharpSchema.Annotations/NumberMode.cs create mode 100644 src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs diff --git a/src/SharpSchema.Annotations/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs index 8094507..5ae058d 100644 --- a/src/SharpSchema.Annotations/GeneratorOptions.cs +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -5,12 +5,13 @@ namespace SharpSchema.Generator; #pragma warning restore IDE0130 // Namespace does not match folder structure /// -/// The generator options. +/// Represents the options for the generator. /// -/// The accessibility mode. -/// The traversal mode. -/// The dictionary key mode. -/// The enum mode. +/// Specifies the accessibility mode. +/// Specifies the traversal mode. +/// Specifies the dictionary key mode. +/// Specifies the enum mode. +/// Specifies the number mode. #if SHARPSCHEMA_ASSEMBLY public #else @@ -20,7 +21,8 @@ record GeneratorOptions( AccessibilityMode AccessibilityMode = AccessibilityMode.Public, TraversalMode TraversalMode = TraversalMode.SymbolOnly, DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, - EnumMode EnumMode = EnumMode.String) + EnumMode EnumMode = EnumMode.String, + NumberMode NumberMode = NumberMode.StrictDefs) { /// /// Gets the default generator options. 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 index 05a2ea9..b51fa67 100644 --- a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SchemaAttribute.SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute +public #else -internal class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute +internal #endif +class SchemaAccessibilityModeAttribute(AccessibilityMode value) : SchemaAttribute { /// /// Gets the accessibility option for the type. 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 index 443f823..3740bfc 100644 --- a/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaDictionaryKeyModeAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SchemaAttribute.SupportedMembers)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute +public #else -internal class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute +internal #endif +class SchemaDictionaryKeyModeAttribute(DictionaryKeyMode value) : SchemaAttribute { /// /// Gets the dictionary key mode. diff --git a/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs index bea6df3..87e8b8f 100644 --- a/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaEnumModeAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(AttributeTargets.Enum)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute +public #else -internal class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute +internal #endif +class SchemaEnumModeAttribute(EnumMode value) : SchemaAttribute { /// /// Gets the enum mode. 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/SchemaNumberModeAttribute.cs b/src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs new file mode 100644 index 0000000..bd2959e --- /dev/null +++ b/src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace SharpSchema.Annotations; + +/// +/// Overrides the default number mode for a given type. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Parameter)] +#if SHARPSCHEMA_ASSEMBLY +public +#else +internal +#endif +class SchemaNumberModeAttribute(NumberMode value) : SchemaAttribute +{ + /// + /// Gets the number mode. + /// + public NumberMode Value { get; } = value; +} 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 index 7eeaa4d..7474f56 100644 --- a/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaTraversalModeAttribute.cs @@ -9,10 +9,11 @@ namespace SharpSchema.Annotations; /// [AttributeUsage(SchemaAttribute.SupportedTypes)] #if SHARPSCHEMA_ASSEMBLY -public class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute +public #else -internal class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute +internal #endif +class SchemaTraversalModeAttribute(TraversalMode value) : SchemaAttribute { /// /// Gets the traversal option for the type. 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. From 3d1f422e14b763ec446948126d6e06827269ef66 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:21:41 +0000 Subject: [PATCH 03/11] Remove JSON schema definitions from verified tests --- ...ties_accessibilities=Internal.verified.txt | 13 - ...ities_accessibilities=Private.verified.txt | 13 - ...lities_accessibilities=Public.verified.txt | 4 - ...ccessibilities=PublicInternal.verified.txt | 13 - ...me=Class_ExtendsAbstractClass.verified.txt | 13 - ...ame=Class_WithArrayProperties.verified.txt | 102 -------- ...lass_WithDictionaryProperties.verified.txt | 48 ---- ...estName=Class_WithDocComments.verified.txt | 23 -- ...ame=Class_WithIgnoredProperty.verified.txt | 13 - ...=Class_WithInternalProperties.verified.txt | 4 - ...e=Class_WithInvalidProperties.verified.txt | 5 - ...Name=Class_WithSchemaOverride.verified.txt | 18 -- ...=Class_WithTypeSchemaOverride.verified.txt | 24 -- ..._WithUnsupportedDictionaryKey.verified.txt | 21 -- ...testName=Class_WithValueTypes.verified.txt | 115 --------- ...aultOptions_testName=GameHall.verified.txt | 230 ------------------ ...Record_WithAbstractParameters.verified.txt | 76 ------ ...d_WithGenericAbstractProperty.verified.txt | 84 ------- ...e=Record_WithIgnoredParameter.verified.txt | 13 - ...d_WithParametersAndProperties.verified.txt | 47 ---- ...d_WithReferenceTypeParameters.verified.txt | 62 ----- ...d_WithReferenceTypeProperties.verified.txt | 91 ------- ...ame=Record_WithSchemaOverride.verified.txt | 18 -- ...ecord_WithValueTypeParameters.verified.txt | 34 --- ...Options_testName=SimpleRecord.verified.txt | 33 --- ...Struct_WithAbstractProperties.verified.txt | 42 ---- ...Struct_WithNullableValueTypes.verified.txt | 186 -------------- ...yMode_dictionaryKeyMode=Loose.verified.txt | 21 -- ...Mode_dictionaryKeyMode=Silent.verified.txt | 20 -- ...eyMode_dictionaryKeyMode=Skip.verified.txt | 5 - ...Mode_dictionaryKeyMode=Strict.verified.txt | 14 -- ...mHandling_enumHandling=String.verified.txt | 76 ------ ...g_enumHandling=UnderlyingType.verified.txt | 61 ----- ...ify_Traversal_traversal=Bases.verified.txt | 21 -- ...rify_Traversal_traversal=Full.verified.txt | 21 -- ...raversal_traversal=Interfaces.verified.txt | 13 - ...raversal_traversal=SymbolOnly.verified.txt | 13 - 37 files changed, 1610 deletions(-) delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt delete mode 100644 test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt deleted file mode 100644 index 68fb175..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Internal.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "internal": { - "type": "string", - "title": "Internal" - } - }, - "required": [ - "internal" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt deleted file mode 100644 index c84de0f..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Private.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "private": { - "type": "string", - "title": "Private" - } - }, - "required": [ - "private" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=Public.verified.txt deleted file mode 100644 index 95c99cb..0000000 --- a/test/Generator/RootSyntaxVisitorTests/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/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt deleted file mode 100644 index 68fb175..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Accessibilities_accessibilities=PublicInternal.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "internal": { - "type": "string", - "title": "Internal" - } - }, - "required": [ - "internal" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt deleted file mode 100644 index 03fd3c2..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_ExtendsAbstractClass.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - } - }, - "required": [ - "name" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt deleted file mode 100644 index aa8510c..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithArrayProperties.verified.txt +++ /dev/null @@ -1,102 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "numbersArray": { - "type": "array", - "items": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Numbers Array" - }, - "names": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "title": "Names" - }, - "addresses": { - "oneOf": [ - { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" - } - }, - { - "type": "null" - } - ], - "title": "Addresses" - }, - "numbersList": { - "type": "array", - "items": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Numbers List" - }, - "stringsEnumerable": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ], - "title": "Strings Enumerable" - }, - "stringImmutableArray": { - "type": "array", - "items": { - "type": "string" - }, - "title": "String Immutable Array" - } - }, - "required": [ - "numbersArray", - "names", - "numbersList", - "stringImmutableArray" - ], - "title": "Class Witharrayproperties", - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ], - "title": "Address" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt deleted file mode 100644 index e47a808..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDictionaryProperties.verified.txt +++ /dev/null @@ -1,48 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "valueTypes": { - "type": "object", - "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Value Types" - }, - "referenceTypes": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" - }, - "title": "Reference Types" - } - }, - "required": [ - "valueTypes", - "referenceTypes" - ], - "title": "Class Withdictionaryproperties", - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ], - "title": "Address" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt deleted file mode 100644 index 2f95fe2..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithDocComments.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "The name of the person." - }, - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Age", - "description": "The age of the person." - } - }, - "required": [ - "name", - "age" - ], - "title": "Class Withdoccomments" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt deleted file mode 100644 index 8de934f..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithIgnoredProperty.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "notIgnored": { - "type": "string", - "title": "Not Ignored" - } - }, - "required": [ - "notIgnored" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInternalProperties.verified.txt deleted file mode 100644 index 95c99cb..0000000 --- a/test/Generator/RootSyntaxVisitorTests/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/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt deleted file mode 100644 index d1faa8c..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithInvalidProperties.verified.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Class Withinvalidproperties" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt deleted file mode 100644 index 95c6637..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithSchemaOverride.verified.txt +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "maxLength": 50 - }, - "age": { - "type": "integer", - "minimum": 0 - } - }, - "required": [ - "name", - "age" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt deleted file mode 100644 index b704747..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithTypeSchemaOverride.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "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" - }, - "goodOverride": { - "type": "object", - "properties": { - "custom": { - "type": "string" - } - }, - "title": "Good Override" - } - }, - "required": [ - "badOverride", - "goodOverride" - ], - "title": "Class Withtypeschemaoverride" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt deleted file mode 100644 index 4171c73..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithUnsupportedDictionaryKey.verified.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "type": "object", - "$comment": "Key type 'Object' must be convertible to string", - "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Data" - } - }, - "required": [ - "data" - ], - "title": "Class Withunsupporteddictionarykey" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt deleted file mode 100644 index 09f0930..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Class_WithValueTypes.verified.txt +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "string": { - "type": "string", - "title": "String" - }, - "int": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Int" - }, - "bool": { - "type": "boolean", - "title": "Bool" - }, - "byte": { - "type": "integer", - "minimum": 0, - "maximum": 255, - "$comment": "System.Byte", - "title": "Byte" - }, - "sByte": { - "type": "integer", - "minimum": -128, - "maximum": 127, - "$comment": "System.SByte", - "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" - }, - "uInt": { - "type": "integer", - "minimum": 0, - "maximum": 4294967295, - "$comment": "System.UInt32", - "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" - }, - "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" - } - }, - "required": [ - "string", - "int", - "bool", - "byte", - "sByte", - "short", - "uShort", - "uInt", - "long", - "uLong", - "float", - "double", - "decimal", - "char" - ], - "title": "Class Withvaluetypes" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt deleted file mode 100644 index 164d76f..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=GameHall.verified.txt +++ /dev/null @@ -1,230 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "rooms": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.GameRoom" - }, - "title": "Rooms" - } - }, - "required": [ - "name", - "rooms" - ], - "title": "Game Hall", - "$defs": { - "T:SharpSchema.Generator.TestData.BaseHand.Poker": { - "type": "object", - "properties": { - "game": { - "type": "object", - "const": "Poker", - "title": "Game" - }, - "isRoyalFlush": { - "type": "boolean", - "title": "Is Royal Flush" - } - }, - "required": [ - "game", - "isRoyalFlush" - ], - "title": "Poker" - }, - "T:SharpSchema.Generator.TestData.PokerTable": { - "type": "object", - "properties": { - "playerCount": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Player Count" - }, - "dealerHand": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Poker", - "title": "Dealer Hand" - } - }, - "required": [ - "playerCount", - "dealerHand" - ], - "title": "Poker Table" - }, - "T:SharpSchema.Generator.TestData.BaseHand.Blackjack": { - "type": "object", - "properties": { - "game": { - "type": "object", - "const": "Blackjack", - "title": "Game" - }, - "value": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Value" - } - }, - "required": [ - "game", - "value" - ], - "title": "Blackjack" - }, - "T:SharpSchema.Generator.TestData.BlackjackTable": { - "type": "object", - "properties": { - "playerCount": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Player Count" - }, - "dealerHand": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Blackjack", - "title": "Dealer Hand" - } - }, - "required": [ - "playerCount", - "dealerHand" - ], - "title": "Blackjack Table" - }, - "T:SharpSchema.Generator.TestData.BaseHand.Bridge": { - "type": "object", - "properties": { - "game": { - "type": "object", - "const": "Bridge", - "title": "Game" - }, - "isNoTrump": { - "type": "boolean", - "title": "Is No Trump" - } - }, - "required": [ - "game", - "isNoTrump" - ], - "title": "Bridge" - }, - "T:SharpSchema.Generator.TestData.BridgeTable": { - "type": "object", - "properties": { - "playerCount": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Player Count" - }, - "dealerHand": { - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" - }, - { - "type": "null" - } - ], - "title": "Dealer Hand" - } - }, - "required": [ - "playerCount" - ], - "title": "Bridge Table" - }, - "T:SharpSchema.Generator.TestData.GameRoom": { - "type": "object", - "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" - ], - "title": "Game Room" - }, - "T:SharpSchema.Generator.TestData.WhistTable": { - "type": "object", - "properties": { - "playerCount": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Player Count" - }, - "dealerHand": { - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" - }, - { - "type": "null" - } - ], - "title": "Dealer Hand" - } - }, - "required": [ - "playerCount" - ], - "title": "Whist Table" - }, - "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" - } - ] - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt deleted file mode 100644 index 9cbde1b..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithAbstractParameters.verified.txt +++ /dev/null @@ -1,76 +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" - ], - "title": "Record Withabstractparameters", - "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "string", - "enum": [ - "spades", - "hearts", - "clubs", - "diamonds" - ], - "title": "Suit Kind" - }, - "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": { - "type": "object", - "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" - ], - "title": "Card" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt deleted file mode 100644 index 507e0fc..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithGenericAbstractProperty.verified.txt +++ /dev/null @@ -1,84 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "stack": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.MagicStack", - "title": "Stack" - } - }, - "required": [ - "stack" - ], - "title": "Record Withgenericabstractproperty", - "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "string", - "enum": [ - "spades", - "hearts", - "clubs", - "diamonds" - ], - "title": "Suit Kind" - }, - "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": { - "type": "object", - "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" - ], - "title": "Card" - }, - "T:SharpSchema.Generator.TestData.MagicStack": { - "type": "object", - "properties": { - "cards": { - "type": "array", - "items": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" - }, - "title": "Cards" - } - }, - "required": [ - "cards" - ], - "title": "Magic Stack" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt deleted file mode 100644 index 8de934f..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithIgnoredParameter.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "notIgnored": { - "type": "string", - "title": "Not Ignored" - } - }, - "required": [ - "notIgnored" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt deleted file mode 100644 index c8a3405..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithParametersAndProperties.verified.txt +++ /dev/null @@ -1,47 +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" - ], - "title": "Record Withparametersandproperties", - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ], - "title": "Address" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt deleted file mode 100644 index d7f2c73..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeParameters.verified.txt +++ /dev/null @@ -1,62 +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" - ], - "title": "Record Withreferencetypeparameters", - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ], - "title": "Address" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt deleted file mode 100644 index 65943bb..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithReferenceTypeProperties.verified.txt +++ /dev/null @@ -1,91 +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" - ], - "title": "Record Withreferencetypeproperties", - "$defs": { - "T:SharpSchema.Generator.TestData.Address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "title": "Street" - }, - "city": { - "type": "string", - "title": "City" - } - }, - "required": [ - "street", - "city" - ], - "title": "Address" - }, - "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" - ], - "title": "Office" - }, - "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" - ], - "title": "Person" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt deleted file mode 100644 index 95c6637..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithSchemaOverride.verified.txt +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "maxLength": 50 - }, - "age": { - "type": "integer", - "minimum": 0 - } - }, - "required": [ - "name", - "age" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt deleted file mode 100644 index 75e1650..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Record_WithValueTypeParameters.verified.txt +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "NameOfRecord", - "description": "The record's name.", - "examples": [ - "John Doe" - ] - }, - "age": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "default": 42, - "title": "NameOfRecord", - "description": "The record's name.", - "examples": [ - "42" - ] - } - }, - "required": [ - "name" - ], - "title": "NameOfRecord", - "description": "The record's name.", - "examples": [ - "John Doe" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt deleted file mode 100644 index 1157b93..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=SimpleRecord.verified.txt +++ /dev/null @@ -1,33 +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", - "default": 42, - "title": "Age" - }, - "description": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - } - }, - "required": [ - "name" - ], - "title": "Simple Record" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt deleted file mode 100644 index 0e42cbf..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithAbstractProperties.verified.txt +++ /dev/null @@ -1,42 +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" - ], - "title": "Struct Withabstractproperties", - "$defs": { - "T:SharpSchema.Generator.TestData.Class_ExtendsAbstractClass": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - } - }, - "required": [ - "name" - ], - "title": "Class Extendsabstractclass" - }, - "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/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt deleted file mode 100644 index c2ff213..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DefaultOptions_testName=Struct_WithNullableValueTypes.verified.txt +++ /dev/null @@ -1,186 +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" - } - }, - "title": "Struct Withnullablevaluetypes" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt deleted file mode 100644 index 4171c73..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Loose.verified.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "type": "object", - "$comment": "Key type 'Object' must be convertible to string", - "additionalProperties": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32" - }, - "title": "Data" - } - }, - "required": [ - "data" - ], - "title": "Class Withunsupporteddictionarykey" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt deleted file mode 100644 index bfc4ea4..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Silent.verified.txt +++ /dev/null @@ -1,20 +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" - ], - "title": "Class Withunsupporteddictionarykey" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt deleted file mode 100644 index 6b119f4..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Skip.verified.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Class Withunsupporteddictionarykey" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt deleted file mode 100644 index a86427b..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_DictionaryKeyMode_dictionaryKeyMode=Strict.verified.txt +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "data": { - "$unsupportedObject": "Key type 'Object' is not supported.", - "title": "Data" - } - }, - "required": [ - "data" - ], - "title": "Class Withunsupporteddictionarykey" -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt deleted file mode 100644 index 9cbde1b..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=String.verified.txt +++ /dev/null @@ -1,76 +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" - ], - "title": "Record Withabstractparameters", - "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "string", - "enum": [ - "spades", - "hearts", - "clubs", - "diamonds" - ], - "title": "Suit Kind" - }, - "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": { - "type": "object", - "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" - ], - "title": "Card" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt deleted file mode 100644 index 7a6e231..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_EnumHandling_enumHandling=UnderlyingType.verified.txt +++ /dev/null @@ -1,61 +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" - ], - "title": "Record Withabstractparameters", - "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Suit Kind" - }, - "T:SharpSchema.Generator.TestData.Card.RankKind": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647, - "$comment": "System.Int32", - "title": "Rank Kind" - }, - "T:SharpSchema.Generator.TestData.Card": { - "type": "object", - "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" - ], - "title": "Card" - } - } -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt deleted file mode 100644 index 1b5fd4c..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Bases.verified.txt +++ /dev/null @@ -1,21 +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" - } - }, - "required": [ - "name", - "age" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt deleted file mode 100644 index 1b5fd4c..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Full.verified.txt +++ /dev/null @@ -1,21 +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" - } - }, - "required": [ - "name", - "age" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt deleted file mode 100644 index 03fd3c2..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=Interfaces.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - } - }, - "required": [ - "name" - ] -} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt b/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt deleted file mode 100644 index 03fd3c2..0000000 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.Verify_Traversal_traversal=SymbolOnly.verified.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name" - } - }, - "required": [ - "name" - ] -} \ No newline at end of file From c39e5554453311efc5f099c9b5614f6be02029b8 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:43:35 +0000 Subject: [PATCH 04/11] Enhance schema generation and testing infrastructure - Added `.runsettings` and `nvim-diff.runsettings` for diff engine configurations. - Refactored `GeneratorOptions` to use `init` properties for better immutability. - Removed `SchemaNumberModeAttribute` class to simplify code. - Introduced `Unsupported` class for centralized error messaging in schema generation. - Improved `LeafSyntaxVisitor` for better caching and error handling. - Streamlined schema creation in `CommonSchemas`. - Updated `MemberMeta` for more user-friendly title generation. - Renamed and refactored `TypeSchemaSymbolVisitor` to `NamedTypeSymbolVisitor`. - Enhanced `VerifyTests` with new verification methods and improved organization. - Updated test data files to reflect schema changes, including new records and properties. - Added verification files for accessibility and number modes. - Introduced new test cases for dictionary key modes and enum modes. - Updated `vscode-diff.runsettings` for Visual Studio Code configuration. --- .runsettings | 11 + nvim-diff.runsettings | 11 + .../GeneratorOptions.cs | 37 ++- .../SchemaNumberModeAttribute.cs | 22 -- .../GenericTypeSymbolVisitor.cs | 0 .../LeafNodeSymbolVisitor.cs | 0 .../LeafSyntaxVisitor.Unsupported.cs | 21 ++ .../LeafSyntaxVisitor.cs | 150 ++++-------- .../Model/CommonSchemas.cs | 48 ++-- src/SharpSchema.Generator/Model/Metadata.cs | 2 +- ...olVisitor.cs => NamedTypeSymbolVisitor.cs} | 99 ++++---- .../Resolvers/EnumSymbolVisitor.cs | 2 +- .../RootSyntaxVisitor.cs | 72 +++++- .../Utilities/CommonSchemas.cs | 0 .../Utilities/SymbolExtensions.cs | 24 +- .../RootSyntaxVisitorTests/TestData.cs | 69 ++++-- .../RootSyntaxVisitorTests/TestDataFixture.cs | 2 - .../AccessibilityMode_Internal.verified.txt | 14 ++ .../AccessibilityMode_Private.verified.txt | 14 ++ .../AccessibilityMode_Public.verified.txt | 14 ++ ...essibilityMode_PublicInternal.verified.txt | 19 ++ ...ons_Class_WithArrayProperties.verified.txt | 101 ++++++++ ...Options_Class_WithDocComments.verified.txt | 27 +++ .../DefaultOptions_GameHall.verified.txt | 217 ++++++++++++++++++ ...ord_WithDefaultValueParameter.verified.txt | 26 +++ ...ParametersAndConstantProperty.verified.txt | 31 +++ ...ptions_Record_WithDocComments.verified.txt | 38 +++ ...ns_Record_WithValueParameters.verified.txt | 26 +++ ...ithValueParametersAndProperty.verified.txt | 31 +++ ...ametersAndPropertyInitializer.verified.txt | 31 +++ .../DictionaryKeyMode_Loose.verified.txt | 25 ++ .../DictionaryKeyMode_Silent.verified.txt | 24 ++ .../DictionaryKeyMode_Skip.verified.txt | 12 + .../DictionaryKeyMode_Strict.verified.txt | 21 ++ .../EnumMode_String.verified.txt | 76 ++++++ .../EnumMode_UnderlyingType.verified.txt | 55 +++++ .../NumberMode_JsonNative.verified.txt | 81 +++++++ .../NumberMode_StrictDefs.verified.txt | 141 ++++++++++++ .../NumberMode_StrictInline.verified.txt | 103 +++++++++ .../RootSyntaxVisitorTests/VerifyTests.cs | 97 +++++--- vscode-diff.runsettings | 11 + 41 files changed, 1525 insertions(+), 280 deletions(-) create mode 100644 .runsettings create mode 100644 nvim-diff.runsettings delete mode 100644 src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs delete mode 100644 src/SharpSchema.Generator/GenericTypeSymbolVisitor.cs delete mode 100644 src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs create mode 100644 src/SharpSchema.Generator/LeafSyntaxVisitor.Unsupported.cs rename src/SharpSchema.Generator/{TypeSchemaSymbolVisitor.cs => NamedTypeSymbolVisitor.cs} (73%) delete mode 100644 src/SharpSchema.Generator/Utilities/CommonSchemas.cs create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_PublicInternal.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParameter.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDefaultValueParametersAndConstantProperty.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndProperty.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParametersAndPropertyInitializer.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Loose.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Skip.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Strict.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_JsonNative.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt create mode 100644 vscode-diff.runsettings 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/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs index 5ae058d..5cc5432 100644 --- a/src/SharpSchema.Annotations/GeneratorOptions.cs +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -7,25 +7,40 @@ namespace SharpSchema.Generator; /// /// Represents the options for the generator. /// -/// Specifies the accessibility mode. -/// Specifies the traversal mode. -/// Specifies the dictionary key mode. -/// Specifies the enum mode. -/// Specifies the number mode. #if SHARPSCHEMA_ASSEMBLY public #else internal #endif -record GeneratorOptions( - AccessibilityMode AccessibilityMode = AccessibilityMode.Public, - TraversalMode TraversalMode = TraversalMode.SymbolOnly, - DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, - EnumMode EnumMode = EnumMode.String, - NumberMode NumberMode = NumberMode.StrictDefs) +class GeneratorOptions { /// /// Gets the default generator options. /// public static GeneratorOptions Default { get; } = new GeneratorOptions(); + + /// + /// Gets the accessibility mode. + /// + public AccessibilityMode AccessibilityMode { get; init; } = AccessibilityMode.Public; + + /// + /// Gets the traversal mode. + /// + public TraversalMode TraversalMode { get; init; } = TraversalMode.SymbolOnly; + + /// + /// Gets the dictionary key mode. + /// + public DictionaryKeyMode DictionaryKeyMode { get; init; } = DictionaryKeyMode.Loose; + + /// + /// Gets the enum mode. + /// + public EnumMode EnumMode { get; init; } = EnumMode.String; + + /// + /// Gets the number mode. + /// + public NumberMode NumberMode { get; init; } = NumberMode.StrictDefs; } diff --git a/src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs b/src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs deleted file mode 100644 index bd2959e..0000000 --- a/src/SharpSchema.Annotations/SchemaNumberModeAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -#nullable enable - -namespace SharpSchema.Annotations; - -/// -/// Overrides the default number mode for a given type. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Parameter)] -#if SHARPSCHEMA_ASSEMBLY -public -#else -internal -#endif -class SchemaNumberModeAttribute(NumberMode value) : SchemaAttribute -{ - /// - /// Gets the number mode. - /// - public NumberMode Value { get; } = value; -} 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/LeafNodeSymbolVisitor.cs b/src/SharpSchema.Generator/LeafNodeSymbolVisitor.cs deleted file mode 100644 index e69de29..0000000 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 index 95af9b8..d5a3005 100644 --- a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs +++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs @@ -23,74 +23,20 @@ internal partial class LeafSyntaxVisitor : CSharpSyntaxVisitor private readonly Dictionary _cachedTypeSchemas; private readonly Dictionary _cachedAbstractSymbols; - public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) + public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticModelCache, GeneratorOptions options) { _compilation = compilation; _options = options; - _semanticModelCache = new(compilation); + _semanticModelCache = semanticModelCache; _metadataVisitor = new(); _collectionResolver = new(compilation); _cachedTypeSchemas = []; _cachedAbstractSymbols = []; } - internal GeneratorOptions Options => _options; + internal Dictionary CachedTypeSchemas => _cachedTypeSchemas; - public IReadOnlyDictionary? GetCachedSchemas() - { - if (_cachedAbstractSymbols.Count > 0) - { - using var trace = Tracer.Enter("Building abstract type schemas."); - ImmutableArray namedTypes = [.. _compilation.GetAllNamedTypes(_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 (NamedType 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 = this.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) - return null; - - return _cachedTypeSchemas.ToDictionary( - p => p.Key, - p => p.Value.Build()); - } + internal Dictionary CachedAbstractSymbols => _cachedAbstractSymbols; [ExcludeFromCodeCoverage] public override Builder? DefaultVisit(SyntaxNode node) @@ -134,7 +80,7 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) using Tracer.TraceScope 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}'."); + return CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); if (enumSymbol.GetOverrideSchema() is Builder overrideSchema) return overrideSchema; @@ -145,7 +91,7 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) MemberMeta metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(enumSymbol)); Builder builder = EnumSymbolVisitor.Instance.Visit(enumSymbol, _options) ?? - CommonSchemas.UnsupportedObject($"Failed to build schema for enum '{node.Identifier}'."); + CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); _cachedTypeSchemas[cacheKey] = builder.ApplyMemberMeta(metadata); } @@ -161,10 +107,10 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) TypeInfo typeInfo = node.GetTypeInfo(_semanticModelCache); if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for identifier '{node.Identifier}'."); + return CommonSchemas.UnsupportedObject(Unsupported.IdentifierMessage, node.Identifier); return this.Visit(typeSymbol.FindDeclaringSyntax()) - ?? CommonSchemas.UnsupportedObject($"Could not find declaration for identifier '{node.Identifier}'."); + ?? CommonSchemas.UnsupportedObject(Unsupported.DeclarationMessage, node.Identifier); } public override Builder? VisitGenericName(GenericNameSyntax node) @@ -173,24 +119,24 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); if (node.IsUnboundGenericName) - return CommonSchemas.UnsupportedObject($"Failed to evaluate unbound generic type '{node.Identifier}'."); + return CommonSchemas.UnsupportedObject(Unsupported.UnboundGenericMessage, 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."); + return CommonSchemas.UnsupportedObject(Unsupported.TypeSymbolMessage, node.Identifier.Text); + Builder? elementSchema = null; var (kind, keyType, elementSymbol) = _collectionResolver.Resolve(boundTypeSymbol); - if (kind is CollectionKind.Dictionary) + if (kind is CollectionKind.Dictionary or CollectionKind.Array) { - if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema)) - { - if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx) - elementSchema = elx.Accept(this); - } + elementSchema = node.TypeArgumentList.Arguments.Last().Accept(this); + } + if (kind is CollectionKind.Dictionary) + { if (elementSchema is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for dictionary element type '{elementSymbol}'."); + return CommonSchemas.UnsupportedObject(Unsupported.DictionaryElementMessage, elementSymbol.Name); Builder builder = CommonSchemas.Object; @@ -201,7 +147,7 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) case DictionaryKeyMode.Skip: return null; case DictionaryKeyMode.Strict: - return CommonSchemas.UnsupportedObject($"Key type '{keyType}' is not supported."); + return CommonSchemas.UnsupportedObject(Unsupported.KeyTypeMessage, keyType); case DictionaryKeyMode.Loose: builder = builder.Comment($"Key type '{keyType}' must be convertible to string"); break; @@ -217,21 +163,15 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) if (kind is CollectionKind.Array) { - if (!elementSymbol.IsJsonDefinedType(out Builder? elementSchema)) - { - if (elementSymbol.FindDeclaringSyntax() is BaseTypeDeclarationSyntax elx) - elementSchema = elx.Accept(this); - } - if (elementSchema is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for array element type '{elementSymbol}'."); + return CommonSchemas.UnsupportedObject(Unsupported.ArrayElementMessage, elementSymbol); return CommonSchemas.ArrayOf(elementSchema); } Builder? boundTypeBuilder = this.Visit(boundTypeSymbol.FindDeclaringSyntax()); if (boundTypeBuilder is null) - return CommonSchemas.UnsupportedObject($"Failed to build schema for generic type '{node.Identifier}'."); + return CommonSchemas.UnsupportedObject(Unsupported.GenericTypeMessage, node.Identifier); // Add to the oneOf for the unbound generic type INamedTypeSymbol unboundGeneric = boundTypeSymbol.ConstructUnboundGenericType(); @@ -252,12 +192,11 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) Throw.IfNullArgument(node); using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString()); - Builder? elementSchema = this.Visit(node.ElementType); - return elementSchema is null - ? new Builder().Const(null) - : CommonSchemas.Nullable( - Throw.ForUnexpectedNull( - node.ElementType.Accept(this))); + Builder? elementSchema = node.ElementType.Accept(this); + if (elementSchema is null) + return CommonSchemas.UnsupportedObject(Unsupported.NullableElementMessage, node.ElementType); + + return CommonSchemas.Nullable(elementSchema); } public override Builder? VisitPredefinedType(PredefinedTypeSyntax node) @@ -265,17 +204,31 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) 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 (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol) - { - return CommonSchemas.UnsupportedObject($"Failed to build schema for predefined type '{node}'."); - } + return CommonSchemas.UnsupportedObject(Unsupported.PredefinedTypeMessage, node); + + bool shouldCache = _options.NumberMode is NumberMode.StrictDefs; + + string cacheKey = typeSymbol.GetDefCacheKey(); + if (shouldCache && _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 (typeSymbol.IsJsonDefinedType(out Builder? valueTypeSchema)) + if (shouldCache) { - return valueTypeSchema; + _cachedTypeSchemas[cacheKey] = valueTypeSchema; + return CommonSchemas.DefRef(cacheKey); } - return CommonSchemas.UnsupportedObject($"Unexpected built-in type '{node.Keyword.Text}'."); + return valueTypeSchema; } public override Builder? VisitArrayType(ArrayTypeSyntax node) @@ -285,7 +238,7 @@ public LeafSyntaxVisitor(Compilation compilation, GeneratorOptions options) 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.UnsupportedObject(Unsupported.ArrayElementMessage, node.ElementType); return CommonSchemas.ArrayOf(elementSchema); } @@ -295,29 +248,29 @@ public Builder CreateTypeSchema(TypeDeclarationSyntax node) Throw.IfNullArgument(node); if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed to build schema for type '{node.Identifier}'."); + return CommonSchemas.UnsupportedObject(Unsupported.TypeMessage, node.Identifier); return this.CreateTypeSchema(typeSymbol, node); } - private Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax node, TraversalMode? traversalMode = null) + public Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax node, TraversalMode? traversalMode = null) { Throw.IfNullArgument(node); using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); return symbol.Accept( - new TypeSchemaSymbolVisitor( + new NamedTypeSymbolVisitor( // Always create a new visitor instance this, _semanticModelCache, _options), - node) + argument: null) ?? CommonSchemas.UnsupportedObject(symbol.Name); } private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node, Tracer.TraceScope trace) { if (_semanticModelCache.GetSemanticModel(node).GetDeclaredSymbol(node) is not INamedTypeSymbol typeSymbol) - return CommonSchemas.UnsupportedObject($"Failed building schema for {node.Identifier.ValueText}"); + return CommonSchemas.UnsupportedObject(Unsupported.SymbolMessage, node.Identifier.ValueText); string typeId = typeSymbol.GetDefCacheKey(); if (_cachedTypeSchemas.TryGetValue(typeId, out _)) @@ -326,7 +279,6 @@ private Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax if (typeSymbol.GetOverrideSchema() is Builder overrideSchema) return overrideSchema; - // Handle abstract types if (typeSymbol.IsAbstract) { trace.WriteLine($"Found abstract type '{typeSymbol.Name}'."); diff --git a/src/SharpSchema.Generator/Model/CommonSchemas.cs b/src/SharpSchema.Generator/Model/CommonSchemas.cs index ac4fcac..411bdef 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,73 @@ 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) => new Builder() + .UnsupportedObject(string.Format(format, args)); + public static Builder DefRef(string key) => new Builder() .Ref(string.Format(DefUriFormat, key)); @@ -117,4 +108,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/Metadata.cs b/src/SharpSchema.Generator/Model/Metadata.cs index a797a55..c7268e1 100644 --- a/src/SharpSchema.Generator/Model/Metadata.cs +++ b/src/SharpSchema.Generator/Model/Metadata.cs @@ -72,7 +72,7 @@ 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; diff --git a/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs similarity index 73% rename from src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs rename to src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs index 1074e78..6a8628e 100644 --- a/src/SharpSchema.Generator/TypeSchemaSymbolVisitor.cs +++ b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs @@ -12,13 +12,18 @@ namespace SharpSchema.Generator; using Builder = JsonSchemaBuilder; -internal class TypeSchemaSymbolVisitor : SymbolVisitor +internal class NamedTypeSymbolVisitor : SymbolVisitor { + public class StateContainer + { + internal bool? WasLastPropertyRequired { get; set; } + } + private readonly CSharpSyntaxVisitor _syntaxVisitor; private readonly SemanticModelCache _semanticModelCache; private readonly GeneratorOptions _options; - public TypeSchemaSymbolVisitor( + public NamedTypeSymbolVisitor( CSharpSyntaxVisitor visitor, SemanticModelCache semanticModelCache, GeneratorOptions options) @@ -30,8 +35,9 @@ public TypeSchemaSymbolVisitor( protected override Builder? DefaultResult { get; } - public override Builder? VisitNamedType(INamedTypeSymbol symbol, BaseTypeDeclarationSyntax argument) + public override Builder? VisitNamedType(INamedTypeSymbol symbol, StateContainer? state) { + state ??= new StateContainer(); using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); Dictionary properties = new(StringComparer.OrdinalIgnoreCase); @@ -42,75 +48,50 @@ public TypeSchemaSymbolVisitor( IMethodSymbol primaryCtor = symbol.Constructors.First(); primaryCtor.Parameters.ForEach(param => { - Builder? typeBuilder = param.Accept(this, argument); + Builder? typeBuilder = param.Accept(this, state); if (typeBuilder is null) return; string propertyName = param.Name.Camelize(); properties.Add(propertyName, typeBuilder); - bool isNullable = param.NullableAnnotation switch - { - NullableAnnotation.NotAnnotated => false, - NullableAnnotation.Annotated => true, - NullableAnnotation.None => false, // TODO: Make Configurable - _ => throw new NotSupportedException() - }; - - bool isRequired = !isNullable; - if (param.HasExplicitDefaultValue) - isRequired = false; - - if (EvaluateSchemaRequired(param, isRequired)) + if (state.WasLastPropertyRequired == true) _requiredProperties.Add(propertyName); + + state.WasLastPropertyRequired = null; }); } symbol.GetMembers().OfType().ForEach(prop => { - Builder? valueBuilder = prop.Accept(this, argument); + Builder? valueBuilder = prop.Accept(this, state); if (valueBuilder is null) return; string propertyName = prop.Name.Camelize(); properties.Add(propertyName, valueBuilder); - bool isNullable = prop.NullableAnnotation switch - { - NullableAnnotation.NotAnnotated => false, - NullableAnnotation.Annotated => true, - NullableAnnotation.None => false, // TODO: Make Configurable - _ => throw new NotSupportedException() - }; - - bool isRequired = !isNullable; - if (EvaluateSchemaRequired(prop, isRequired)) + if (state.WasLastPropertyRequired == true) _requiredProperties.Add(propertyName); + + state.WasLastPropertyRequired = null; }); 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 (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) - builder = builder.ApplyMemberMeta(meta); - return builder; - - static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) - { - AttributeHandler schemaRequired = prop.GetAttributeHandler(); - if (schemaRequired[0] is bool overrideRequired) - return overrideRequired; - - return isRequired; - } } - public override Builder? VisitProperty(IPropertySymbol symbol, BaseTypeDeclarationSyntax argument) + public override Builder? VisitProperty(IPropertySymbol symbol, StateContainer? state) { using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); @@ -124,19 +105,26 @@ static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) if (typeBuilder is null) return null; + bool isRequired = !IsNullable(symbol.NullableAnnotation); + if (pdx.ExpressionBody is ArrowExpressionClauseSyntax aec && GetConstantValue(aec.Expression) is JsonNode constantValue) { - typeBuilder = CommonSchemas.Object.Const(constantValue); + typeBuilder = CommonSchemas.Const(constantValue); + isRequired = true; } else if (ExtractDefaultValue(pdx) is JsonNode defaultValue) { typeBuilder = typeBuilder.Default(defaultValue); + isRequired = false; } if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) typeBuilder = typeBuilder.ApplyMemberMeta(meta); + + state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); + return typeBuilder; } @@ -161,7 +149,7 @@ static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) } } - public override Builder? VisitParameter(IParameterSymbol symbol, BaseTypeDeclarationSyntax argument) + public override Builder? VisitParameter(IParameterSymbol symbol, StateContainer? state) { using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); @@ -175,19 +163,44 @@ static bool EvaluateSchemaRequired(ISymbol prop, bool isRequired) if (typeBuilder is null) return null; + bool isRequired = !IsNullable(symbol.NullableAnnotation); ; + 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); + state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); + return typeBuilder; } return null; } + + 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/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs index a780793..72fb286 100644 --- a/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs +++ b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs @@ -41,7 +41,7 @@ private EnumSymbolVisitor() { } trace.WriteLine("Underlying type enum handling."); if (symbol.EnumUnderlyingType is INamedTypeSymbol underlyingSymbol - && underlyingSymbol.IsJsonDefinedType(out Builder? underlyingBuilder)) + && underlyingSymbol.IsJsonDefinedType(NumberMode.JsonNative, out Builder? underlyingBuilder)) { return underlyingBuilder; } diff --git a/src/SharpSchema.Generator/RootSyntaxVisitor.cs b/src/SharpSchema.Generator/RootSyntaxVisitor.cs index ec9b4f9..ddd9d4e 100644 --- a/src/SharpSchema.Generator/RootSyntaxVisitor.cs +++ b/src/SharpSchema.Generator/RootSyntaxVisitor.cs @@ -1,8 +1,10 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +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; -using Json.Schema; namespace SharpSchema.Generator; @@ -14,6 +16,8 @@ namespace SharpSchema.Generator; public class RootSyntaxVisitor : CSharpSyntaxVisitor { private readonly LeafSyntaxVisitor _cachingVisitor; + private readonly Compilation _compilation; + private readonly SemanticModelCache _semanticModelCache; /// /// Initializes a new instance of the class. @@ -22,7 +26,12 @@ public class RootSyntaxVisitor : CSharpSyntaxVisitor /// The generator options. public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) { - _cachingVisitor = new(compilation, options); + Throw.IfNullArgument(compilation); + Throw.IfNullArgument(options); + + _compilation = compilation; + _semanticModelCache = new(compilation); + _cachingVisitor = new(compilation, _semanticModelCache, options); } /// @@ -96,9 +105,60 @@ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) { Builder builder = _cachingVisitor.CreateTypeSchema(node); - if (_cachingVisitor.GetCachedSchemas() is IReadOnlyDictionary defs) + Dictionary cachedAbstractSymbols = _cachingVisitor.CachedAbstractSymbols; + Dictionary cachedTypeSchemas = _cachingVisitor.CachedTypeSchemas; + + if (cachedAbstractSymbols.Count > 0) + { + using var trace = Tracer.Enter("Building abstract type schemas."); + ImmutableArray namedTypes = [.. _compilation.GetAllNamedTypes(_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 (NamedType 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 = _cachingVisitor.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(defs); + builder.Defs(cachedTypeSchemas.ToDictionary( + p => p.Key, + p => p.Value.Build())); } return builder; 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/SymbolExtensions.cs b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs index f5d5ed5..0f5c171 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -206,7 +206,7 @@ public static bool InheritsFrom(this INamedTypeSymbol symbol, INamedTypeSymbol b 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) @@ -219,21 +219,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/test/Generator/RootSyntaxVisitorTests/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs index 7b9be5f..c15f713 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -7,13 +7,52 @@ #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 SimpleRecord(string Name, int Age = 42) +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? Description { get; set; } + 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 { /// @@ -88,29 +127,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( @@ -206,8 +230,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; } diff --git a/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs index d0b703a..17f8249 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.IO; using Xunit; -using DiffEngine; using SharpSchema.Generator; using SharpSchema.Test.Generator.TestUtilities; using System.Collections.Immutable; @@ -23,7 +22,6 @@ public class TestDataFixture public TestDataFixture() { - DiffRunner.Disabled = true; string pathToTestData = PathHelper.GetRepoPath( "test", "Generator", diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt new file mode 100644 index 0000000..a57cef1 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Internal.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with internal properties", + "properties": { + "internal": { + "type": "string", + "title": "Internal" + } + }, + "required": [ + "internal" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt new file mode 100644 index 0000000..b65799f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Private.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with internal properties", + "properties": { + "private": { + "type": "string", + "title": "Private" + } + }, + "required": [ + "private" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt new file mode 100644 index 0000000..546e0be --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityMode_Public.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with internal properties", + "properties": { + "public": { + "type": "string", + "title": "Public" + } + }, + "required": [ + "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/DefaultOptions_Class_WithArrayProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt new file mode 100644 index 0000000..e258b4f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with array properties", + "properties": { + "numbersArray": { + "type": "array", + "items": { + "$ref": "#/$defs/T:System.Int32" + }, + "title": "Numbers array" + }, + "names": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": "Names" + }, + "addresses": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" + } + }, + { + "type": "null" + } + ], + "title": "Addresses" + }, + "numbersList": { + "type": "array", + "items": { + "$ref": "#/$defs/T:System.Int32" + }, + "title": "Numbers list" + }, + "stringsEnumerable": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "title": "Strings enumerable" + }, + "stringImmutableArray": { + "type": "array", + "items": { + "type": "string" + }, + "title": "String immutable array" + } + }, + "required": [ + "numbersArray", + "names", + "numbersList", + "stringImmutableArray" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:SharpSchema.Generator.TestData.Address": { + "type": "object", + "title": "Address", + "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/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt new file mode 100644 index 0000000..d498641 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDocComments.verified.txt @@ -0,0 +1,27 @@ +{ + "$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": { + "$ref": "#/$defs/T:System.Int32", + "title": "Age", + "description": "The age of the person." + } + }, + "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_GameHall.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt new file mode 100644 index 0000000..18119cf --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt @@ -0,0 +1,217 @@ +{ + "$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:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:SharpSchema.Generator.TestData.BaseHand.Poker": { + "type": "object", + "title": "Poker", + "properties": { + "game": { + "const": "Poker", + "title": "Game" + }, + "isRoyalFlush": { + "type": "boolean", + "title": "Is royal flush" + } + }, + "required": [ + "game", + "isRoyalFlush" + ] + }, + "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.BaseHand.Blackjack": { + "type": "object", + "title": "Blackjack", + "properties": { + "game": { + "const": "Blackjack", + "title": "Game" + }, + "value": { + "$ref": "#/$defs/T:System.Int32", + "title": "Value" + } + }, + "required": [ + "game", + "value" + ] + }, + "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.BaseHand.Bridge": { + "type": "object", + "title": "Bridge", + "properties": { + "game": { + "const": "Bridge", + "title": "Game" + }, + "isNoTrump": { + "type": "boolean", + "title": "Is no trump" + } + }, + "required": [ + "game", + "isNoTrump" + ] + }, + "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.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.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: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" + } + ] + } + } +} \ 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/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt new file mode 100644 index 0000000..6f6b566 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithDocComments.verified.txt @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "NameOfRecord", + "description": "The record's name.", + "examples": [ + "John Doe" + ], + "properties": { + "name": { + "type": "string", + "title": "NameOfRecord", + "description": "The record's name.", + "examples": [ + "John Doe" + ] + }, + "age": { + "$ref": "#/$defs/T:System.Int32", + "default": 42, + "title": "NameOfRecord", + "description": "The record's name.", + "examples": [ + "42" + ] + } + }, + "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_WithValueParameters.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt new file mode 100644 index 0000000..8c81e3a --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithValueParameters.verified.txt @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with value parameters", + "properties": { + "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/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/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt new file mode 100644 index 0000000..46614c1 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyMode_Silent.verified.txt @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with unsupported dictionary key", + "properties": { + "data": { + "type": "object", + "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/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/EnumMode_String.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt new file mode 100644 index 0000000..63d8dd0 --- /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.SuitKind": { + "type": "string", + "enum": [ + "spades", + "hearts", + "clubs", + "diamonds" + ], + "title": "Suit kind" + }, + "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": { + "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" + ] + } + } +} \ 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..cf2e0a7 --- /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.SuitKind": { + "type": "integer", + "title": "Suit kind" + }, + "T:SharpSchema.Generator.TestData.Card.RankKind": { + "type": "integer", + "title": "Rank kind" + }, + "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" + ] + } + } +} \ 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..47beea0 --- /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.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:System.Byte": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "T:System.SByte": { + "type": "integer", + "minimum": -128, + "maximum": 127 + }, + "T:System.Int16": { + "type": "integer", + "minimum": -32768, + "maximum": 32767 + }, + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "T:System.Int64": { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807 + }, + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 + }, + "T:System.Single": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Double": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Decimal": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Char": { + "type": "string", + "minLength": 1, + "maxLength": 1 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt new file mode 100644 index 0000000..bdafc3e --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictInline.verified.txt @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with value types", + "properties": { + "string": { + "type": "string", + "title": "String" + }, + "int": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "title": "Int" + }, + "bool": { + "type": "boolean", + "title": "Bool" + }, + "byte": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "title": "Byte" + }, + "sByte": { + "type": "integer", + "minimum": -128, + "maximum": 127, + "title": "S byte" + }, + "short": { + "type": "integer", + "minimum": -32768, + "maximum": 32767, + "title": "Short" + }, + "uShort": { + "type": "integer", + "minimum": 0, + "maximum": 65535, + "title": "U short" + }, + "uInt": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "title": "U int" + }, + "long": { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "title": "Long" + }, + "uLong": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615, + "title": "U long" + }, + "float": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335, + "title": "Float" + }, + "double": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335, + "title": "Double" + }, + "decimal": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335, + "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/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index 5350be1..76b528b 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -15,6 +15,7 @@ namespace SharpSchema.Test.Generator.RootSyntaxVisitorTests; + public class VerifyTests : IDisposable, IClassFixture { private readonly ITestOutputHelper _output; @@ -26,38 +27,32 @@ public VerifyTests(TestDataFixture fixture, ITestOutputHelper outputHelper) _fixture = fixture; Tracer.Writer = outputHelper.WriteLine; - Tracer.EnableTiming = true; _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(SimpleRecord))] - [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(Record_WithValueParameters))] + [InlineData(nameof(Record_WithDefaultValueParameter))] + [InlineData(nameof(Record_WithValueParametersAndProperty))] + [InlineData(nameof(Record_WithValueParametersAndPropertyInitializer))] + [InlineData(nameof(Record_WithDefaultValueParametersAndConstantProperty))] + [InlineData(nameof(Record_WithDocComments))] [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))] [InlineData(nameof(GameHall))] public Task Verify_DefaultOptions(string testName) { + Tracer.EnableTiming = true; + RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); _output.WriteSeparator(); @@ -65,15 +60,15 @@ public Task Verify_DefaultOptions(string testName) _output.WriteLine(schemaString); _output.WriteSeparator(); - // Get type instance from test name - Type? type = Assembly.GetExecutingAssembly().GetType($"SharpSchema.Generator.TestData.{testName}"); - if (type is not null) - { - System.Text.Json.Nodes.JsonNode n = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, type, JsonSchemaExporterOptions.Default); - _output.WriteLine(n.ToString()); - } + //// Get type instance from test name + //Type? type = Assembly.GetExecutingAssembly().GetType($"SharpSchema.Generator.TestData.{testName}"); + //if (type is not null) + //{ + // System.Text.Json.Nodes.JsonNode n = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, type, JsonSchemaExporterOptions.Default); + // _output.WriteLine(n.ToString()); + //} - return Verifier.Verify(schemaString).UseParameters(testName); + return Verify(schemaString, "DefaultOptions", testName); } [InlineData(DictionaryKeyMode.Loose)] @@ -91,9 +86,11 @@ public Task Verify_DictionaryKeyMode(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 Verifier.Verify(schemaString).UseParameters(dictionaryKeyMode); + + return Verify(schemaString, "DictionaryKeyMode", dictionaryKeyMode.ToString()); } [InlineData(AccessibilityMode.Public)] @@ -101,7 +98,7 @@ public Task Verify_DictionaryKeyMode(DictionaryKeyMode dictionaryKeyMode) [InlineData(AccessibilityMode.Private)] [InlineData(AccessibilityMode.PublicInternal)] [Theory] - public Task Verify_Accessibilities(AccessibilityMode accessibilities) + public Task Verify_AccessibilityMode(AccessibilityMode accessibilities) { GeneratorOptions options = new() { @@ -111,33 +108,38 @@ public Task Verify_Accessibilities(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 Verifier.Verify(schemaString).UseParameters(accessibilities); + + return Verify(schemaString, "AccessibilityMode", accessibilities.ToString()); } [InlineData(EnumMode.String)] [InlineData(EnumMode.UnderlyingType)] [Theory] - public Task Verify_EnumHandling(EnumMode enumHandling) + 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 Verifier.Verify(schemaString).UseParameters(enumHandling); + + return Verify(schemaString, "EnumMode", enumHandling.ToString()); } [InlineData(TraversalMode.SymbolOnly)] [InlineData(TraversalMode.Bases)] [InlineData(TraversalMode.Interfaces)] [InlineData(TraversalMode.Full)] - [Theory] + [Theory(Skip = "Traversal is not correctly implemented.")] public Task Verify_Traversal(TraversalMode traversal) { GeneratorOptions options = new() @@ -148,8 +150,31 @@ public Task Verify_Traversal(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 Verifier.Verify(schemaString).UseParameters(traversal); + + return Verify(schemaString, "NumberMode", numberMode.ToString()); } } 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 + + + From 59bb81409d3e71757c23578ad63793e794312c10 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:54:38 +0000 Subject: [PATCH 05/11] Remove IsExternalInit and add PolySharp package - Removed `IsExternalInit` package version from `Directory.Packages.props`. - Added `PackageReference` for `PolySharp` in `SharpSchema.Annotations.Source.csproj`. - Replaced `IsExternalInit` with `PolySharp` in `SharpSchema.Annotations.csproj`, maintaining asset settings. --- Directory.Packages.props | 1 - .../SharpSchema.Annotations.Source.csproj | 6 ++++++ src/SharpSchema.Annotations/SharpSchema.Annotations.csproj | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 344e1ad..93172cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,6 @@ - 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.Annotations/SharpSchema.Annotations.csproj b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj index e7f9f9c..7235686 100644 --- a/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj +++ b/src/SharpSchema.Annotations/SharpSchema.Annotations.csproj @@ -9,7 +9,7 @@ SHARPSCHEMA_ASSEMBLY - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 23851f5570e5d78c295693ab5bd955a311a4a4fc Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Wed, 26 Feb 2025 01:12:55 +0000 Subject: [PATCH 06/11] Refactor schema generation logic and update tests - Updated `NamedTypeSymbolVisitor` to improve `WasLastPropertyRequired` evaluation using `IsRequired`. - Simplified `AnnotationExtensions` to rely solely on `SchemaIgnoreAttribute` for ignoring symbols. - Expanded `VerifyTests` to cover new and modified classes and records. --- .../NamedTypeSymbolVisitor.cs | 3 +- .../Utilities/AnnotationExtensions.cs | 16 +- .../RootSyntaxVisitorTests/TestData.cs | 19 +- ...ns_Class_ExtendsAbstractClass.verified.txt | 1 + ...lass_WithDictionaryProperties.verified.txt | 50 +++++ ...ons_Class_WithIgnoredProperty.verified.txt | 14 ++ ...s_Class_WithInvalidProperties.verified.txt | 26 +++ ..._Class_WithRequiredProperties.verified.txt | 57 +++++ ...ions_Class_WithSchemaOverride.verified.txt | 1 + ..._Class_WithTypeSchemaOverride.verified.txt | 1 + ...s_Record_WithIgnoredParameter.verified.txt | 14 ++ ...ons_Record_WithSchemaOverride.verified.txt | 1 + ...Struct_WithNullableValueTypes.verified.txt | 212 ++++++++++++++++++ .../RootSyntaxVisitorTests/VerifyTests.cs | 6 + 14 files changed, 390 insertions(+), 31 deletions(-) create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithInvalidProperties.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt diff --git a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs index 6a8628e..16b8cdd 100644 --- a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs +++ b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs @@ -122,8 +122,7 @@ public NamedTypeSymbolVisitor( if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) typeBuilder = typeBuilder.ApplyMemberMeta(meta); - - state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); + state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, symbol.IsRequired || isRequired); return typeBuilder; } diff --git a/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs b/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs index 155ff07..74e80fe 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,20 +16,7 @@ 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) diff --git a/test/Generator/RootSyntaxVisitorTests/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs index c15f713..83310cc 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -114,6 +114,10 @@ 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; } @@ -148,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 { @@ -183,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}")] @@ -219,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; } diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt new file mode 100644 index 0000000..09b726f --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with dictionary properties", + "properties": { + "valueTypes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/T:System.Int32" + }, + "title": "Value types" + }, + "referenceTypes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Address" + }, + "title": "Reference types" + } + }, + "required": [ + "valueTypes", + "referenceTypes" + ], + "$defs": { + "T:System.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:SharpSchema.Generator.TestData.Address": { + "type": "object", + "title": "Address", + "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/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt new file mode 100644 index 0000000..2140eac --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithIgnoredProperty.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with ignored property", + "properties": { + "notIgnored": { + "type": "string", + "title": "Not ignored" + } + }, + "required": [ + "notIgnored" + ] +} \ No newline at end of file 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..6775ddc --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt @@ -0,0 +1,57 @@ +{ + "$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" + }, + "default": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "default", + "title": "Default" + } + }, + "required": [ + "requiredInt", + "required", + "default" + ], + "$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_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt new file mode 100644 index 0000000..be454b0 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithIgnoredParameter.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with ignored parameter", + "properties": { + "notIgnored": { + "type": "string", + "title": "Not ignored" + } + }, + "required": [ + "notIgnored" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt @@ -0,0 +1 @@ + \ 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..389e820 --- /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.Int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "T:System.Byte": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "T:System.SByte": { + "type": "integer", + "minimum": -128, + "maximum": 127 + }, + "T:System.Int16": { + "type": "integer", + "minimum": -32768, + "maximum": 32767 + }, + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "T:System.Int64": { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807 + }, + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 + }, + "T:System.Single": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Double": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Decimal": { + "type": "number", + "minimum": -79228162514264337593543950335, + "maximum": 79228162514264337593543950335 + }, + "T:System.Char": { + "type": "string", + "minLength": 1, + "maxLength": 1 + } + } +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index 76b528b..6175bc5 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -46,8 +46,14 @@ internal static Task Verify(string schemaString, string testName, string paramet [InlineData(nameof(Record_WithValueParametersAndPropertyInitializer))] [InlineData(nameof(Record_WithDefaultValueParametersAndConstantProperty))] [InlineData(nameof(Record_WithDocComments))] + [InlineData(nameof(Record_WithIgnoredParameter))] [InlineData(nameof(Class_WithDocComments))] [InlineData(nameof(Class_WithArrayProperties))] + [InlineData(nameof(Class_WithDictionaryProperties))] + [InlineData(nameof(Class_WithInvalidProperties))] + [InlineData(nameof(Class_WithIgnoredProperty))] + [InlineData(nameof(Class_WithRequiredProperties))] + [InlineData(nameof(Struct_WithNullableValueTypes))] [InlineData(nameof(GameHall))] public Task Verify_DefaultOptions(string testName) { From fe9cd99805990456fa29dfe8642e40f9b27d05a4 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Thu, 27 Feb 2025 02:28:32 +0000 Subject: [PATCH 07/11] Add schema override handling in NamedTypeSymbolVisitor Enhance the NamedTypeSymbolVisitor to support schema overrides for properties and constructors, refining the logic for required properties and default values. Expand VerifyTests with additional cases to validate the new functionality. --- .../NamedTypeSymbolVisitor.cs | 73 +++++++++++-------- ...ions_Class_WithSchemaOverride.verified.txt | 22 +++++- ..._Class_WithTypeSchemaOverride.verified.txt | 25 ++++++- ...ons_Record_WithSchemaOverride.verified.txt | 22 +++++- .../RootSyntaxVisitorTests/VerifyTests.cs | 3 + 5 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs index 16b8cdd..0adfa9f 100644 --- a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs +++ b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs @@ -48,11 +48,14 @@ public NamedTypeSymbolVisitor( IMethodSymbol primaryCtor = symbol.Constructors.First(); primaryCtor.Parameters.ForEach(param => { + string propertyName = param.Name.Camelize(); + if (symbol.GetOverrideSchema() is Builder overrideSchema) + properties.Add(propertyName, overrideSchema); + Builder? typeBuilder = param.Accept(this, state); if (typeBuilder is null) return; - string propertyName = param.Name.Camelize(); properties.Add(propertyName, typeBuilder); if (state.WasLastPropertyRequired == true) @@ -64,11 +67,12 @@ public NamedTypeSymbolVisitor( symbol.GetMembers().OfType().ForEach(prop => { + string propertyName = prop.Name.Camelize(); + Builder? valueBuilder = prop.Accept(this, state); if (valueBuilder is null) return; - string propertyName = prop.Name.Camelize(); properties.Add(propertyName, valueBuilder); if (state.WasLastPropertyRequired == true) @@ -98,15 +102,19 @@ public NamedTypeSymbolVisitor( if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) return null; + Builder? typeBuilder = null; + if (symbol.GetOverrideSchema() is Builder overrideSchema) + typeBuilder = overrideSchema; + + bool isRequired = symbol.IsRequired || !IsNullable(symbol.NullableAnnotation); + // Excludes generated properties. - if (symbol.FindDeclaringSyntax() is PropertyDeclarationSyntax pdx) + if (typeBuilder is null && symbol.FindDeclaringSyntax() is PropertyDeclarationSyntax pdx) { - Builder? typeBuilder = pdx.Type.Accept(_syntaxVisitor); + typeBuilder = pdx.Type.Accept(_syntaxVisitor); if (typeBuilder is null) return null; - bool isRequired = !IsNullable(symbol.NullableAnnotation); - if (pdx.ExpressionBody is ArrowExpressionClauseSyntax aec && GetConstantValue(aec.Expression) is JsonNode constantValue) { @@ -118,16 +126,17 @@ public NamedTypeSymbolVisitor( typeBuilder = typeBuilder.Default(defaultValue); isRequired = false; } + } - if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) - typeBuilder = typeBuilder.ApplyMemberMeta(meta); + if (typeBuilder is null) + return null; - state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, symbol.IsRequired || isRequired); + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); - return typeBuilder; - } + state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); - return null; + return typeBuilder; // -- Local functions -- @@ -155,32 +164,36 @@ public NamedTypeSymbolVisitor( if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) return null; + Builder? typeBuilder = null; + if (symbol.GetOverrideSchema() is Builder overrideSchema) + typeBuilder = overrideSchema; + + bool isRequired = !IsNullable(symbol.NullableAnnotation); + // Excludes implicitly-typed parameters. - if (symbol.FindDeclaringSyntax() is ParameterSyntax px && px.Type is TypeSyntax tx) + if (typeBuilder is null && symbol.FindDeclaringSyntax() is ParameterSyntax px && px.Type is TypeSyntax tx) { - Builder? typeBuilder = px.Type.Accept(_syntaxVisitor); - if (typeBuilder is null) - return null; + typeBuilder = px.Type.Accept(_syntaxVisitor); + } - bool isRequired = !IsNullable(symbol.NullableAnnotation); ; + if (typeBuilder is null) + return null; - if (symbol.HasExplicitDefaultValue - && symbol.ExplicitDefaultValue is object edv - && JsonValue.Create(edv) is JsonNode defaultValue) - { - typeBuilder = typeBuilder.Default(defaultValue); - isRequired = 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); + if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) + typeBuilder = typeBuilder.ApplyMemberMeta(meta); - state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); + state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); - return typeBuilder; - } + return typeBuilder; - return null; } private static bool IsNullable(NullableAnnotation annotation) diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt index 5f28270..00932a9 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt @@ -1 +1,21 @@ - \ No newline at end of file +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class with schema override", + "properties": { + "name": { + "type": "string", + "maxLength": 50, + "title": "Name" + }, + "age": { + "type": "integer", + "minimum": 0, + "title": "Age" + } + }, + "required": [ + "name", + "age" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt index 5f28270..5c02023 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithTypeSchemaOverride.verified.txt @@ -1 +1,24 @@ - \ No newline at end of file +{ + "$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" + }, + "goodOverride": { + "type": "object", + "properties": { + "custom": { + "type": "string" + } + }, + "title": "Good override" + } + }, + "required": [ + "badOverride", + "goodOverride" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt index 5f28270..81c0e28 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt @@ -1 +1,21 @@ - \ No newline at end of file +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Record with schema override", + "properties": { + "name": { + "type": "string", + "maxLength": 50, + "title": "Name" + }, + "age": { + "type": "integer", + "minimum": 0, + "title": "Age" + } + }, + "required": [ + "name", + "age" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index 6175bc5..809a37d 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -54,6 +54,9 @@ internal static Task Verify(string schemaString, string testName, string paramet [InlineData(nameof(Class_WithIgnoredProperty))] [InlineData(nameof(Class_WithRequiredProperties))] [InlineData(nameof(Struct_WithNullableValueTypes))] + [InlineData(nameof(Class_WithSchemaOverride))] + [InlineData(nameof(Class_WithTypeSchemaOverride))] + [InlineData(nameof(Record_WithSchemaOverride))] [InlineData(nameof(GameHall))] public Task Verify_DefaultOptions(string testName) { From fecb19a5811c2a70385cded1c07131697d71a910 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:30:12 +0000 Subject: [PATCH 08/11] Refactor schema generation with new records and attributes - Changed `GeneratorOptions` from a class to a record, adding properties for `AccessibilityMode`, `TraversalMode`, `DictionaryKeyMode`, `EnumMode`, and `NumberMode`. - Introduced `ObjectAttributes` and `PropertyAttributes` records to handle various attribute properties. - Updated `NamedTypeSymbolVisitor` and `EnumSymbolVisitor` to utilize new traversal modes. - Added `GetAttributeHandler` methods in `SymbolExtensions` for easier attribute retrieval. - Enhanced `JsonSchemaBuilderExtensions` with a method to merge properties. - Modified `VerifyTests` to include new test cases and adjust existing ones. - Updated test data classes to reflect changes in `SchemaTraversalMode`. - Revised verification files to align with the new schema structure. --- .../GeneratorOptions.cs | 32 +-- .../Model/ObjectAttributes.cs | 27 +++ .../NamedTypeSymbolVisitor.cs | 34 ++- .../Resolvers/EnumSymbolVisitor.cs | 2 +- .../Utilities/AnnotationExtensions.cs | 3 +- .../Utilities/AttributeHandler.cs | 2 + .../Utilities/JsonSchemaBuilderExtensions.cs | 31 +++ .../Utilities/SymbolExtensions.cs | 216 +++++++++++++++++- .../RootSyntaxVisitorTests/TestData.cs | 17 +- ...ns_Class_ExtendsAbstractClass.verified.txt | 26 ++- .../Traversal_Bases.verified.txt | 25 ++ .../Verifications/Traversal_Full.verified.txt | 25 ++ .../Traversal_Interfaces.verified.txt | 14 ++ .../Traversal_SymbolOnly.verified.txt | 14 ++ .../RootSyntaxVisitorTests/VerifyTests.cs | 26 +-- 15 files changed, 437 insertions(+), 57 deletions(-) create mode 100644 src/SharpSchema.Generator/Model/ObjectAttributes.cs create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Bases.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Full.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt diff --git a/src/SharpSchema.Annotations/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs index 5cc5432..d735c4a 100644 --- a/src/SharpSchema.Annotations/GeneratorOptions.cs +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -12,35 +12,15 @@ namespace SharpSchema.Generator; #else internal #endif -class GeneratorOptions +record GeneratorOptions( + AccessibilityMode AccessibilityMode = AccessibilityMode.Public, + TraversalMode TraversalMode = TraversalMode.Bases, + DictionaryKeyMode DictionaryKeyMode = DictionaryKeyMode.Loose, + EnumMode EnumMode = EnumMode.String, + NumberMode NumberMode = NumberMode.StrictDefs) { /// /// Gets the default generator options. /// public static GeneratorOptions Default { get; } = new GeneratorOptions(); - - /// - /// Gets the accessibility mode. - /// - public AccessibilityMode AccessibilityMode { get; init; } = AccessibilityMode.Public; - - /// - /// Gets the traversal mode. - /// - public TraversalMode TraversalMode { get; init; } = TraversalMode.SymbolOnly; - - /// - /// Gets the dictionary key mode. - /// - public DictionaryKeyMode DictionaryKeyMode { get; init; } = DictionaryKeyMode.Loose; - - /// - /// Gets the enum mode. - /// - public EnumMode EnumMode { get; init; } = EnumMode.String; - - /// - /// Gets the number mode. - /// - public NumberMode NumberMode { get; init; } = NumberMode.StrictDefs; } diff --git a/src/SharpSchema.Generator/Model/ObjectAttributes.cs b/src/SharpSchema.Generator/Model/ObjectAttributes.cs new file mode 100644 index 0000000..8865910 --- /dev/null +++ b/src/SharpSchema.Generator/Model/ObjectAttributes.cs @@ -0,0 +1,27 @@ +using SharpSchema.Generator.Utilities; + +namespace SharpSchema.Generator.Model; + +internal record ObjectAttributes( + AttributeHandler AccessibilityMode, + AttributeHandler DictionaryKeyMode, + 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/NamedTypeSymbolVisitor.cs b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs index 0adfa9f..0591d90 100644 --- a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs +++ b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs @@ -40,6 +40,8 @@ public NamedTypeSymbolVisitor( state ??= new StateContainer(); using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); + ObjectAttributes attributes = symbol.GetObjectAttributes(_options.TraversalMode); + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); HashSet _requiredProperties = new(StringComparer.OrdinalIgnoreCase); @@ -82,7 +84,6 @@ public NamedTypeSymbolVisitor( }); Builder builder = CommonSchemas.Object; - if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) builder = builder.ApplyMemberMeta(meta); @@ -92,6 +93,37 @@ public NamedTypeSymbolVisitor( if (_requiredProperties.Count > 0) builder = builder.Required(_requiredProperties); + TraversalMode mode = _options.TraversalMode; + if (attributes.TraversalMode.Get(0) is TraversalMode traversalMode) + mode = traversalMode; + + if (mode.CheckFlag(TraversalMode.Bases)) + { + INamedTypeSymbol? @base = symbol.BaseType; + while (@base is not null && @base.SpecialType is not SpecialType.System_Object) + { + NamedTypeSymbolVisitor baseSymbolVisitor = new(_syntaxVisitor, _semanticModelCache, _options with { TraversalMode = mode }); + if (@base.Accept(baseSymbolVisitor, state) is Builder baseBuilder) + { + builder = builder.MergeProperties(baseBuilder); + } + + @base = @base.BaseType; + } + } + + if (_options.TraversalMode.CheckFlag(TraversalMode.Interfaces)) + { + symbol.AllInterfaces.ForEach(@interface => + { + NamedTypeSymbolVisitor interfaceSymbolVisitor = new(_syntaxVisitor, _semanticModelCache, _options with { TraversalMode = mode }); + if (@interface.Accept(interfaceSymbolVisitor, state) is Builder interfaceBuilder) + { + builder = builder.MergeProperties(interfaceBuilder); + } + }); + } + return builder; } diff --git a/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs index 72fb286..e8f655d 100644 --- a/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs +++ b/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs @@ -30,7 +30,7 @@ private EnumSymbolVisitor() { } { var names = symbol.GetMembers() .OfType() - .Select(fieldSymbol => fieldSymbol.GetAttributeHandler()[0] as string + .Select(fieldSymbol => fieldSymbol.GetAttributeHandler(TraversalMode.SymbolOnly)[0] as string ?? fieldSymbol.Name.Camelize()) .ToList(); diff --git a/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs b/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs index 74e80fe..d11d9a8 100644 --- a/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/AnnotationExtensions.cs @@ -21,8 +21,7 @@ public static bool IsIgnoredForGeneration(this ISymbol symbol) 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/AttributeHandler.cs b/src/SharpSchema.Generator/Utilities/AttributeHandler.cs index a305005..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 diff --git a/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs b/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs index 82064c5..4112db2 100644 --- a/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/JsonSchemaBuilderExtensions.cs @@ -51,6 +51,37 @@ public static JsonSchemaBuilder ApplySchema(this JsonSchema @base, JsonSchema ap return builder; } + public static JsonSchemaBuilder MergeProperties(this JsonSchemaBuilder @base, JsonSchema apply) + { + using var trace = Tracer.Enter($"{apply.BaseUri}"); + + IReadOnlyDictionary? baseProperties = @base.Get()?.Properties; + IReadOnlyDictionary? applyProperties = apply.GetProperties(); + + if (applyProperties is null) + return @base; + + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); + if (baseProperties is not null) + { + foreach ((string name, JsonSchema value) in baseProperties) + properties.Add(name, value); + } + + 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 properties.Count > 0 ? @base.Properties(properties) : @base; + } + public static JsonSchemaBuilder UnsupportedObject(this JsonSchemaBuilder builder, string value) { builder.Add(new UnsupportedObjectKeyword(value)); diff --git a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs index 0f5c171..4e908c3 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -17,6 +17,54 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave 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), + 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. /// @@ -27,6 +75,8 @@ public static AttributeHandler GetAttributeHandler(this ISymbol symbol, Trave 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()) { @@ -45,7 +95,10 @@ 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; @@ -60,13 +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 AttributeData? GetAttributeData(this IPropertySymbol 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 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) @@ -202,8 +410,6 @@ public static bool InheritsFrom(this INamedTypeSymbol symbol, INamedTypeSymbol b return false; } - - public static string GetDefCacheKey(this ITypeSymbol symbol) => symbol.GetDocumentationCommentId() ?? symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); public static bool IsJsonDefinedType(this ITypeSymbol symbol, NumberMode numberMode, [NotNullWhen(true)] out JsonSchemaBuilder? schema) diff --git a/test/Generator/RootSyntaxVisitorTests/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs index 83310cc..b040507 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -247,6 +247,9 @@ public class Class_WithRequiredProperties public string? Default { get; set; } = "default"; } +// -- Not Tested -- // + + public class Class_ExtendsAbstractClass : AbstractClass { public override string Name { get; set; } @@ -333,7 +336,6 @@ public struct GameRoom public BridgeTable BridgeTable { get; set; } } -[SchemaTraversalMode(TraversalMode.Bases)] public abstract record Table(int PlayerCount) where T : BaseHand { public abstract T? DealerHand { get; } @@ -341,24 +343,25 @@ public abstract record Table(int PlayerCount) where T : BaseHand public IReadOnlyCollection Hands { get; } } -[SchemaTraversalMode(TraversalMode.Bases)] +[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record PokerTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Poker DealerHand { get; } } -[SchemaTraversalMode(TraversalMode.Bases)] +[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record BlackjackTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Blackjack DealerHand { get; } } -[SchemaTraversalMode(TraversalMode.Bases)] +[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record BridgeTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Bridge? DealerHand => null; } +[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record WhistTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Bridge? DealerHand => null; @@ -370,7 +373,7 @@ public abstract record BaseHand(int Size) public List Cards { get; set; } - [SchemaTraversalMode(TraversalMode.Bases)] + [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Poker() : BaseHand(5) { public override string Game => "Poker"; @@ -378,7 +381,7 @@ public record Poker() : BaseHand(5) public bool IsRoyalFlush => Cards.Count == Size && Cards.All(c => c.IsFaceCard); } - [SchemaTraversalMode(TraversalMode.Bases)] + [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Blackjack() : BaseHand(2) { public override string Game => "Blackjack"; @@ -393,7 +396,7 @@ public record Blackjack() : BaseHand(2) }); } - [SchemaTraversalMode(TraversalMode.Bases)] + [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Bridge() : BaseHand(13) { public override string Game => "Bridge"; diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt index 5f28270..8a61578 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_ExtendsAbstractClass.verified.txt @@ -1 +1,25 @@ - \ No newline at end of file +{ + "$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_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/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt new file mode 100644 index 0000000..4149796 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_Interfaces.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class extends abstract class", + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "required": [ + "name" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt new file mode 100644 index 0000000..4149796 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/Traversal_SymbolOnly.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Class extends abstract class", + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "required": [ + "name" + ] +} \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index 809a37d..7fc8253 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -1,7 +1,4 @@ using System; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Schema; using System.Threading.Tasks; using Json.Schema; using SharpSchema.Annotations; @@ -40,23 +37,24 @@ internal static Task Verify(string schemaString, string testName, string paramet } [Theory] - [InlineData(nameof(Record_WithValueParameters))] - [InlineData(nameof(Record_WithDefaultValueParameter))] - [InlineData(nameof(Record_WithValueParametersAndProperty))] - [InlineData(nameof(Record_WithValueParametersAndPropertyInitializer))] - [InlineData(nameof(Record_WithDefaultValueParametersAndConstantProperty))] - [InlineData(nameof(Record_WithDocComments))] - [InlineData(nameof(Record_WithIgnoredParameter))] - [InlineData(nameof(Class_WithDocComments))] [InlineData(nameof(Class_WithArrayProperties))] [InlineData(nameof(Class_WithDictionaryProperties))] - [InlineData(nameof(Class_WithInvalidProperties))] + [InlineData(nameof(Class_WithDocComments))] [InlineData(nameof(Class_WithIgnoredProperty))] + [InlineData(nameof(Class_WithInvalidProperties))] [InlineData(nameof(Class_WithRequiredProperties))] - [InlineData(nameof(Struct_WithNullableValueTypes))] [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) { @@ -148,7 +146,7 @@ public Task Verify_EnumMode(EnumMode enumHandling) [InlineData(TraversalMode.Bases)] [InlineData(TraversalMode.Interfaces)] [InlineData(TraversalMode.Full)] - [Theory(Skip = "Traversal is not correctly implemented.")] + [Theory] public Task Verify_Traversal(TraversalMode traversal) { GeneratorOptions options = new() From 8af246281e446a5306a39df88b2ddbbb248f8ffd Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:37:41 +0000 Subject: [PATCH 09/11] Refactor schema generation structure in SharpSchema This commit introduces a `RootContext` class to encapsulate the context for schema generation, simplifying the `LeafSyntaxVisitor` and `NamedTypeResolver` classes. The `CommonSchemas` class has been enhanced for better handling of unsupported objects and schema references. Visitor classes have been documented for clarity, and new utility methods have been added to improve schema generation options. Additionally, property and parameter handling has been refined, and test data has been updated to ensure accuracy in generated schemas. These changes enhance maintainability, readability, and functionality of the code. --- .../LeafSyntaxVisitor.cs | 243 ++++++++++------- .../Model/CommonSchemas.cs | 9 +- .../Model/DeclaredTypePair.cs | 6 + src/SharpSchema.Generator/Model/Metadata.cs | 4 + .../Model/SharedContext.cs | 24 ++ .../NamedTypeResolver.cs | 204 ++++++++++++++ .../NamedTypeSymbolVisitor.cs | 250 ------------------ .../{EnumSymbolVisitor.cs => EnumResolver.cs} | 14 +- .../RootSyntaxVisitor.cs | 32 +-- .../Utilities/CompilationExtensions.cs | 4 +- .../Utilities/GeneratorOptionsExtensions.cs | 21 ++ .../Utilities/RootContextExtensions.cs | 13 + .../Utilities/SymbolExtensions.cs | 9 + src/SharpSchema.Generator/Utilities/Throw.cs | 7 +- .../_docs/visitor-classes.md | 48 ++++ .../RootSyntaxVisitorTests/TestData.cs | 9 +- ...ons_Class_WithArrayProperties.verified.txt | 10 +- ...lass_WithDictionaryProperties.verified.txt | 10 +- ..._Class_WithRequiredProperties.verified.txt | 14 +- ...ions_Class_WithSchemaOverride.verified.txt | 6 +- .../DefaultOptions_GameHall.verified.txt | 204 ++++++++++---- ...ons_Record_WithSchemaOverride.verified.txt | 6 +- ...Struct_WithNullableValueTypes.verified.txt | 64 ++--- .../EnumMode_String.verified.txt | 58 ++-- .../EnumMode_UnderlyingType.verified.txt | 16 +- .../NumberMode_StrictDefs.verified.txt | 64 ++--- .../RootSyntaxVisitorTests/VerifyTests.cs | 11 +- 27 files changed, 784 insertions(+), 576 deletions(-) create mode 100644 src/SharpSchema.Generator/Model/DeclaredTypePair.cs create mode 100644 src/SharpSchema.Generator/Model/SharedContext.cs create mode 100644 src/SharpSchema.Generator/NamedTypeResolver.cs delete mode 100644 src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs rename src/SharpSchema.Generator/Resolvers/{EnumSymbolVisitor.cs => EnumResolver.cs} (75%) create mode 100644 src/SharpSchema.Generator/Utilities/RootContextExtensions.cs create mode 100644 src/SharpSchema.Generator/_docs/visitor-classes.md diff --git a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs index d5a3005..f5d0c41 100644 --- a/src/SharpSchema.Generator/LeafSyntaxVisitor.cs +++ b/src/SharpSchema.Generator/LeafSyntaxVisitor.cs @@ -1,5 +1,5 @@ -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; using Json.Schema; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -15,29 +15,15 @@ namespace SharpSchema.Generator; internal partial class LeafSyntaxVisitor : CSharpSyntaxVisitor { - private readonly Compilation _compilation; + private readonly RootContext _context; private readonly GeneratorOptions _options; - private readonly SemanticModelCache _semanticModelCache; - private readonly MemberMeta.SymbolVisitor _metadataVisitor; - private readonly CollectionResolver _collectionResolver; - private readonly Dictionary _cachedTypeSchemas; - private readonly Dictionary _cachedAbstractSymbols; - public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticModelCache, GeneratorOptions options) + public LeafSyntaxVisitor(RootContext context, GeneratorOptions options) { - _compilation = compilation; + _context = context; _options = options; - _semanticModelCache = semanticModelCache; - _metadataVisitor = new(); - _collectionResolver = new(compilation); - _cachedTypeSchemas = []; - _cachedAbstractSymbols = []; } - internal Dictionary CachedTypeSchemas => _cachedTypeSchemas; - - internal Dictionary CachedAbstractSymbols => _cachedAbstractSymbols; - [ExcludeFromCodeCoverage] public override Builder? DefaultVisit(SyntaxNode node) { @@ -79,22 +65,22 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod Throw.IfNullArgument(node); using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); - if (node.GetDeclaredSymbol(_semanticModelCache) is not INamedTypeSymbol enumSymbol) + 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 (!_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) - { - MemberMeta metadata = Throw.ForUnexpectedNull(_metadataVisitor.Visit(enumSymbol)); + if (_context.CachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + return CommonSchemas.DefRef(cacheKey); - Builder builder = EnumSymbolVisitor.Instance.Visit(enumSymbol, _options) ?? - CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); + MemberMeta metadata = Throw.IfUnexpectedNull(MemberMeta.SymbolVisitor.Default.Visit(enumSymbol)); - _cachedTypeSchemas[cacheKey] = builder.ApplyMemberMeta(metadata); - } + Builder builder = EnumResolver.Resolve(enumSymbol, _options) ?? + CommonSchemas.UnsupportedObject(Unsupported.EnumMessage, node.Identifier); + + _context.CachedTypeSchemas[cacheKey] = builder.ApplyMemberMeta(metadata); return CommonSchemas.DefRef(cacheKey); } @@ -105,12 +91,14 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod using Tracer.TraceScope trace = Tracer.Enter(node.Identifier.Text); - TypeInfo typeInfo = node.GetTypeInfo(_semanticModelCache); + TypeInfo typeInfo = node.GetTypeInfo(_context.SemanticModelCache); if (typeInfo.ConvertedType is not ITypeSymbol typeSymbol) return CommonSchemas.UnsupportedObject(Unsupported.IdentifierMessage, node.Identifier); - return this.Visit(typeSymbol.FindDeclaringSyntax()) - ?? CommonSchemas.UnsupportedObject(Unsupported.DeclarationMessage, node.Identifier); + if (typeSymbol.FindDeclaringSyntax() is SyntaxNode syntaxNode) + return this.Visit(syntaxNode); + + return null; } public override Builder? VisitGenericName(GenericNameSyntax node) @@ -121,70 +109,78 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod if (node.IsUnboundGenericName) return CommonSchemas.UnsupportedObject(Unsupported.UnboundGenericMessage, node.Identifier); - SemanticModel semanticModel = _semanticModelCache.GetSemanticModel(node); - - if (semanticModel.GetTypeInfo(node).Type is not INamedTypeSymbol boundTypeSymbol) + if (node.GetTypeInfo(_context.SemanticModelCache).Type is not INamedTypeSymbol boundTypeSymbol) return CommonSchemas.UnsupportedObject(Unsupported.TypeSymbolMessage, node.Identifier.Text); - Builder? elementSchema = null; - var (kind, keyType, elementSymbol) = _collectionResolver.Resolve(boundTypeSymbol); - if (kind is CollectionKind.Dictionary or CollectionKind.Array) + if (boundTypeSymbol.HasUnresolvedTypeArguments()) { - elementSchema = node.TypeArgumentList.Arguments.Last().Accept(this); + trace.WriteLine("Unresolved type arguments found."); + return null; } - if (kind is CollectionKind.Dictionary) + var (kind, keyType, elementSymbol) = _context.CollectionResolver.Resolve(boundTypeSymbol); + + return kind switch { - if (elementSchema is null) + 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); - Builder builder = CommonSchemas.Object; + if (keyType is SchemaValueType.String) + return CommonSchemas.Object.AdditionalProperties(elementSchema); - if (keyType is not SchemaValueType.String) + return _options.DictionaryKeyMode switch { - switch (_options.DictionaryKeyMode) - { - case DictionaryKeyMode.Skip: - return null; - case DictionaryKeyMode.Strict: - return CommonSchemas.UnsupportedObject(Unsupported.KeyTypeMessage, keyType); - case DictionaryKeyMode.Loose: - builder = builder.Comment($"Key type '{keyType}' must be convertible to string"); - break; - case DictionaryKeyMode.Silent: - break; - default: - throw new InvalidOperationException($"Unexpected dictionary key mode '{_options.DictionaryKeyMode}'."); - } - } - - return builder.AdditionalProperties(elementSchema); + 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) + }; } - if (kind is CollectionKind.Array) + 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? boundTypeBuilder = this.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 (_cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + Builder? HandleGenericType(GenericNameSyntax node, INamedTypeSymbol boundTypeSymbol) { - IReadOnlyList? currentOneOf = cachedSchema.Get()?.Schemas; - cachedSchema = currentOneOf is not null - ? cachedSchema.OneOf(currentOneOf.Append(boundTypeBuilder)) - : CommonSchemas.Object.OneOf(cachedSchema, boundTypeBuilder); - } + 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(); - return boundTypeBuilder; + 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) @@ -192,11 +188,10 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod Throw.IfNullArgument(node); using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString()); - Builder? elementSchema = node.ElementType.Accept(this); - if (elementSchema is null) - return CommonSchemas.UnsupportedObject(Unsupported.NullableElementMessage, node.ElementType); + if (node.ElementType.Accept(this) is Builder elementSchema) + return CommonSchemas.Nullable(elementSchema); - return CommonSchemas.Nullable(elementSchema); + return null; } public override Builder? VisitPredefinedType(PredefinedTypeSyntax node) @@ -210,13 +205,13 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod if (node.Keyword.IsKind(SyntaxKind.BoolKeyword)) return CommonSchemas.Boolean; - if (_semanticModelCache.GetSemanticModel(node).GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol) + 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 && _cachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) + + if (shouldCache && _context.CachedTypeSchemas.TryGetValue(cacheKey, out Builder? cachedSchema)) return CommonSchemas.DefRef(cacheKey); if (!typeSymbol.IsJsonDefinedType(_options.NumberMode, out Builder? valueTypeSchema)) @@ -224,7 +219,7 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod if (shouldCache) { - _cachedTypeSchemas[cacheKey] = valueTypeSchema; + _context.CachedTypeSchemas[cacheKey] = valueTypeSchema; return CommonSchemas.DefRef(cacheKey); } @@ -236,44 +231,93 @@ public LeafSyntaxVisitor(Compilation compilation, SemanticModelCache semanticMod Throw.IfNullArgument(node); using Tracer.TraceScope trace = Tracer.Enter(node.Kind().ToString()); - Builder? elementSchema = node.ElementType.Accept(this); - if (elementSchema is null) - return CommonSchemas.UnsupportedObject(Unsupported.ArrayElementMessage, node.ElementType); + if (node.ElementType.Accept(this) is Builder elementSchema) + return CommonSchemas.ArrayOf(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(_semanticModelCache) is not INamedTypeSymbol typeSymbol) + 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); - return symbol.Accept( - new NamedTypeSymbolVisitor( // Always create a new visitor instance - this, - _semanticModelCache, - _options), - argument: null) - ?? CommonSchemas.UnsupportedObject(symbol.Name); + NamedTypeResolver visitor = new(_context); + return visitor.Resolve(symbol, _options) ?? CommonSchemas.UnsupportedObject(symbol.Name); } private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node, Tracer.TraceScope trace) { - if (_semanticModelCache.GetSemanticModel(node).GetDeclaredSymbol(node) is not INamedTypeSymbol typeSymbol) + if (node.GetDeclaredSymbol(_context.SemanticModelCache) is not INamedTypeSymbol typeSymbol) return CommonSchemas.UnsupportedObject(Unsupported.SymbolMessage, node.Identifier.ValueText); string typeId = typeSymbol.GetDefCacheKey(); - if (_cachedTypeSchemas.TryGetValue(typeId, out _)) + + if (_context.CachedTypeSchemas.TryGetValue(typeId, out _)) return CommonSchemas.DefRef(typeId); if (typeSymbol.GetOverrideSchema() is Builder overrideSchema) @@ -282,20 +326,21 @@ public Builder CreateTypeSchema(INamedTypeSymbol symbol, TypeDeclarationSyntax n if (typeSymbol.IsAbstract) { trace.WriteLine($"Found abstract type '{typeSymbol.Name}'."); - if (!_cachedAbstractSymbols.TryGetValue(typeId, out _)) + if (!_context.CachedAbstractSymbols.ContainsKey(typeId)) { - _cachedAbstractSymbols.Add(typeId, typeSymbol); + _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, schemaTraversal.Get(0) ?? _options.TraversalMode); + Builder builder = CreateTypeSchema(typeSymbol, node, effectiveTraversalMode); - _cachedTypeSchemas[typeId] = builder; + _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 411bdef..e2995a9 100644 --- a/src/SharpSchema.Generator/Model/CommonSchemas.cs +++ b/src/SharpSchema.Generator/Model/CommonSchemas.cs @@ -96,8 +96,13 @@ static CommonSchemas() public static Builder UnsupportedObject(string value) => new Builder() .UnsupportedObject(value); - public static Builder UnsupportedObject(string format, params object[] args) => new Builder() - .UnsupportedObject(string.Format(format, args)); + 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)); 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 c7268e1..cd3ddd5 100644 --- a/src/SharpSchema.Generator/Model/Metadata.cs +++ b/src/SharpSchema.Generator/Model/Metadata.cs @@ -42,6 +42,10 @@ internal class SymbolVisitor : SymbolVisitor public static SymbolVisitor Default { get; } = new(); + private SymbolVisitor() + { + } + /// /// Visits a named type symbol and extracts . /// 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..fe22aab --- /dev/null +++ b/src/SharpSchema.Generator/NamedTypeResolver.cs @@ -0,0 +1,204 @@ +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); + 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(); + primaryCtor.Parameters.ForEach(param => + { + string propertyName = param.Name.Camelize(); + + (Builder? typeBuilder, bool isRequired) = this.VisitParameter(param, options); + if (typeBuilder is null) + return; + + properties.Add(propertyName, typeBuilder); + + if (isRequired) + _requiredProperties.Add(propertyName); + }); + } + + symbol.GetMembers().OfType().ForEach(prop => + { + string propertyName = prop.Name.Camelize(); + + (Builder? valueBuilder, bool isRequired) = this.VisitProperty(prop, options); + if (valueBuilder is null) + return; + + 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"); + + if (!options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) + return (null, false); + + 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"); + + if (!options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) + return (null, false); + + 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/NamedTypeSymbolVisitor.cs b/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs deleted file mode 100644 index 0591d90..0000000 --- a/src/SharpSchema.Generator/NamedTypeSymbolVisitor.cs +++ /dev/null @@ -1,250 +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 Humanizer; -using SharpSchema.Annotations; -using System.Text.Json.Nodes; - -namespace SharpSchema.Generator; - -using Builder = JsonSchemaBuilder; - -internal class NamedTypeSymbolVisitor : SymbolVisitor -{ - public class StateContainer - { - internal bool? WasLastPropertyRequired { get; set; } - } - - private readonly CSharpSyntaxVisitor _syntaxVisitor; - private readonly SemanticModelCache _semanticModelCache; - private readonly GeneratorOptions _options; - - public NamedTypeSymbolVisitor( - CSharpSyntaxVisitor visitor, - SemanticModelCache semanticModelCache, - GeneratorOptions options) - { - _syntaxVisitor = visitor; - _semanticModelCache = semanticModelCache; - _options = options; - } - - protected override Builder? DefaultResult { get; } - - public override Builder? VisitNamedType(INamedTypeSymbol symbol, StateContainer? state) - { - state ??= new StateContainer(); - using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); - - ObjectAttributes attributes = symbol.GetObjectAttributes(_options.TraversalMode); - - Dictionary properties = new(StringComparer.OrdinalIgnoreCase); - HashSet _requiredProperties = new(StringComparer.OrdinalIgnoreCase); - - if (symbol.IsRecord) - { - IMethodSymbol primaryCtor = symbol.Constructors.First(); - primaryCtor.Parameters.ForEach(param => - { - string propertyName = param.Name.Camelize(); - if (symbol.GetOverrideSchema() is Builder overrideSchema) - properties.Add(propertyName, overrideSchema); - - Builder? typeBuilder = param.Accept(this, state); - if (typeBuilder is null) - return; - - properties.Add(propertyName, typeBuilder); - - if (state.WasLastPropertyRequired == true) - _requiredProperties.Add(propertyName); - - state.WasLastPropertyRequired = null; - }); - } - - symbol.GetMembers().OfType().ForEach(prop => - { - string propertyName = prop.Name.Camelize(); - - Builder? valueBuilder = prop.Accept(this, state); - if (valueBuilder is null) - return; - - properties.Add(propertyName, valueBuilder); - - if (state.WasLastPropertyRequired == true) - _requiredProperties.Add(propertyName); - - state.WasLastPropertyRequired = null; - }); - - 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); - - TraversalMode mode = _options.TraversalMode; - if (attributes.TraversalMode.Get(0) is TraversalMode traversalMode) - mode = traversalMode; - - if (mode.CheckFlag(TraversalMode.Bases)) - { - INamedTypeSymbol? @base = symbol.BaseType; - while (@base is not null && @base.SpecialType is not SpecialType.System_Object) - { - NamedTypeSymbolVisitor baseSymbolVisitor = new(_syntaxVisitor, _semanticModelCache, _options with { TraversalMode = mode }); - if (@base.Accept(baseSymbolVisitor, state) is Builder baseBuilder) - { - builder = builder.MergeProperties(baseBuilder); - } - - @base = @base.BaseType; - } - } - - if (_options.TraversalMode.CheckFlag(TraversalMode.Interfaces)) - { - symbol.AllInterfaces.ForEach(@interface => - { - NamedTypeSymbolVisitor interfaceSymbolVisitor = new(_syntaxVisitor, _semanticModelCache, _options with { TraversalMode = mode }); - if (@interface.Accept(interfaceSymbolVisitor, state) is Builder interfaceBuilder) - { - builder = builder.MergeProperties(interfaceBuilder); - } - }); - } - - return builder; - } - - public override Builder? VisitProperty(IPropertySymbol symbol, StateContainer? state) - { - using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); - - if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) - return null; - - Builder? typeBuilder = null; - if (symbol.GetOverrideSchema() is Builder overrideSchema) - typeBuilder = overrideSchema; - - bool isRequired = symbol.IsRequired || !IsNullable(symbol.NullableAnnotation); - - // Excludes generated properties. - if (typeBuilder is null && symbol.FindDeclaringSyntax() is PropertyDeclarationSyntax pdx) - { - typeBuilder = pdx.Type.Accept(_syntaxVisitor); - if (typeBuilder is null) - return null; - - if (pdx.ExpressionBody is ArrowExpressionClauseSyntax aec - && GetConstantValue(aec.Expression) is JsonNode constantValue) - { - typeBuilder = CommonSchemas.Const(constantValue); - isRequired = true; - } - else if (ExtractDefaultValue(pdx) is JsonNode defaultValue) - { - typeBuilder = typeBuilder.Default(defaultValue); - isRequired = false; - } - } - - if (typeBuilder is null) - return null; - - if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) - typeBuilder = typeBuilder.ApplyMemberMeta(meta); - - state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); - - return typeBuilder; - - // -- Local functions -- - - JsonNode? ExtractDefaultValue(PropertyDeclarationSyntax propertyDeclaration) - { - return propertyDeclaration.Initializer is EqualsValueClauseSyntax evc - ? GetConstantValue(evc.Value) - : null; - } - - JsonNode? GetConstantValue(SyntaxNode node) - { - SemanticModel sm = _semanticModelCache.GetSemanticModel(node); - return sm.GetConstantValue(node) is Optional optValue - && optValue.HasValue - ? JsonValue.Create(optValue.Value) - : (JsonNode?)null; - } - } - - public override Builder? VisitParameter(IParameterSymbol symbol, StateContainer? state) - { - using var scope = Tracer.Enter($"[SYMBOL] {symbol.Name}"); - - if (!_options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) - return null; - - Builder? typeBuilder = null; - if (symbol.GetOverrideSchema() is Builder overrideSchema) - typeBuilder = overrideSchema; - - bool isRequired = !IsNullable(symbol.NullableAnnotation); - - // Excludes implicitly-typed parameters. - if (typeBuilder is null && symbol.FindDeclaringSyntax() is ParameterSyntax px && px.Type is TypeSyntax tx) - { - typeBuilder = px.Type.Accept(_syntaxVisitor); - } - - if (typeBuilder is null) - return null; - - 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); - - state!.WasLastPropertyRequired = EvaluateSchemaRequired(symbol, isRequired); - - return typeBuilder; - - } - - 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/EnumSymbolVisitor.cs b/src/SharpSchema.Generator/Resolvers/EnumResolver.cs similarity index 75% rename from src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs rename to src/SharpSchema.Generator/Resolvers/EnumResolver.cs index e8f655d..9bcbae1 100644 --- a/src/SharpSchema.Generator/Resolvers/EnumSymbolVisitor.cs +++ b/src/SharpSchema.Generator/Resolvers/EnumResolver.cs @@ -9,20 +9,14 @@ namespace SharpSchema.Generator.Resolvers; using Builder = JsonSchemaBuilder; -internal class EnumSymbolVisitor : SymbolVisitor +internal class EnumResolver { - public static EnumSymbolVisitor Instance { get; } = new EnumSymbolVisitor(); - - private EnumSymbolVisitor() { } - - protected override Builder? DefaultResult => null; - - public override Builder? VisitNamedType(INamedTypeSymbol symbol, GeneratorOptions options) + public static Builder? Resolve(INamedTypeSymbol symbol, GeneratorOptions options) { using var trace = Tracer.Enter(symbol.Name); if (symbol.TypeKind != TypeKind.Enum) - return DefaultResult; + return null; trace.WriteLine($"{options.EnumMode}"); @@ -47,6 +41,6 @@ private EnumSymbolVisitor() { } } } - return DefaultResult; + return null; } } diff --git a/src/SharpSchema.Generator/RootSyntaxVisitor.cs b/src/SharpSchema.Generator/RootSyntaxVisitor.cs index ddd9d4e..c0ad316 100644 --- a/src/SharpSchema.Generator/RootSyntaxVisitor.cs +++ b/src/SharpSchema.Generator/RootSyntaxVisitor.cs @@ -15,9 +15,8 @@ namespace SharpSchema.Generator; ///
public class RootSyntaxVisitor : CSharpSyntaxVisitor { - private readonly LeafSyntaxVisitor _cachingVisitor; - private readonly Compilation _compilation; - private readonly SemanticModelCache _semanticModelCache; + private readonly LeafSyntaxVisitor _leafSyntaxVisitor; + private readonly RootContext _context; /// /// Initializes a new instance of the class. @@ -29,9 +28,8 @@ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) Throw.IfNullArgument(compilation); Throw.IfNullArgument(options); - _compilation = compilation; - _semanticModelCache = new(compilation); - _cachingVisitor = new(compilation, _semanticModelCache, options); + _context = new RootContext(compilation, new SemanticModelCache(compilation)); + _leafSyntaxVisitor = new(_context, options); } /// @@ -103,26 +101,26 @@ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) /// A JSON schema builder or null. private Builder? VisitTypeDeclaration(TypeDeclarationSyntax node) { - Builder builder = _cachingVisitor.CreateTypeSchema(node); + Builder builder = _leafSyntaxVisitor.CreateTypeSchema(node); - Dictionary cachedAbstractSymbols = _cachingVisitor.CachedAbstractSymbols; - Dictionary cachedTypeSchemas = _cachingVisitor.CachedTypeSchemas; + Dictionary cachedAbstractSymbols = _context.CachedAbstractSymbols; + Dictionary cachedTypeSchemas = _context.CachedTypeSchemas; if (cachedAbstractSymbols.Count > 0) { using var trace = Tracer.Enter("Building abstract type schemas."); - ImmutableArray namedTypes = [.. _compilation.GetAllNamedTypes(_semanticModelCache)]; + 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 + IEnumerable subTypes = namedTypes .Where(t => t.Symbol.InheritsFrom(abstractSymbol)); List subSchemas = []; - foreach (NamedType subType in subTypes) + foreach (DeclaredTypePair subType in subTypes) { // Check if the sub-type is already cached string cacheKey = subType.Symbol.GetDefCacheKey(); @@ -134,7 +132,7 @@ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) else { trace.WriteLine($"Building schema for '{subType.Symbol.Name}'."); - Builder? subSchema = _cachingVisitor.CreateTypeSchema(subType.Symbol, subType.SyntaxNode); + Builder? subSchema = _leafSyntaxVisitor.CreateTypeSchema(subType.Symbol, subType.SyntaxNode); if (subSchema is not null) { cachedTypeSchemas[cacheKey] = subSchema; @@ -150,15 +148,17 @@ public RootSyntaxVisitor(Compilation compilation, GeneratorOptions options) cachedTypeSchemas[key] = abstractSchema; } else + { trace.WriteLine($"No sub-types found for '{abstractSymbol.Name}'."); + } } } if (cachedTypeSchemas.Count > 0) { - builder.Defs(cachedTypeSchemas.ToDictionary( - p => p.Key, - p => p.Value.Build())); + builder.Defs(cachedTypeSchemas.ToImmutableSortedDictionary( + p => p.Key, + p => p.Value.Build())); } return builder; diff --git a/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs index a4acebb..e9a3b36 100644 --- a/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/CompilationExtensions.cs @@ -4,11 +4,9 @@ namespace SharpSchema.Generator.Utilities; -internal readonly record struct NamedType(TypeDeclarationSyntax SyntaxNode, INamedTypeSymbol Symbol); - internal static class CompilationExtensions { - internal static IEnumerable GetAllNamedTypes(this Compilation compilation, SemanticModelCache semanticModelCache) + internal static IEnumerable GetAllNamedTypes(this Compilation compilation, SemanticModelCache semanticModelCache) { foreach (SyntaxTree tree in compilation.SyntaxTrees) { diff --git a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs index 01b1dc5..53d4c84 100644 --- a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using SharpSchema.Annotations; +using SharpSchema.Generator.Model; namespace SharpSchema.Generator.Utilities; @@ -32,4 +33,24 @@ static bool ShouldProcessAccessibility(Accessibility accessibility, Accessibilit }; } } + + 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: attributes.DictionaryKeyMode.Get(0) ?? 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/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 4e908c3..5963327 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Json.Schema; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using SharpSchema.Annotations; using SharpSchema.Generator.Model; @@ -303,6 +304,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. /// 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/_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/RootSyntaxVisitorTests/TestData.cs b/test/Generator/RootSyntaxVisitorTests/TestData.cs index b040507..b24e852 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestData.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.cs @@ -244,6 +244,8 @@ public class Class_WithRequiredProperties public string? Optional { get; set; } [SchemaRequired] + public string? DefaultRequired { get; set; } = "default"; + public string? Default { get; set; } = "default"; } @@ -343,25 +345,21 @@ public abstract record Table(int PlayerCount) where T : BaseHand public IReadOnlyCollection Hands { get; } } -[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record PokerTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Poker DealerHand { get; } } -[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record BlackjackTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Blackjack DealerHand { get; } } -[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record BridgeTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Bridge? DealerHand => null; } -[SchemaTraversalMode(TraversalMode.SymbolOnly)] public record WhistTable(int PlayerCount) : Table(PlayerCount) { public override BaseHand.Bridge? DealerHand => null; @@ -373,7 +371,6 @@ public abstract record BaseHand(int Size) public List Cards { get; set; } - [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Poker() : BaseHand(5) { public override string Game => "Poker"; @@ -381,7 +378,6 @@ public record Poker() : BaseHand(5) public bool IsRoyalFlush => Cards.Count == Size && Cards.All(c => c.IsFaceCard); } - [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Blackjack() : BaseHand(2) { public override string Game => "Blackjack"; @@ -396,7 +392,6 @@ public record Blackjack() : BaseHand(2) }); } - [SchemaTraversalMode(TraversalMode.SymbolOnly)] public record Bridge() : BaseHand(13) { public override string Game => "Bridge"; diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt index e258b4f..aa1a33a 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithArrayProperties.verified.txt @@ -74,11 +74,6 @@ "stringImmutableArray" ], "$defs": { - "T:System.Int32": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647 - }, "T:SharpSchema.Generator.TestData.Address": { "type": "object", "title": "Address", @@ -96,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/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt index 09b726f..e8ff5bd 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithDictionaryProperties.verified.txt @@ -23,11 +23,6 @@ "referenceTypes" ], "$defs": { - "T:System.Int32": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647 - }, "T:SharpSchema.Generator.TestData.Address": { "type": "object", "title": "Address", @@ -45,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/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt index 6775ddc..2d7d24e 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithRequiredProperties.verified.txt @@ -29,6 +29,18 @@ ], "title": "Optional" }, + "defaultRequired": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "default", + "title": "Default required" + }, "default": { "oneOf": [ { @@ -45,7 +57,7 @@ "required": [ "requiredInt", "required", - "default" + "defaultRequired" ], "$defs": { "T:System.Int32": { diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt index 00932a9..7989690 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Class_WithSchemaOverride.verified.txt @@ -5,13 +5,11 @@ "properties": { "name": { "type": "string", - "maxLength": 50, - "title": "Name" + "maxLength": 50 }, "age": { "type": "integer", - "minimum": 0, - "title": "Age" + "minimum": 0 } }, "required": [ diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt index 18119cf..638f365 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_GameHall.verified.txt @@ -20,63 +20,94 @@ "rooms" ], "$defs": { - "T:System.Int32": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647 - }, - "T:SharpSchema.Generator.TestData.BaseHand.Poker": { + "T:SharpSchema.Generator.TestData.BaseHand.Blackjack": { "type": "object", - "title": "Poker", + "title": "Blackjack", "properties": { "game": { - "const": "Poker", - "title": "Game" + "const": "Blackjack", + "title": "Game", + "type": "string" }, - "isRoyalFlush": { - "type": "boolean", - "title": "Is royal flush" + "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", - "isRoyalFlush" + "value" ] }, - "T:SharpSchema.Generator.TestData.PokerTable": { + "T:SharpSchema.Generator.TestData.BaseHand.Bridge": { "type": "object", - "title": "Poker table", + "title": "Bridge", "properties": { - "playerCount": { + "game": { + "const": "Bridge", + "title": "Game", + "type": "string" + }, + "isNoTrump": { + "type": "boolean", + "title": "Is no trump" + }, + "size": { "$ref": "#/$defs/T:System.Int32", - "title": "Player count" + "title": "Size" }, - "dealerHand": { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Poker", - "title": "Dealer hand" + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" } }, "required": [ - "playerCount", - "dealerHand" + "game", + "isNoTrump" ] }, - "T:SharpSchema.Generator.TestData.BaseHand.Blackjack": { + "T:SharpSchema.Generator.TestData.BaseHand.Poker": { "type": "object", - "title": "Blackjack", + "title": "Poker", "properties": { "game": { - "const": "Blackjack", - "title": "Game" + "const": "Poker", + "title": "Game", + "type": "string" }, - "value": { + "isRoyalFlush": { + "type": "boolean", + "title": "Is royal flush" + }, + "size": { "$ref": "#/$defs/T:System.Int32", - "title": "Value" + "title": "Size" + }, + "cards": { + "type": "array", + "items": { + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.Card" + }, + "title": "Cards" } }, "required": [ "game", - "value" + "isRoyalFlush" ] }, "T:SharpSchema.Generator.TestData.BlackjackTable": { @@ -97,24 +128,6 @@ "dealerHand" ] }, - "T:SharpSchema.Generator.TestData.BaseHand.Bridge": { - "type": "object", - "title": "Bridge", - "properties": { - "game": { - "const": "Bridge", - "title": "Game" - }, - "isNoTrump": { - "type": "boolean", - "title": "Is no trump" - } - }, - "required": [ - "game", - "isNoTrump" - ] - }, "T:SharpSchema.Generator.TestData.BridgeTable": { "type": "object", "title": "Bridge table", @@ -139,6 +152,58 @@ "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", @@ -172,28 +237,22 @@ "bridgeTable" ] }, - "T:SharpSchema.Generator.TestData.WhistTable": { + "T:SharpSchema.Generator.TestData.PokerTable": { "type": "object", - "title": "Whist table", + "title": "Poker table", "properties": { "playerCount": { "$ref": "#/$defs/T:System.Int32", "title": "Player count" }, "dealerHand": { - "oneOf": [ - { - "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Bridge" - }, - { - "type": "null" - } - ], + "$ref": "#/$defs/T:SharpSchema.Generator.TestData.BaseHand.Poker", "title": "Dealer hand" } }, "required": [ - "playerCount" + "playerCount", + "dealerHand" ] }, "T:SharpSchema.Generator.TestData.Table`1": { @@ -212,6 +271,35 @@ "$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_WithSchemaOverride.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt index 81c0e28..25a8fe0 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Record_WithSchemaOverride.verified.txt @@ -5,13 +5,11 @@ "properties": { "name": { "type": "string", - "maxLength": 50, - "title": "Name" + "maxLength": 50 }, "age": { "type": "integer", - "minimum": 0, - "title": "Age" + "minimum": 0 } }, "required": [ diff --git a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt index 389e820..26c6c93 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/DefaultOptions_Struct_WithNullableValueTypes.verified.txt @@ -148,65 +148,65 @@ } }, "$defs": { - "T:System.Int32": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647 - }, "T:System.Byte": { "type": "integer", "minimum": 0, "maximum": 255 }, - "T:System.SByte": { - "type": "integer", - "minimum": -128, - "maximum": 127 + "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.UInt16": { - "type": "integer", - "minimum": 0, - "maximum": 65535 - }, - "T:System.UInt32": { + "T:System.Int32": { "type": "integer", - "minimum": 0, - "maximum": 4294967295 + "minimum": -2147483648, + "maximum": 2147483647 }, "T:System.Int64": { "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "T:System.UInt64": { + "T:System.SByte": { "type": "integer", - "minimum": 0, - "maximum": 18446744073709551615 + "minimum": -128, + "maximum": 127 }, "T:System.Single": { "type": "number", "minimum": -79228162514264337593543950335, "maximum": 79228162514264337593543950335 }, - "T:System.Double": { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335 + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 }, - "T:System.Decimal": { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335 + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 }, - "T:System.Char": { - "type": "string", - "minLength": 1, - "maxLength": 1 + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 } } } \ 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 index 63d8dd0..85d7d7f 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_String.verified.txt @@ -20,35 +20,6 @@ "deck" ], "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "string", - "enum": [ - "spades", - "hearts", - "clubs", - "diamonds" - ], - "title": "Suit kind" - }, - "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": { "type": "object", "title": "Card", @@ -71,6 +42,35 @@ "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 index cf2e0a7..6543c17 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/EnumMode_UnderlyingType.verified.txt @@ -20,14 +20,6 @@ "deck" ], "$defs": { - "T:SharpSchema.Generator.TestData.Card.SuitKind": { - "type": "integer", - "title": "Suit kind" - }, - "T:SharpSchema.Generator.TestData.Card.RankKind": { - "type": "integer", - "title": "Rank kind" - }, "T:SharpSchema.Generator.TestData.Card": { "type": "object", "title": "Card", @@ -50,6 +42,14 @@ "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_StrictDefs.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt index 47beea0..5383374 100644 --- a/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/NumberMode_StrictDefs.verified.txt @@ -77,65 +77,65 @@ "char" ], "$defs": { - "T:System.Int32": { - "type": "integer", - "minimum": -2147483648, - "maximum": 2147483647 - }, "T:System.Byte": { "type": "integer", "minimum": 0, "maximum": 255 }, - "T:System.SByte": { - "type": "integer", - "minimum": -128, - "maximum": 127 + "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.UInt16": { - "type": "integer", - "minimum": 0, - "maximum": 65535 - }, - "T:System.UInt32": { + "T:System.Int32": { "type": "integer", - "minimum": 0, - "maximum": 4294967295 + "minimum": -2147483648, + "maximum": 2147483647 }, "T:System.Int64": { "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "T:System.UInt64": { + "T:System.SByte": { "type": "integer", - "minimum": 0, - "maximum": 18446744073709551615 + "minimum": -128, + "maximum": 127 }, "T:System.Single": { "type": "number", "minimum": -79228162514264337593543950335, "maximum": 79228162514264337593543950335 }, - "T:System.Double": { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335 + "T:System.UInt16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 }, - "T:System.Decimal": { - "type": "number", - "minimum": -79228162514264337593543950335, - "maximum": 79228162514264337593543950335 + "T:System.UInt32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 }, - "T:System.Char": { - "type": "string", - "minLength": 1, - "maxLength": 1 + "T:System.UInt64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 } } } \ No newline at end of file diff --git a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index 7fc8253..fc3114b 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -58,23 +58,14 @@ internal static Task Verify(string schemaString, string testName, string paramet [InlineData(nameof(GameHall))] public Task Verify_DefaultOptions(string testName) { - Tracer.EnableTiming = true; - RootSyntaxVisitor visitor = _fixture.GetVisitor(GeneratorOptions.Default); JsonSchemaBuilder builder = _fixture.GetJsonSchemaBuilder(visitor, testName); _output.WriteSeparator(); + string schemaString = builder.Build().SerializeToJson(); _output.WriteLine(schemaString); _output.WriteSeparator(); - //// Get type instance from test name - //Type? type = Assembly.GetExecutingAssembly().GetType($"SharpSchema.Generator.TestData.{testName}"); - //if (type is not null) - //{ - // System.Text.Json.Nodes.JsonNode n = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, type, JsonSchemaExporterOptions.Default); - // _output.WriteLine(n.ToString()); - //} - return Verify(schemaString, "DefaultOptions", testName); } From bdb81d424d0a1ce5bee17e63ff9bcf188dba3d64 Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Fri, 7 Mar 2025 01:16:11 +0000 Subject: [PATCH 10/11] Enhance GeneratorOptions and add accessibility tests - Updated `GeneratorOptions` to include a constructor for copying values from another instance, improving flexibility. - Changed attribute usage in `SchemaAccessibilityModeAttribute.cs` from `SupportedMembers` to `SupportedTypes`. - Modified `NamedTypeResolver` to preserve original options when processing parameters and properties, enhancing schema generation accuracy. - Added new test classes and methods to verify accessibility overrides in schema generation. - Updated `TestDataFixture` to include a path to the new accessibility test data file. - Added or updated JSON schema verification files to reflect new accessibility rules. - Enhanced `VerifyTests` with new test cases for accessibility overrides. --- .../GeneratorOptions.cs | 16 ++++++ .../SchemaAccessibilityModeAttribute.cs | 2 +- .../NamedTypeResolver.cs | 30 +++++----- .../TestData.Accessibility.cs | 55 +++++++++++++++++++ .../RootSyntaxVisitorTests/TestDataFixture.cs | 13 ++++- ...e_Accessibility_ClassOverride.verified.txt | 19 +++++++ ...verride_Accessibility_Default.verified.txt | 14 +++++ ...e_Accessibility_NestedDefault.verified.txt | 52 ++++++++++++++++++ ..._Accessibility_NestedOverride.verified.txt | 52 ++++++++++++++++++ .../RootSyntaxVisitorTests/VerifyTests.cs | 19 +++++++ 10 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_ClassOverride.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedDefault.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_NestedOverride.verified.txt diff --git a/src/SharpSchema.Annotations/GeneratorOptions.cs b/src/SharpSchema.Annotations/GeneratorOptions.cs index d735c4a..188e71e 100644 --- a/src/SharpSchema.Annotations/GeneratorOptions.cs +++ b/src/SharpSchema.Annotations/GeneratorOptions.cs @@ -19,6 +19,22 @@ record GeneratorOptions( 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. /// diff --git a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs index b51fa67..f068991 100644 --- a/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs +++ b/src/SharpSchema.Annotations/SchemaAccessibilityModeAttribute.cs @@ -7,7 +7,7 @@ namespace SharpSchema.Annotations; /// /// Overrides the default traversal option for a given type. /// -[AttributeUsage(SchemaAttribute.SupportedMembers)] +[AttributeUsage(SchemaAttribute.SupportedTypes)] #if SHARPSCHEMA_ASSEMBLY public #else diff --git a/src/SharpSchema.Generator/NamedTypeResolver.cs b/src/SharpSchema.Generator/NamedTypeResolver.cs index fe22aab..ff6da04 100644 --- a/src/SharpSchema.Generator/NamedTypeResolver.cs +++ b/src/SharpSchema.Generator/NamedTypeResolver.cs @@ -27,6 +27,7 @@ public NamedTypeResolver( 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"); @@ -37,34 +38,41 @@ public NamedTypeResolver( if (symbol.IsRecord) { IMethodSymbol primaryCtor = symbol.Constructors.First(); - primaryCtor.Parameters.ForEach(param => + 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, options); + (Builder? typeBuilder, bool isRequired) = this.VisitParameter(param, originalOptions); if (typeBuilder is null) - return; + continue; properties.Add(propertyName, typeBuilder); if (isRequired) _requiredProperties.Add(propertyName); - }); + } } - symbol.GetMembers().OfType().ForEach(prop => + 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, options); + (Builder? valueBuilder, bool isRequired) = this.VisitProperty(prop, originalOptions); if (valueBuilder is null) - return; + continue; properties.Add(propertyName, valueBuilder); if (isRequired) _requiredProperties.Add(propertyName); - }); + } Builder builder = CommonSchemas.Object; if (symbol.Accept(MemberMeta.SymbolVisitor.Default) is MemberMeta meta) @@ -115,9 +123,6 @@ private PropertyResult VisitProperty(IPropertySymbol symbol, GeneratorOptions op scope.WriteLine(options.ToString(), "Options"); - if (!options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) - return (null, false); - bool isRequired = symbol.IsRequired || !IsNullable(symbol.NullableAnnotation); if (symbol.GetOverrideSchema() is Builder overrideBuilder) @@ -153,9 +158,6 @@ private PropertyResult VisitParameter(IParameterSymbol symbol, GeneratorOptions scope.WriteLine(options.ToString(), "Options"); - if (!options.ShouldProcess(symbol) || !symbol.IsValidForGeneration()) - return (null, false); - bool isRequired = !IsNullable(symbol.NullableAnnotation); if (symbol.GetOverrideSchema() is Builder overrideBuilder) diff --git a/test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs b/test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs new file mode 100644 index 0000000..9ce22c4 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace SharpSchema.Generator.TestData; + +using SharpSchema.Annotations; +#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. + +using Test.Generator.RootSyntaxVisitorTests; + +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/RootSyntaxVisitorTests/TestDataFixture.cs b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs index 17f8249..7bc7f10 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs @@ -28,13 +28,24 @@ public TestDataFixture() "RootSyntaxVisitorTests", "TestData.cs"); + string pathToAccessibility = PathHelper.GetRepoPath( + "test", + "Generator", + "RootSyntaxVisitorTests", + "TestData.Accessibility.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), 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/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt new file mode 100644 index 0000000..e5b3128 --- /dev/null +++ b/test/Generator/RootSyntaxVisitorTests/Verifications/AccessibilityOverride_Accessibility_Default.verified.txt @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "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_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/RootSyntaxVisitorTests/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index fc3114b..d502b74 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -69,6 +69,25 @@ public Task Verify_DefaultOptions(string testName) 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); + + } + [InlineData(DictionaryKeyMode.Loose)] [InlineData(DictionaryKeyMode.Strict)] [InlineData(DictionaryKeyMode.Silent)] From 9857c02ee6cd7d5c7dbfc4036367bc573f12215a Mon Sep 17 00:00:00 2001 From: trippwill <5862883+trippwill@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:35:23 +0000 Subject: [PATCH 11/11] Enhance ObjectAttributes and add dictionary key tests Updated `ObjectAttributes` and `PropertyAttributes` to reintroduce `TraversalMode` and `ValueRange` attribute handlers. Modified `GeneratorOptionsExtensions` to adjust how `DictionaryKeyMode` is handled. Added new classes in `TestData.Attributes.cs` for various dictionary key scenarios and updated test references accordingly. Created new JSON schema files for these scenarios and added a test method in `VerifyTests.cs` to validate the schemas, improving test coverage. --- .../Model/ObjectAttributes.cs | 7 +- .../Utilities/GeneratorOptionsExtensions.cs | 2 +- .../Utilities/SymbolExtensions.cs | 1 - ...ccessibility.cs => TestData.Attributes.cs} | 41 ++++++-- .../RootSyntaxVisitorTests/TestDataFixture.cs | 2 +- ...verride_DictionaryKey_Default.verified.txt | 35 +++++++ ..._DictionaryKey_NestedOverride.verified.txt | 93 +++++++++++++++++++ ...ictionaryKey_PropertyOverride.verified.txt | 39 ++++++++ .../RootSyntaxVisitorTests/VerifyTests.cs | 17 ++++ 9 files changed, 222 insertions(+), 15 deletions(-) rename test/Generator/RootSyntaxVisitorTests/{TestData.Accessibility.cs => TestData.Attributes.cs} (57%) create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_Default.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_NestedOverride.verified.txt create mode 100644 test/Generator/RootSyntaxVisitorTests/Verifications/DictionaryKeyOverride_DictionaryKey_PropertyOverride.verified.txt diff --git a/src/SharpSchema.Generator/Model/ObjectAttributes.cs b/src/SharpSchema.Generator/Model/ObjectAttributes.cs index 8865910..5c8b007 100644 --- a/src/SharpSchema.Generator/Model/ObjectAttributes.cs +++ b/src/SharpSchema.Generator/Model/ObjectAttributes.cs @@ -4,13 +4,13 @@ namespace SharpSchema.Generator.Model; internal record ObjectAttributes( AttributeHandler AccessibilityMode, - AttributeHandler DictionaryKeyMode, AttributeHandler EnumMode, AttributeHandler Meta, AttributeHandler Override, AttributeHandler PropertiesRange, AttributeHandler Root, - AttributeHandler TraversalMode); + AttributeHandler TraversalMode +); internal record PropertyAttributes( AttributeHandler Const, @@ -24,4 +24,5 @@ internal record PropertyAttributes( AttributeHandler Override, AttributeHandler Regex, AttributeHandler Required, - AttributeHandler ValueRange); + AttributeHandler ValueRange +); diff --git a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs index 53d4c84..add70be 100644 --- a/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/GeneratorOptionsExtensions.cs @@ -39,7 +39,7 @@ public static GeneratorOptions Override(this GeneratorOptions options, ObjectAtt return new GeneratorOptions( AccessibilityMode: attributes.AccessibilityMode.Get(0) ?? options.AccessibilityMode, TraversalMode: attributes.TraversalMode.Get(0) ?? (options.TraversalMode), - DictionaryKeyMode: attributes.DictionaryKeyMode.Get(0) ?? options.DictionaryKeyMode, + DictionaryKeyMode: options.DictionaryKeyMode, EnumMode: attributes.EnumMode.Get(0) ?? options.EnumMode, NumberMode: options.NumberMode); } diff --git a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs index 5963327..3c6f659 100644 --- a/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs +++ b/src/SharpSchema.Generator/Utilities/SymbolExtensions.cs @@ -40,7 +40,6 @@ public static ObjectAttributes GetObjectAttributes(this ISymbol symbol, Traversa { return new ObjectAttributes( GetAttributeHandler(symbol, traversal), - GetAttributeHandler(symbol, traversal), GetAttributeHandler(symbol, traversal), GetAttributeHandler(symbol, traversal), GetAttributeHandler(symbol, traversal), diff --git a/test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs b/test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs similarity index 57% rename from test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs rename to test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs index 9ce22c4..16bd577 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestData.Accessibility.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestData.Attributes.cs @@ -1,16 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -#pragma warning disable IDE0130 // Namespace does not match folder structure +#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; -#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. -using Test.Generator.RootSyntaxVisitorTests; +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 { diff --git a/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs index 7bc7f10..09b7918 100644 --- a/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs +++ b/test/Generator/RootSyntaxVisitorTests/TestDataFixture.cs @@ -32,7 +32,7 @@ public TestDataFixture() "test", "Generator", "RootSyntaxVisitorTests", - "TestData.Accessibility.cs"); + "TestData.Attributes.cs"); // Create an array of syntax tree from all cs files in src/SharpSchema.Annotations/ string[] annotationFiles = Directory.GetFiles( 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/VerifyTests.cs b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs index d502b74..259c1d7 100644 --- a/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs +++ b/test/Generator/RootSyntaxVisitorTests/VerifyTests.cs @@ -88,6 +88,23 @@ public Task Verify_AccessibilityOverride(string 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)]