lutaml-model 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.github/workflows/release.yml +3 -0
  4. data/.gitignore +6 -1
  5. data/.irbrc +1 -0
  6. data/.pryrc +1 -0
  7. data/.rubocop_todo.yml +25 -52
  8. data/README.adoc +2177 -192
  9. data/docs/custom_registers.adoc +228 -0
  10. data/docs/schema_generation.adoc +898 -0
  11. data/docs/schema_import.adoc +364 -0
  12. data/flake.lock +114 -0
  13. data/flake.nix +103 -0
  14. data/lib/lutaml/model/attribute.rb +230 -94
  15. data/lib/lutaml/model/choice.rb +30 -0
  16. data/lib/lutaml/model/collection.rb +194 -0
  17. data/lib/lutaml/model/comparable_model.rb +3 -3
  18. data/lib/lutaml/model/config.rb +26 -3
  19. data/lib/lutaml/model/constants.rb +2 -0
  20. data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
  21. data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
  22. data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
  23. data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
  24. data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
  25. data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
  26. data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
  27. data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
  28. data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
  29. data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
  30. data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
  31. data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
  32. data/lib/lutaml/model/error.rb +10 -0
  33. data/lib/lutaml/model/errors.rb +36 -0
  34. data/lib/lutaml/model/format_registry.rb +5 -2
  35. data/lib/lutaml/model/global_register.rb +41 -0
  36. data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
  37. data/lib/lutaml/model/jsonl/document.rb +14 -0
  38. data/lib/lutaml/model/jsonl/mapping.rb +19 -0
  39. data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
  40. data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
  41. data/lib/lutaml/model/jsonl/transform.rb +19 -0
  42. data/lib/lutaml/model/jsonl.rb +21 -0
  43. data/lib/lutaml/model/key_value_document.rb +3 -2
  44. data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
  45. data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
  46. data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
  47. data/lib/lutaml/model/register.rb +105 -0
  48. data/lib/lutaml/model/registrable.rb +6 -0
  49. data/lib/lutaml/model/schema/base_schema.rb +64 -0
  50. data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
  51. data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
  52. data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
  53. data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
  54. data/lib/lutaml/model/schema/generator/definition.rb +53 -0
  55. data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
  56. data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
  57. data/lib/lutaml/model/schema/generator/property.rb +110 -0
  58. data/lib/lutaml/model/schema/generator/ref.rb +24 -0
  59. data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
  60. data/lib/lutaml/model/schema/json_schema.rb +42 -49
  61. data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
  62. data/lib/lutaml/model/schema/renderer.rb +36 -0
  63. data/lib/lutaml/model/schema/shared_methods.rb +24 -0
  64. data/lib/lutaml/model/schema/templates/model.erb +9 -0
  65. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
  66. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
  67. data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
  68. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
  69. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
  70. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
  71. data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
  72. data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
  73. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
  74. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
  75. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
  76. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
  77. data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
  78. data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
  79. data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
  80. data/lib/lutaml/model/schema.rb +1 -0
  81. data/lib/lutaml/model/sequence.rb +60 -30
  82. data/lib/lutaml/model/serialize.rb +175 -53
  83. data/lib/lutaml/model/services/base.rb +11 -0
  84. data/lib/lutaml/model/services/logger.rb +2 -2
  85. data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
  86. data/lib/lutaml/model/services/type/validator/number.rb +25 -0
  87. data/lib/lutaml/model/services/type/validator/string.rb +52 -0
  88. data/lib/lutaml/model/services/type/validator.rb +43 -0
  89. data/lib/lutaml/model/services/validator.rb +145 -0
  90. data/lib/lutaml/model/services.rb +3 -0
  91. data/lib/lutaml/model/transform/key_value_transform.rb +60 -50
  92. data/lib/lutaml/model/transform/xml_transform.rb +46 -57
  93. data/lib/lutaml/model/transform.rb +22 -8
  94. data/lib/lutaml/model/type/boolean.rb +1 -1
  95. data/lib/lutaml/model/type/date.rb +1 -1
  96. data/lib/lutaml/model/type/date_time.rb +1 -1
  97. data/lib/lutaml/model/type/decimal.rb +11 -9
  98. data/lib/lutaml/model/type/float.rb +2 -1
  99. data/lib/lutaml/model/type/integer.rb +24 -21
  100. data/lib/lutaml/model/type/string.rb +4 -2
  101. data/lib/lutaml/model/type/time.rb +1 -1
  102. data/lib/lutaml/model/type/time_without_date.rb +1 -1
  103. data/lib/lutaml/model/type/value.rb +5 -1
  104. data/lib/lutaml/model/type.rb +5 -2
  105. data/lib/lutaml/model/utils.rb +30 -8
  106. data/lib/lutaml/model/validation.rb +6 -4
  107. data/lib/lutaml/model/version.rb +1 -1
  108. data/lib/lutaml/model/xml/document.rb +37 -19
  109. data/lib/lutaml/model/xml/mapping.rb +74 -13
  110. data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
  111. data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
  112. data/lib/lutaml/model/xml/oga/element.rb +4 -1
  113. data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
  114. data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
  115. data/lib/lutaml/model/xml/xml_element.rb +3 -28
  116. data/lib/lutaml/model/xml_adapter/element.rb +1 -1
  117. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
  118. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
  119. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
  120. data/lib/lutaml/model/yamls/document.rb +14 -0
  121. data/lib/lutaml/model/yamls/mapping.rb +19 -0
  122. data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
  123. data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
  124. data/lib/lutaml/model/yamls/transform.rb +19 -0
  125. data/lib/lutaml/model/yamls.rb +21 -0
  126. data/lib/lutaml/model.rb +7 -31
  127. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
  128. data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
  129. data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
  130. data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
  131. data/spec/fixtures/xml/product_catalog.xsd +151 -0
  132. data/spec/fixtures/xml/specifications_schema.xsd +38 -0
  133. data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
  134. data/spec/lutaml/model/attribute_spec.rb +41 -44
  135. data/spec/lutaml/model/choice_spec.rb +44 -0
  136. data/spec/lutaml/model/custom_collection_spec.rb +830 -0
  137. data/spec/lutaml/model/custom_model_spec.rb +15 -3
  138. data/spec/lutaml/model/defaults_spec.rb +5 -1
  139. data/spec/lutaml/model/global_register_spec.rb +108 -0
  140. data/spec/lutaml/model/group_spec.rb +9 -3
  141. data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
  142. data/spec/lutaml/model/jsonl_spec.rb +229 -0
  143. data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
  144. data/spec/lutaml/model/register/key_value_spec.rb +275 -0
  145. data/spec/lutaml/model/register/xml_spec.rb +187 -0
  146. data/spec/lutaml/model/register_spec.rb +147 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
  148. data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
  149. data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
  150. data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
  151. data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
  152. data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
  153. data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
  154. data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
  155. data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
  156. data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
  157. data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
  158. data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
  159. data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
  160. data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
  161. data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
  162. data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
  163. data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
  164. data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
  165. data/spec/lutaml/model/sequence_spec.rb +36 -0
  166. data/spec/lutaml/model/serializable_spec.rb +31 -0
  167. data/spec/lutaml/model/type_spec.rb +8 -4
  168. data/spec/lutaml/model/utils_spec.rb +3 -3
  169. data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
  170. data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
  171. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  172. data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
  173. data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
  174. data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
  175. data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
  176. data/spec/lutaml/model/yamls_spec.rb +294 -0
  177. data/spec/spec_helper.rb +1 -0
  178. metadata +105 -9
  179. data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
  180. /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
  181. /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
  182. /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
  183. /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
  184. /data/lib/lutaml/model/{hash → hash_adapter}/transform.rb +0 -0
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute"
4
+ require_relative "choices"
5
+
6
+ module Lutaml
7
+ module Model
8
+ module Schema
9
+ module Decorators
10
+ class ClassDefinition
11
+ attr_accessor :base_class, :sub_classes
12
+ attr_reader :additional_properties, :properties, :namespaced_name
13
+
14
+ # Decorates a JSON schema information to be used in class definitions.
15
+ # This class is used to provide a structured way to handle schema
16
+ # information for class definitions, including attributes,
17
+ # required fields, and JSON mappings.
18
+ #
19
+ # @param schema [Hash] The JSON schema to be decorated.
20
+ # @param options [Hash] Additional options for the decorator.
21
+ def initialize(namespaced_name, schema)
22
+ @namespaced_name = namespaced_name
23
+ @choices = schema["oneOf"] || []
24
+ @additional_properties = schema["additionalProperties"] || false
25
+ @polymorphic_attributes = []
26
+
27
+ @properties = (schema["properties"] || {}).to_h do |name, attr|
28
+ attribute = Decorators::Attribute.new(name, attr)
29
+ polymorphic_attributes << attribute if attribute.polymorphic?
30
+
31
+ [name, attribute]
32
+ end
33
+
34
+ @base_class = nil
35
+ @sub_classes = []
36
+ end
37
+
38
+ def name
39
+ @name ||= @namespaced_name.split("_").last
40
+ end
41
+
42
+ def namespaces
43
+ @namespaces ||= @namespaced_name.split("_")[0..-2]
44
+ end
45
+
46
+ def parent_class
47
+ @parent_class ||= @base_class&.namespaced_name&.gsub("_", "::") || "Lutaml::Model::Serializable"
48
+ end
49
+
50
+ def choice?
51
+ @choices&.any?
52
+ end
53
+
54
+ def attributes
55
+ return @properties.values if !choice?
56
+
57
+ choice_attributes = {}
58
+ @choices.each do |choice|
59
+ choice["properties"].each do |name, attr|
60
+ choice_attributes[name] = properties[name] || Decorators::Attribute.new(name, attr)
61
+ properties[name] = nil if properties[name]
62
+ end
63
+ end
64
+
65
+ @properties.values.compact + [Choices.new(choice_attributes)]
66
+ end
67
+
68
+ def polymorphic?
69
+ polymorphic_attributes.any?
70
+ end
71
+
72
+ def polymorphic_attributes
73
+ return @polymorphic_attributes if !@polymorphic_attributes.nil?
74
+
75
+ attributes.each do |attr|
76
+ @polymorphic_attributes << attr if attr.polymorphic?
77
+ end
78
+
79
+ @polymorphic_attributes
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "class_definition"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ module Decorators
9
+ class DefinitionCollection
10
+ include Enumerable
11
+
12
+ def initialize(definitions_hash)
13
+ @definition_hash = definitions_hash
14
+ @polymorphic_classes = []
15
+
16
+ @definitions = definitions_hash.to_h do |name, schema|
17
+ definition = definition_class.new(name, schema)
18
+ @polymorphic_classes << definition if definition.polymorphic?
19
+
20
+ [name, definition]
21
+ end
22
+
23
+ resolve_polymorphic_base_types!
24
+ end
25
+
26
+ def each(&block)
27
+ @definitions.each(&block)
28
+ end
29
+
30
+ def transform_values(&block)
31
+ @definitions.transform_values(&block)
32
+ end
33
+
34
+ def [](name)
35
+ @definitions[name] || raise("Definition not found: #{name}")
36
+ end
37
+
38
+ private
39
+
40
+ def definition_class
41
+ Lutaml::Model::Schema::Decorators::ClassDefinition
42
+ end
43
+
44
+ def resolve_polymorphic_base_types!
45
+ @polymorphic_classes.each do |poly_class|
46
+ poly_class.polymorphic_attributes.each do |attr|
47
+ resolve_polymorphic_attribute(attr)
48
+ end
49
+ end
50
+ end
51
+
52
+ def resolve_polymorphic_attribute(attr)
53
+ child_classes = resolve_child_classes(attr.polymorphic)
54
+ return if child_classes.size < 2
55
+
56
+ base_class = find_common_base_class(child_classes)
57
+ return unless base_class
58
+
59
+ update_polymorphic_attribute(attr, base_class)
60
+ refactor_subclasses(child_classes, base_class)
61
+ end
62
+
63
+ def resolve_child_classes(class_names)
64
+ class_names.filter_map { |name| @definitions[name] }
65
+ end
66
+
67
+ def update_polymorphic_attribute(attr, base_class)
68
+ attr.base_class = base_class
69
+ attr.type = base_class.namespaced_name.gsub("_", "::")
70
+ end
71
+
72
+ def refactor_subclasses(child_classes, base_class)
73
+ common_keys = base_class.properties.keys.to_set
74
+ (child_classes - [base_class]).each do |child|
75
+ child.base_class = base_class
76
+ child.properties.reject! { |key, _| common_keys.include?(key) }
77
+ base_class.sub_classes << child unless base_class.sub_classes.include?(child)
78
+ end
79
+ end
80
+
81
+ def find_common_base_class(class_defs)
82
+ return nil if class_defs.size < 2
83
+
84
+ # Get intersection of all property names
85
+ common_props = class_defs.map { |klass| klass.properties.keys }.reduce(:&)
86
+ return nil if common_props.empty?
87
+
88
+ # Look for a class that exactly matches the common properties — assume it's the base
89
+ class_defs.find do |klass|
90
+ klass.properties.keys.to_set == common_props.to_set
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,53 @@
1
+ require_relative "properties_collection"
2
+ require_relative "../shared_methods"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ module Generator
8
+ class Definition
9
+ include SharedMethods
10
+
11
+ attr_reader :type, :name
12
+
13
+ def initialize(type)
14
+ @type = type
15
+ @name = type.name.gsub("::", "_")
16
+ end
17
+
18
+ def to_schema
19
+ @schema = {
20
+ name => {
21
+ "type" => "object",
22
+ "additionalProperties" => false,
23
+ "properties" => properties_to_schema(type),
24
+ },
25
+ }
26
+
27
+ # Add choice validation if present
28
+ if type.choice_attributes.any?
29
+ @schema[name]["oneOf"] = generate_choice_attributes(type)
30
+ end
31
+
32
+ @schema
33
+ end
34
+
35
+ private
36
+
37
+ def generate_choice_attributes(type)
38
+ type.choice_attributes.map do |choice|
39
+ {
40
+ "type" => "object",
41
+ "properties" => PropertiesCollection.from_attributes(choice.attributes, extract_register_from(type)).to_schema,
42
+ }
43
+ end
44
+ end
45
+
46
+ def properties_to_schema(type)
47
+ PropertiesCollection.from_class(type).to_schema
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ require_relative "definition"
2
+ require_relative "../shared_methods"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ module Generator
8
+ class DefinitionsCollection
9
+ class << self
10
+ include SharedMethods
11
+
12
+ def from_class(klass)
13
+ new.tap do |collection|
14
+ collection << Definition.new(klass)
15
+
16
+ process_attributes(collection, klass)
17
+ end
18
+ end
19
+
20
+ def process_attributes(collection, klass)
21
+ register = extract_register_from(klass)
22
+ klass.attributes.each_value do |attribute|
23
+ next unless attribute.serializable?(register)
24
+
25
+ process_attribute(collection, attribute, register)
26
+ end
27
+ end
28
+
29
+ def process_attribute(collection, attribute, register)
30
+ attr_type = Lutaml::Model::GlobalRegister.lookup(register).get_class(attribute.type)
31
+ collection.merge(DefinitionsCollection.from_class(attr_type))
32
+
33
+ process_polymorphic_types(collection, attribute)
34
+ end
35
+
36
+ def process_polymorphic_types(collection, attribute)
37
+ return unless attribute.options&.[](:polymorphic)
38
+
39
+ attribute.options[:polymorphic].each do |child|
40
+ collection.merge(DefinitionsCollection.from_class(child))
41
+ end
42
+ end
43
+ end
44
+
45
+ attr_reader :definitions
46
+
47
+ def initialize(definitions = [])
48
+ @definitions = definitions.map do |definition|
49
+ next definition if definition.is_a?(Definition)
50
+
51
+ Definition.new(definition)
52
+ end
53
+ end
54
+
55
+ def to_schema
56
+ definitions.each_with_object({}) do |definition, schema|
57
+ schema.merge!(definition.to_schema)
58
+ end
59
+ end
60
+
61
+ def add_definition(definition)
62
+ @definitions ||= []
63
+ @definitions << definition
64
+ end
65
+ alias << add_definition
66
+ alias push add_definition
67
+
68
+ def merge(collection)
69
+ @definitions ||= []
70
+
71
+ if collection.is_a?(Array)
72
+ @definitions.concat(collection)
73
+ else
74
+ @definitions.concat(collection.definitions)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "property"
2
+ require_relative "../shared_methods"
3
+
4
+ module Lutaml
5
+ module Model
6
+ module Schema
7
+ module Generator
8
+ class PropertiesCollection
9
+ class << self
10
+ include SharedMethods
11
+
12
+ def from_class(klass)
13
+ from_attributes(klass.attributes.values, extract_register_from(klass))
14
+ end
15
+
16
+ def from_attributes(attributes, register)
17
+ new(register: register).tap do |collection|
18
+ attributes.each do |attribute|
19
+ name = attribute.name
20
+ collection << Property.new(name, attribute, register: register)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :properties, :register
27
+
28
+ def initialize(properties = [], register:)
29
+ self.properties = properties
30
+ @register = register
31
+ end
32
+
33
+ def to_schema
34
+ properties.each_with_object({}) do |property, schema|
35
+ schema.merge!(property.to_schema)
36
+ end
37
+ end
38
+
39
+ def add_property(property)
40
+ @properties << if property.is_a?(Property)
41
+ property
42
+ else
43
+ Property.new(property.name, property, register: register)
44
+ end
45
+ end
46
+ alias << add_property
47
+ alias push add_property
48
+
49
+ def properties=(properties)
50
+ @properties ||= []
51
+ @properties.clear
52
+
53
+ @properties = properties.map do |property|
54
+ next property if property.is_a?(Property)
55
+
56
+ Property.new(property.name, property, register)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,110 @@
1
+ module Lutaml
2
+ module Model
3
+ module Schema
4
+ module Generator
5
+ # This class is used to generate a property schema definition.
6
+ # It is used in the context of generating JSON schemas.
7
+ class Property
8
+ attr_reader :name, :attribute, :register
9
+
10
+ def initialize(name, attribute, register:)
11
+ @name = name.to_s.gsub("::", "_")
12
+ @attribute = attribute
13
+ @register = register
14
+ end
15
+
16
+ def to_schema
17
+ { name => generate_attribute_schema(attribute) }
18
+ end
19
+
20
+ private
21
+
22
+ def generate_attribute_schema(attr, options = {})
23
+ include_null = options.fetch(:include_null, true)
24
+ inside_collection = options.fetch(:inside_collection, false)
25
+ if attr.collection? && !inside_collection
26
+ collection_schema(attr)
27
+ elsif attr.serializable?(register) && polymorphic?(attr)
28
+ polymorphic_schema(attr)
29
+ elsif attr.serializable?(register)
30
+ Generator::Ref.new(attr.type(register)).to_schema
31
+ else
32
+ primitive_schema(attr, include_null: include_null)
33
+ end
34
+ end
35
+
36
+ def collection_schema(attr)
37
+ schema = {
38
+ "type" => "array",
39
+ "items" => generate_attribute_schema(
40
+ attr,
41
+ include_null: false,
42
+ inside_collection: true,
43
+ ),
44
+ }
45
+
46
+ if attr.options[:collection].is_a?(Range)
47
+ add_collection_constraints!(schema, attr.options[:collection])
48
+ end
49
+
50
+ schema
51
+ end
52
+
53
+ def add_collection_constraints!(schema, range)
54
+ schema["minItems"] = range.begin
55
+ schema["maxItems"] = range.end if range.end
56
+ end
57
+
58
+ def polymorphic_schema(attr)
59
+ ref_schemas = attr.options[:polymorphic].map do |type|
60
+ Ref.new(type).to_schema
61
+ end
62
+
63
+ ref_schemas << Ref.new(attr.type).to_schema if attr.type
64
+
65
+ {
66
+ "type" => ["object", "null"],
67
+ "oneOf" => ref_schemas,
68
+ }
69
+ end
70
+
71
+ def primitive_schema(attr, include_null: true)
72
+ type = get_type(attr.type(register))
73
+ type = [type, "null"] if include_null
74
+
75
+ { "type" => type }.merge(get_constraints(attr))
76
+ end
77
+
78
+ def get_constraints(attr)
79
+ constraints = {}
80
+
81
+ # Add pattern validation
82
+ constraints["pattern"] = attr.pattern.source if attr.pattern
83
+
84
+ # Add default value
85
+ constraints["default"] = attr.default(register) if attr.default_set?(register)
86
+
87
+ # Add enumeration values
88
+ constraints["enum"] = attr.enum_values if attr.enum?
89
+
90
+ constraints
91
+ end
92
+
93
+ def polymorphic?(attr)
94
+ Utils.present?(attr.options[:polymorphic])
95
+ end
96
+
97
+ def get_type(type)
98
+ {
99
+ Lutaml::Model::Type::String => "string",
100
+ Lutaml::Model::Type::Integer => "integer",
101
+ Lutaml::Model::Type::Boolean => "boolean",
102
+ Lutaml::Model::Type::Float => "number",
103
+ Lutaml::Model::Type::Hash => "object",
104
+ }[type] || "string" # Default to string for unknown types
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,24 @@
1
+ module Lutaml
2
+ module Model
3
+ module Schema
4
+ module Generator
5
+ # This class is used to generate a reference to a schema definition.
6
+ # It is used in the context of generating JSON schemas.
7
+ class Ref
8
+ attr_reader :name
9
+
10
+ def initialize(type)
11
+ @type = type
12
+ @name = @type.name.gsub("::", "_")
13
+ end
14
+
15
+ def to_schema
16
+ {
17
+ "$ref" => "#/$defs/#{name}",
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module Lutaml
2
+ module Model
3
+ module Schema
4
+ module Helpers
5
+ module TemplateHelper
6
+ def open_namespaces(namespaces)
7
+ namespaces.map { |ns| "module #{ns}" }.join("\n")
8
+ end
9
+
10
+ def close_namespaces(namespaces)
11
+ namespaces.reverse.map { "end" }.join("\n")
12
+ end
13
+
14
+ def indent(level)
15
+ " " * level
16
+ end
17
+
18
+ def attribute_line(attribute, level)
19
+ if attribute.choice?
20
+ <<~RUBY.chomp
21
+
22
+ #{indent(level)}choice do
23
+ #{attribute.attributes.map { |attr| attribute_line(attr, level + 1) }.join("\n")}
24
+ #{indent(level)}end
25
+ RUBY
26
+ else
27
+ "#{indent(level)}attribute #{attribute_properties(attribute)}"
28
+ end
29
+ end
30
+
31
+ def attribute_properties(attribute)
32
+ # properties = {}
33
+ # properties[:default] = attribute.default if attribute.default
34
+ # properties[:collection] = true if attribute.collection?
35
+
36
+ required_properties = ":#{attribute.name}, #{attribute.type}"
37
+ required_properties += ", default: #{attribute.default.inspect}" if attribute.default
38
+ required_properties += ", collection: #{attribute.collection}" if attribute.collection?
39
+ required_properties += ", values: #{attribute.options['enum']}" if attribute.options["enum"]
40
+ required_properties += ", pattern: /#{attribute.options['pattern']}/" if attribute.options["pattern"]
41
+ required_properties += ", polymorphic: [#{attribute.polymorphic_classes.join(', ')}]" if attribute.polymorphic?
42
+
43
+ required_properties
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,66 +1,59 @@
1
+ require_relative "shared_methods"
2
+ require_relative "base_schema"
3
+ require_relative "renderer"
4
+ require_relative "decorators/class_definition"
5
+ require_relative "decorators/definition_collection"
6
+
1
7
  require "json"
2
8
 
3
9
  module Lutaml
4
10
  module Model
5
11
  module Schema
6
- class JsonSchema
7
- def self.generate(klass, options = {})
8
- schema = {
9
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
10
- "$id" => options[:id],
11
- "description" => options[:description],
12
- "$ref" => "#/$defs/#{klass.name}",
13
- "$defs" => generate_definitions(klass),
14
- }.compact
12
+ class JsonSchema < BaseSchema
13
+ class << self
14
+ include Lutaml::Model::Schema::SharedMethods
15
15
 
16
- options[:pretty] ? JSON.pretty_generate(schema) : schema.to_json
17
- end
16
+ def generate(
17
+ klass,
18
+ id: nil,
19
+ title: nil,
20
+ description: nil,
21
+ pretty: false
22
+ )
23
+ options = {
24
+ schema: "https://json-schema.org/draft/2020-12/schema",
25
+ id: id,
26
+ title: title,
27
+ description: description,
28
+ pretty: pretty,
29
+ }
18
30
 
19
- def self.generate_definitions(klass)
20
- defs = { klass.name => generate_class_schema(klass) }
21
- klass.attributes.each_value do |attr|
22
- if attr.type <= Lutaml::Model::Serialize
23
- defs.merge!(generate_definitions(attr.type))
24
- end
31
+ super(klass, options)
25
32
  end
26
- defs
27
- end
28
33
 
29
- def self.generate_class_schema(klass)
30
- {
31
- "type" => "object",
32
- "properties" => generate_properties(klass),
33
- "required" => klass.attributes.keys,
34
- }
35
- end
34
+ def format_schema(schema, options)
35
+ options[:pretty] ? JSON.pretty_generate(schema) : schema.to_json
36
+ end
37
+
38
+ def generate_model_classes(schema)
39
+ definitions = definition_collection_class.new(schema["$defs"])
36
40
 
37
- def self.generate_properties(klass)
38
- klass.attributes.transform_values do |attr|
39
- generate_property_schema(attr)
41
+ definitions.transform_values do |definition|
42
+ generate_model_class(definition)
43
+ end
40
44
  end
41
- end
42
45
 
43
- def self.generate_property_schema(attr)
44
- if attr.type <= Lutaml::Model::Serialize
45
- { "$ref" => "#/$defs/#{attr.type.name}" }
46
- elsif attr.collection?
47
- {
48
- "type" => "array",
49
- "items" => { "type" => get_json_type(attr.type) },
50
- }
51
- else
52
- { "type" => get_json_type(attr.type) }
46
+ private
47
+
48
+ def generate_model_class(schema)
49
+ template = File.join(__dir__, "templates", "model.erb")
50
+
51
+ Lutaml::Model::Schema::Renderer.render(template, schema: schema)
53
52
  end
54
- end
55
53
 
56
- def self.get_json_type(type)
57
- {
58
- Lutaml::Model::Type::String => "string",
59
- Lutaml::Model::Type::Integer => "integer",
60
- Lutaml::Model::Type::Boolean => "boolean",
61
- Lutaml::Model::Type::Float => "number",
62
- Lutaml::Model::Type::Hash => "object",
63
- }[type] || "string" # Default to string for unknown types
54
+ def definition_collection_class
55
+ Lutaml::Model::Schema::Decorators::DefinitionCollection
56
+ end
64
57
  end
65
58
  end
66
59
  end