lutaml-model 0.8.19 → 0.8.23

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +1 -1
  3. data/.github/workflows/rake.yml +1 -0
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +133 -72
  6. data/bench/bench_common.rb +8 -5
  7. data/bench/bench_niso.rb +1 -2
  8. data/bench/gate_config.rb +4 -1
  9. data/benchmark/serialization_benchmark.rb +4 -4
  10. data/docs/_guides/value-transformations.adoc +27 -0
  11. data/docs/_guides/xml-mapping.adoc +31 -0
  12. data/lib/compat/opal/generate_boot.rb +3 -1
  13. data/lib/compat/opal/lutaml_model_boot.rb +10 -7
  14. data/lib/compat/opal/moxml_boot.rb +6 -1
  15. data/lib/lutaml/hash_format/format.rb +29 -0
  16. data/lib/lutaml/hash_format.rb +5 -27
  17. data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
  18. data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
  19. data/lib/lutaml/json/format.rb +61 -0
  20. data/lib/lutaml/json/generator_options.rb +62 -0
  21. data/lib/lutaml/json.rb +4 -59
  22. data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
  23. data/lib/lutaml/jsonl/format.rb +22 -0
  24. data/lib/lutaml/jsonl.rb +5 -19
  25. data/lib/lutaml/jsonld/adapter.rb +12 -4
  26. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
  27. data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
  28. data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
  29. data/lib/lutaml/key_value/format.rb +26 -0
  30. data/lib/lutaml/key_value/transform.rb +4 -1
  31. data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
  32. data/lib/lutaml/key_value.rb +4 -21
  33. data/lib/lutaml/model/attribute.rb +38 -4
  34. data/lib/lutaml/model/collection.rb +7 -2
  35. data/lib/lutaml/model/compiled_rule.rb +1 -1
  36. data/lib/lutaml/model/config.rb +2 -2
  37. data/lib/lutaml/model/consolidation_map.rb +2 -2
  38. data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
  39. data/lib/lutaml/model/errors.rb +2 -2
  40. data/lib/lutaml/model/global_context.rb +4 -2
  41. data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
  42. data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
  43. data/lib/lutaml/model/runtime_compatibility.rb +2 -2
  44. data/lib/lutaml/model/schema/class_loader.rb +2 -1
  45. data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
  46. data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
  47. data/lib/lutaml/model/schema/file_writer.rb +5 -2
  48. data/lib/lutaml/model/schema/renderers/base.rb +4 -3
  49. data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
  50. data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
  51. data/lib/lutaml/model/schema/renderers/model.rb +18 -6
  52. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
  53. data/lib/lutaml/model/schema/renderers.rb +2 -1
  54. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
  55. data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
  56. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
  57. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
  58. data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
  59. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
  60. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
  61. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
  62. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
  63. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
  64. data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
  65. data/lib/lutaml/model/sequence.rb +4 -4
  66. data/lib/lutaml/model/serialization_adapter.rb +1 -1
  67. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
  68. data/lib/lutaml/model/serialize/format_conversion.rb +40 -9
  69. data/lib/lutaml/model/serialize/initialization.rb +9 -1
  70. data/lib/lutaml/model/serialize/model_import.rb +1 -0
  71. data/lib/lutaml/model/serialize.rb +22 -3
  72. data/lib/lutaml/model/transformation.rb +5 -0
  73. data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
  74. data/lib/lutaml/model/type.rb +9 -0
  75. data/lib/lutaml/model/type_registry.rb +2 -2
  76. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  77. data/lib/lutaml/model/union.rb +5 -2
  78. data/lib/lutaml/model/utils.rb +2 -2
  79. data/lib/lutaml/model/value_policy.rb +27 -0
  80. data/lib/lutaml/model/version.rb +1 -1
  81. data/lib/lutaml/model.rb +17 -16
  82. data/lib/lutaml/rdf/mapping.rb +2 -2
  83. data/lib/lutaml/rdf/namespace_set.rb +2 -2
  84. data/lib/lutaml/rdf/transform.rb +2 -2
  85. data/lib/lutaml/toml/format.rb +48 -0
  86. data/lib/lutaml/toml.rb +5 -46
  87. data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
  88. data/lib/lutaml/xml/builder/base.rb +2 -2
  89. data/lib/lutaml/xml/format.rb +331 -0
  90. data/lib/lutaml/xml/mapping.rb +26 -2
  91. data/lib/lutaml/xml/mapping_rule.rb +62 -3
  92. data/lib/lutaml/xml/model_transform.rb +183 -55
  93. data/lib/lutaml/xml/namespace_collector.rb +3 -3
  94. data/lib/lutaml/xml/parse_session.rb +59 -0
  95. data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
  96. data/lib/lutaml/xml/schema/builder.rb +5 -5
  97. data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
  98. data/lib/lutaml/xml/schema/xsd.rb +0 -2
  99. data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
  100. data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
  101. data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
  102. data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
  103. data/lib/lutaml/xml/transformation.rb +29 -0
  104. data/lib/lutaml/xml/xml_element.rb +2 -0
  105. data/lib/lutaml/xml.rb +4 -328
  106. data/lib/lutaml/yaml/format.rb +30 -0
  107. data/lib/lutaml/yaml.rb +5 -28
  108. data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
  109. data/lib/lutaml/yamls/format.rb +22 -0
  110. data/lib/lutaml/yamls.rb +5 -19
  111. data/lib/tasks/benchmark_runner.rb +2 -2
  112. data/lutaml-model.gemspec +1 -1
  113. data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
  114. data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
  115. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
  116. data/spec/lutaml/model/attribute_spec.rb +8 -3
  117. data/spec/lutaml/model/collection_spec.rb +1 -1
  118. data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
  119. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
  120. data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
  121. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
  122. data/spec/lutaml/model/defaults_spec.rb +45 -0
  123. data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
  124. data/spec/lutaml/model/json_spec.rb +1 -2
  125. data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
  126. data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
  127. data/spec/lutaml/model/store_spec.rb +3 -1
  128. data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
  129. data/spec/lutaml/model/union_attribute_spec.rb +4 -2
  130. data/spec/lutaml/model/value_policy_spec.rb +82 -0
  131. data/spec/lutaml/require_load_order_spec.rb +25 -0
  132. data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
  133. data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
  134. data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
  135. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
  136. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
  137. data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
  138. data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
  139. metadata +26 -4
@@ -29,7 +29,8 @@ module Lutaml
29
29
  array.each_with_index do |element, i|
30
30
  next unless element == builder
31
31
 
32
- array[i] = Array(object.public_send(Utils.snake_case(builder.name)))[index]
32
+ array[i] =
33
+ Array(object.public_send(Utils.snake_case(builder.name)))[index]
33
34
  index += 1
34
35
  end
35
36
  end
@@ -25,7 +25,8 @@ module Lutaml
25
25
  def build(complex_type)
26
26
  model = Definitions::Model.new(
27
27
  class_name: Utils.camel_case(complex_type.name),
28
- xml_root: Definitions::XmlRoot.new(kind: :element, name: complex_type.name),
28
+ xml_root: Definitions::XmlRoot.new(kind: :element,
29
+ name: complex_type.name),
29
30
  mixed: !!complex_type.mixed,
30
31
  namespace_class_name: @parent.namespace_class_name,
31
32
  )
@@ -49,7 +50,8 @@ module Lutaml
49
50
  base_name = group.name || Utils.last_of_split(group.ref)
50
51
  Definitions::Model.new(
51
52
  class_name: Utils.camel_case(base_name),
52
- xml_root: Definitions::XmlRoot.new(kind: :type_name, name: base_name),
53
+ xml_root: Definitions::XmlRoot.new(kind: :type_name,
54
+ name: base_name),
53
55
  members: inner_members,
54
56
  module_wrappable: false,
55
57
  lazy_register: true,
@@ -56,7 +56,8 @@ module Lutaml
56
56
  type: build_type_ref(resolve_element_type(element)),
57
57
  xml_name: element.name,
58
58
  kind: :element,
59
- collection: collection_from_occurs(element.min_occurs, element.max_occurs),
59
+ collection: collection_from_occurs(element.min_occurs,
60
+ element.max_occurs),
60
61
  default: element.default,
61
62
  render_default: !element.default.nil?,
62
63
  render_empty: element_required?(element.min_occurs),
@@ -75,7 +76,9 @@ module Lutaml
75
76
  end
76
77
 
77
78
  def build_choice(choice)
78
- alternatives = ElementOrder.resolved(choice).filter_map { |item| build_member(item) }
79
+ alternatives = ElementOrder.resolved(choice).filter_map do |item|
80
+ build_member(item)
81
+ end
79
82
  Definitions::Choice.new(
80
83
  alternatives: alternatives,
81
84
  header: choice_header(choice),
@@ -97,11 +100,18 @@ module Lutaml
97
100
  # ----- TypeRef ---------------------------------------------------
98
101
 
99
102
  def build_type_ref(raw_type)
100
- return Definitions::TypeRef.new(kind: :symbol, value: "string") if raw_type.nil?
101
- return Definitions::TypeRef.new(kind: :w3c, value: raw_type) if w3c_type?(raw_type)
103
+ if raw_type.nil?
104
+ return Definitions::TypeRef.new(kind: :symbol,
105
+ value: "string")
106
+ end
107
+ if w3c_type?(raw_type)
108
+ return Definitions::TypeRef.new(kind: :w3c,
109
+ value: raw_type)
110
+ end
102
111
 
103
112
  local = Utils.last_of_split(raw_type)
104
- Definitions::TypeRef.new(kind: :symbol, value: Utils.snake_case(local))
113
+ Definitions::TypeRef.new(kind: :symbol,
114
+ value: Utils.snake_case(local))
105
115
  end
106
116
 
107
117
  private
@@ -131,7 +141,8 @@ module Lutaml
131
141
  def resolve_attribute_type(attr)
132
142
  return attr.type if attr.type
133
143
 
134
- register_anonymous_simple_type("ST_#{attr.name}", attr.simple_type)
144
+ register_anonymous_simple_type("ST_#{attr.name}",
145
+ attr.simple_type)
135
146
  end
136
147
 
137
148
  def resolve_element_ref(element)
@@ -143,7 +154,8 @@ module Lutaml
143
154
  type: target.type,
144
155
  xml_name: target.xml_name,
145
156
  kind: :element,
146
- collection: collection_from_occurs(element.min_occurs, element.max_occurs),
157
+ collection: collection_from_occurs(element.min_occurs,
158
+ element.max_occurs),
147
159
  default: target.default,
148
160
  render_default: !target.default.nil?,
149
161
  render_empty: element_required?(element.min_occurs),
@@ -152,28 +164,42 @@ module Lutaml
152
164
 
153
165
  def resolve_element_type(element)
154
166
  return element.type if element.type
155
- return register_anonymous_simple_type("ST_#{element.name}", element.simple_type) if element.simple_type
156
- return register_anonymous_complex_type("CT_#{element.name}", element.complex_type) if element.complex_type
167
+ if element.simple_type
168
+ return register_anonymous_simple_type("ST_#{element.name}",
169
+ element.simple_type)
170
+ end
171
+ if element.complex_type
172
+ return register_anonymous_complex_type("CT_#{element.name}",
173
+ element.complex_type)
174
+ end
157
175
 
158
176
  "string"
159
177
  end
160
178
 
161
179
  def top_level_type_str(item)
162
180
  return item.type if item.respond_to?(:type) && item.type
163
- return register_anonymous_simple_type("ST_#{item.name}", item.simple_type) if item.respond_to?(:simple_type) && item.simple_type
181
+ if item.respond_to?(:simple_type) && item.simple_type
182
+ return register_anonymous_simple_type("ST_#{item.name}",
183
+ item.simple_type)
184
+ end
164
185
 
165
- register_anonymous_complex_type("CT_#{item.name}", item.complex_type) if item.respond_to?(:complex_type) && item.complex_type
186
+ if item.respond_to?(:complex_type) && item.complex_type
187
+ register_anonymous_complex_type("CT_#{item.name}",
188
+ item.complex_type)
189
+ end
166
190
  end
167
191
 
168
192
  def register_anonymous_simple_type(anon_name, anon_node)
169
193
  anon_node.name = anon_name
170
- @parent.simple_types[anon_name] = @parent.build_simple_type(anon_node)
194
+ @parent.simple_types[anon_name] =
195
+ @parent.build_simple_type(anon_node)
171
196
  anon_name
172
197
  end
173
198
 
174
199
  def register_anonymous_complex_type(anon_name, anon_node)
175
200
  anon_node.name = anon_name
176
- @parent.complex_types[anon_name] = @parent.build_complex_type(anon_node)
201
+ @parent.complex_types[anon_name] =
202
+ @parent.build_complex_type(anon_node)
177
203
  anon_name
178
204
  end
179
205
 
@@ -65,7 +65,9 @@ module Lutaml
65
65
  # passed :namespace, every generated complex type gets this
66
66
  # name. Mirrors XmlCompiler::ComplexType#setup_options on main.
67
67
  @namespace_class_name =
68
- requested_uri && @namespace_classes.values.find { |ns| ns.uri == requested_uri }&.class_name
68
+ requested_uri && @namespace_classes.values.find do |ns|
69
+ ns.uri == requested_uri
70
+ end&.class_name
69
71
  end
70
72
 
71
73
  def walk_schemas(schemas)
@@ -79,11 +81,15 @@ module Lutaml
79
81
  end
80
82
 
81
83
  def collect_lookups(schemas)
82
- each_schema_item(schemas) { |item, schema| dispatch_lookup(item, schema) }
84
+ each_schema_item(schemas) do |item, schema|
85
+ dispatch_lookup(item, schema)
86
+ end
83
87
  end
84
88
 
85
89
  def build_complex_types(schemas)
86
- each_schema_item(schemas) { |item, schema| dispatch_complex(item, schema) }
90
+ each_schema_item(schemas) do |item, schema|
91
+ dispatch_complex(item, schema)
92
+ end
87
93
  end
88
94
 
89
95
  def dispatch_lookup(item, schema)
@@ -91,13 +97,17 @@ module Lutaml
91
97
  when Lutaml::Xml::Schema::Xsd::SimpleType
92
98
  @simple_types[item.name] = build_simple_type(item)
93
99
  when Lutaml::Xml::Schema::Xsd::Element
94
- @elements[item.name] = @members_builder.build_top_level_attribute(item, kind: :element)
100
+ @elements[item.name] =
101
+ @members_builder.build_top_level_attribute(item, kind: :element)
95
102
  when Lutaml::Xml::Schema::Xsd::Attribute
96
103
  return if xml_defined_attribute?(schema, item.name)
97
104
 
98
- @attributes[item.name] = @members_builder.build_top_level_attribute(item, kind: :attribute)
105
+ @attributes[item.name] =
106
+ @members_builder.build_top_level_attribute(item,
107
+ kind: :attribute)
99
108
  when Lutaml::Xml::Schema::Xsd::AttributeGroup
100
- @attribute_groups[item.name] = @complex_types_builder.build_attribute_group(item)
109
+ @attribute_groups[item.name] =
110
+ @complex_types_builder.build_attribute_group(item)
101
111
  end
102
112
  end
103
113
 
@@ -117,7 +127,8 @@ module Lutaml
117
127
  next if info[:skippable]
118
128
 
119
129
  str_name = name.to_s
120
- @simple_types[str_name] = @simple_types_builder.build_supported(str_name, info)
130
+ @simple_types[str_name] =
131
+ @simple_types_builder.build_supported(str_name, info)
121
132
  end
122
133
  end
123
134
 
@@ -141,8 +152,13 @@ module Lutaml
141
152
  # Public surface used by the Members sub-builder to register
142
153
  # nested anonymous types it encounters during attribute /
143
154
  # element resolution.
144
- def build_simple_type(simple_type) = @simple_types_builder.build(simple_type)
145
- def build_complex_type(complex_type) = @complex_types_builder.build(complex_type)
155
+ def build_simple_type(simple_type)
156
+ @simple_types_builder.build(simple_type)
157
+ end
158
+
159
+ def build_complex_type(complex_type)
160
+ @complex_types_builder.build(complex_type)
161
+ end
146
162
 
147
163
  private
148
164
 
@@ -150,7 +166,10 @@ module Lutaml
150
166
  # yields each resolved element-order item with its owning schema.
151
167
  def each_schema_item(schemas, &dispatch)
152
168
  schemas.each do |schema|
153
- each_schema_item(schema.include, &dispatch) if schema.include&.any?
169
+ if schema.include&.any?
170
+ each_schema_item(schema.include,
171
+ &dispatch)
172
+ end
154
173
  each_schema_item(schema.import, &dispatch) if schema.import&.any?
155
174
  schema.resolved_element_order.each { |item| yield(item, schema) }
156
175
  end
@@ -59,8 +59,8 @@ module Lutaml
59
59
  TABLE[name]
60
60
  end
61
61
 
62
- def each(&)
63
- TABLE.each(&)
62
+ def each(&block)
63
+ TABLE.each(&block)
64
64
  end
65
65
 
66
66
  # `TypeRef.value` and `simple_content.base_class` ask the same
@@ -9,10 +9,12 @@ module Lutaml
9
9
  module XmlCompiler
10
10
  extend self
11
11
 
12
- autoload :SpecBuilder, "#{__dir__}/xml_compiler/spec_builder"
13
- autoload :RegistryGenerator, "#{__dir__}/xml_compiler/registry_generator"
14
- autoload :SupportedDataTypes, "#{__dir__}/xml_compiler/supported_data_types"
15
- autoload :ElementOrder, "#{__dir__}/xml_compiler/element_order"
12
+ autoload :SpecBuilder, "#{__dir__}/xml_compiler/spec_builder"
13
+ autoload :RegistryGenerator,
14
+ "#{__dir__}/xml_compiler/registry_generator"
15
+ autoload :SupportedDataTypes,
16
+ "#{__dir__}/xml_compiler/supported_data_types"
17
+ autoload :ElementOrder, "#{__dir__}/xml_compiler/element_order"
16
18
 
17
19
  ELEMENT_ORDER_IGNORABLE = %w[import include].freeze
18
20
 
@@ -60,7 +62,10 @@ module Lutaml
60
62
  FileWriter.write(output, dir, registry_generator: RegistryGenerator)
61
63
  true
62
64
  else
63
- ClassLoader.load(output, registry_generator: RegistryGenerator) if options[:load_classes]
65
+ if options[:load_classes]
66
+ ClassLoader.load(output,
67
+ registry_generator: RegistryGenerator)
68
+ end
64
69
  output.sources
65
70
  end
66
71
  end
@@ -27,12 +27,12 @@ module Lutaml
27
27
  @model.attribute(name, type, options)
28
28
  end
29
29
 
30
- def sequence(&)
31
- instance_eval(&)
30
+ def sequence(&block)
31
+ instance_eval(&block)
32
32
  end
33
33
 
34
- def map_element(name, **)
35
- @attributes << @model.map_element(name, **)
34
+ def map_element(name, **options)
35
+ @attributes << @model.map_element(name, **options)
36
36
  end
37
37
 
38
38
  def import_model_mappings(model, register = nil)
@@ -17,7 +17,7 @@ module Lutaml
17
17
  end
18
18
 
19
19
  def self.parse(data, _options = {})
20
- document_class.parse(data, create_additions: false)
20
+ document_class.parse(data)
21
21
  end
22
22
  end
23
23
  end
@@ -188,6 +188,7 @@ module Lutaml
188
188
 
189
189
  attr = Attribute.new(name, type, options)
190
190
  @attributes[name] = attr
191
+ @merged_attributes_cache = nil
191
192
  define_attribute_methods(attr)
192
193
 
193
194
  attr
@@ -13,7 +13,7 @@ module Lutaml
13
13
  # @param format [Symbol] The format (:xml, :json, etc.)
14
14
  # @param args [Array] Additional arguments (e.g., mapping class for XML)
15
15
  # @param block [Proc] The DSL block to evaluate
16
- def process_mapping(format, *_args, &)
16
+ def process_mapping(format, *_args, &block)
17
17
  klass = ::Lutaml::Model::Config.mappings_class_for(format)
18
18
  existing = mappings[format]
19
19
  mappings[format] = if existing.nil? || !existing.is_a?(klass)
@@ -21,7 +21,7 @@ module Lutaml
21
21
  else
22
22
  existing
23
23
  end
24
- mappings[format].instance_eval(&)
24
+ mappings[format].instance_eval(&block)
25
25
 
26
26
  if mappings[format].is_a?(Lutaml::Xml::Mapping)
27
27
  mappings[format].finalize(self)
@@ -192,24 +192,55 @@ module Lutaml
192
192
  # is always preserved when available, regardless of this option.
193
193
  # @return [String] The serialized output
194
194
  def to(format, instance, options = {})
195
+ # Ruby's JSON generator hands #to_json its own JSON::State rather
196
+ # than an options hash. It carries no LutaML options, but it does
197
+ # carry the surrounding indent context, so it is forwarded to the
198
+ # adapter unchanged instead of being read like a Hash -- json 3.0
199
+ # removed JSON::State#[].
200
+ options = Serialize.wrap_generator_state(options)
201
+ generator_state = options.delete(Serialize::GENERATOR_STATE_KEY)
202
+
195
203
  Instrumentation.instrument(:to, model: name, format: format) do
196
- adapter_override = options.is_a?(Hash) && options.delete(:adapter)
197
- if adapter_override && options.is_a?(Hash)
198
- options[:_adapter_override] =
199
- true
200
- end
204
+ adapter_override = options.delete(:adapter)
205
+ options[:_adapter_override] = true if adapter_override
201
206
  value = public_send(:"as_#{format}", instance, options)
202
207
  adapter = resolve_adapter(format, adapter_override)
203
208
 
204
209
  # Hook for format-specific options preparation (e.g., XML prefix/namespace/declaration)
205
210
  options = prepare_to_options(format, instance, options)
206
211
 
207
- adapter.new(value, register: options[:register]).public_send(
208
- :"to_#{format}", options
212
+ document = adapter.new(value, register: options[:register])
213
+
214
+ document.public_send(
215
+ :"to_#{format}",
216
+ forward_options(document, generator_state, options),
209
217
  )
210
218
  end
211
219
  end
212
220
 
221
+ # Main's behaviour differs per ADAPTER, not per option, so this follows
222
+ # the adapter rather than trying to translate option names:
223
+ # stdlib honours script_safe / ascii_only / pretty -> give it the state
224
+ # Oj ignores them all and uses its own escape_mode -> give it none
225
+ # others reach the stdlib generator underneath -> give them the options
226
+ def forward_options(document, generator_state, options)
227
+ return options if generator_state.nil?
228
+
229
+ if declares?(document, :accepts_generator_state?)
230
+ generator_state
231
+ elsif declares?(document, :ignores_generator_options?)
232
+ options
233
+ elsif generator_state.respond_to?(:to_h)
234
+ options.merge(generator_state.to_h)
235
+ else
236
+ options
237
+ end
238
+ end
239
+
240
+ def declares?(document, predicate)
241
+ document.respond_to?(predicate) && document.public_send(predicate)
242
+ end
243
+
213
244
  # Hook for format-specific options preparation before serialization.
214
245
  # XML overrides to handle prefix, namespace overrides, declaration plan.
215
246
  #
@@ -115,13 +115,18 @@ module Lutaml
115
115
  # Get all attributes for this model
116
116
  #
117
117
  # Merges class-level attributes with register-specific attributes.
118
+ # Memoized per register: this is on the per-element deserialization
119
+ # hot path, and the merged hash is stable until an attribute, import
120
+ # or register record mutates (those clear @merged_attributes_cache).
118
121
  #
119
122
  # @param register [Symbol, nil] The register context
120
123
  # @return [Hash] The attributes hash
121
124
  def attributes(register = nil)
122
125
  ensure_imports!(register) if finalized?
123
126
  if @register_records&.any?
124
- @attributes.merge(@register_records[extract_register_id(register)][:attributes])
127
+ register_id = extract_register_id(register)
128
+ (@merged_attributes_cache ||= {})[register_id] ||=
129
+ @attributes.merge(@register_records[register_id][:attributes])
125
130
  else
126
131
  @attributes
127
132
  end
@@ -183,6 +188,9 @@ module Lutaml
183
188
  GlobalContext.resolver.clear_cache(register_id)
184
189
  end
185
190
 
191
+ # Clear memoized attribute merge (see .attributes)
192
+ @merged_attributes_cache = nil
193
+
186
194
  # Clear per-Attribute type caches (stale entries from GC'd TypeContext objects)
187
195
  class_attributes.each_value(&:clear_type_cache)
188
196
  @register_records&.each_value do |record|
@@ -121,6 +121,7 @@ module Lutaml
121
121
  @register_records[register_id][:choice_attributes].concat(
122
122
  deep_duplicate_choice_attributes(model, register_id),
123
123
  )
124
+ @merged_attributes_cache = nil
124
125
  end
125
126
  private :register_only_import_model_attributes
126
127
 
@@ -175,18 +175,19 @@ module Lutaml
175
175
  @using_default[attribute_name]
176
176
  end
177
177
 
178
- # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+, but required_ruby_version >= 3.0
179
- def method_missing(method_name, *args)
178
+ def method_missing(method_name, *args, &block)
180
179
  if method_name.to_s.end_with?("=") && attribute_exist?(method_name)
181
180
  define_singleton_method(method_name) do |value|
182
181
  instance_variable_set(:"@#{method_name.to_s.chomp('=')}", value)
183
182
  end
184
183
  send(method_name, *args)
184
+ elsif ::Lutaml::Model::Attribute.default_evaluation? &&
185
+ self.class.respond_to?(method_name)
186
+ self.class.public_send(method_name, *args, &block)
185
187
  else
186
188
  super
187
189
  end
188
190
  end
189
- # rubocop:enable Style/ArgumentsForwarding
190
191
 
191
192
  def respond_to_missing?(method_name, include_private = false)
192
193
  (method_name.to_s.end_with?("=") && attribute_exist?(method_name)) ||
@@ -224,7 +225,25 @@ module Lutaml
224
225
  self.class.as_yaml(self)
225
226
  end
226
227
 
228
+ # Ruby's JSON generator hands #to_json its own JSON::State instead of an
229
+ # options hash. It carries no LutaML options, but it does carry the
230
+ # surrounding indent context, so it travels IN BAND under a private key
231
+ # rather than replacing the options hash. Wrapping rather than returning
232
+ # early keeps every later step -- register propagation, root-mapping
233
+ # validation, Collection's `collection: true` merge -- working on a real
234
+ # Hash. json 3.0 removed JSON::State#[] and rejects unknown keys in
235
+ # State#merge, so nothing may treat it as a Hash.
236
+ GENERATOR_STATE_KEY = :_generator_state
237
+
238
+ def self.wrap_generator_state(options)
239
+ return options if options.is_a?(::Hash)
240
+
241
+ { GENERATOR_STATE_KEY => options }
242
+ end
243
+
227
244
  def to_format(format, options = {})
245
+ options = Lutaml::Model::Serialize.wrap_generator_state(options)
246
+
228
247
  # Hook for format-specific validation (e.g., XML root mapping check)
229
248
  validate_root_mapping!(format, options)
230
249
 
@@ -37,9 +37,14 @@ module Lutaml
37
37
  @format = format
38
38
  @register = register
39
39
  @compiled_rules = compile_rules(mapping_dsl)
40
+ after_compile
40
41
  freeze
41
42
  end
42
43
 
44
+ # Hook for format subclasses to derive rule-set-level data after
45
+ # compilation, before the transformation is frozen and shared.
46
+ def after_compile; end
47
+
43
48
  # Transform a model instance into format-specific representation
44
49
  #
45
50
  # @abstract Subclasses must implement this method
@@ -17,7 +17,7 @@ module Lutaml
17
17
  #
18
18
  # The prepend ensures this method runs BEFORE the subclass's
19
19
  # cast implementation, even if the subclass doesn't call super.
20
- def cast(value, *args, **kwargs, &)
20
+ def cast(value, *args, **kwargs, &block)
21
21
  # Return UninitializedClass unchanged - don't transform it
22
22
  return value if Utils.uninitialized?(value)
23
23
 
@@ -55,7 +55,16 @@ module Lutaml
55
55
  hex_binary: "Lutaml::Model::Type::HexBinary",
56
56
  }.freeze
57
57
 
58
+ BUILTIN_CLASSES =
59
+ TYPE_CODES.each_value.map { |type_class_name| const_get(type_class_name) }.freeze
60
+
58
61
  class << self
62
+ # @param type [Class] a candidate type class
63
+ # @return [Boolean] whether the class is one of the built-ins
64
+ def builtin?(type)
65
+ BUILTIN_CLASSES.include?(type)
66
+ end
67
+
59
68
  # Register all built-in types into any TypeRegistry.
60
69
  #
61
70
  # This method is used by the new type resolution architecture.
@@ -127,8 +127,8 @@ module Lutaml
127
127
  #
128
128
  # @example
129
129
  # registry.each { |name, klass| puts "#{name}: #{klass}" }
130
- def each(&)
131
- @types.each(&)
130
+ def each(&block)
131
+ @types.each(&block)
132
132
  end
133
133
 
134
134
  # Create a copy of this registry
@@ -48,7 +48,7 @@ module Lutaml
48
48
  "".encoding
49
49
  end
50
50
 
51
- def method_missing(method, *_args, &)
51
+ def method_missing(method, *_args)
52
52
  return false if method.end_with?("?")
53
53
 
54
54
  nil
@@ -131,7 +131,8 @@ module Lutaml
131
131
 
132
132
  def validate_members!(members)
133
133
  unless members.is_a?(::Array) && !members.empty?
134
- raise ArgumentError, "union type must be a non-empty array of types"
134
+ raise ArgumentError,
135
+ "union type must be a non-empty array of types"
135
136
  end
136
137
 
137
138
  resolved = members.map { |member| resolve_member!(member) }
@@ -166,7 +167,9 @@ module Lutaml
166
167
  return :no_match if keys.nil? || keys.empty?
167
168
 
168
169
  field_names = member_field_names(member, format, register)
169
- return :no_match unless keys.all? { |key| field_names.include?(key) }
170
+ return :no_match unless keys.all? do |key|
171
+ field_names.include?(key)
172
+ end
170
173
 
171
174
  # Plain-Ruby assignment (format nil) builds straight from the
172
175
  # attribute hash, mirroring how non-union model attributes accept
@@ -201,10 +201,10 @@ module Lutaml
201
201
  end
202
202
  end
203
203
 
204
- def add_singleton_method_if_not_defined(instance, method_name, &)
204
+ def add_singleton_method_if_not_defined(instance, method_name, &block)
205
205
  return if instance.singleton_class.method_defined?(method_name, false)
206
206
 
207
- instance.define_singleton_method(method_name, &)
207
+ instance.define_singleton_method(method_name, &block)
208
208
  end
209
209
 
210
210
  # Determines the appropriate register for a child model during deserialization.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ # Decides how an attribute shapes incoming raw values: whether an
6
+ # Array is split element-wise (collection semantics) or handed whole
7
+ # to the declared type (value-owning semantics).
8
+ #
9
+ # A user-defined Type::Value subclass may legitimately wrap lists or
10
+ # maps in a singular slot (#752); built-in scalar types keep the
11
+ # "collection: true is missing" guidance error.
12
+ class ValuePolicy
13
+ def initialize(collection:, union:, reference:)
14
+ @collection = collection
15
+ @union = union
16
+ @reference = reference
17
+ end
18
+
19
+ # @param type [Class, Symbol] the resolved (or unresolved) type
20
+ # @return [Boolean] whether raw values must reach the type whole
21
+ def whole_value?(type)
22
+ !@collection && !@union && !@reference &&
23
+ type.is_a?(Class) && type < Type::Value && !Type.builtin?(type)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.19"
5
+ VERSION = "0.8.23"
6
6
  end
7
7
  end