lutaml-model 0.8.17 → 0.8.18
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.
- checksums.yaml +4 -4
- data/.github/workflows/downstream-performance.yml +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
- data/lib/lutaml/model/serialize/builder.rb +15 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +30 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +97 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
|
@@ -39,8 +39,10 @@ module Lutaml
|
|
|
39
39
|
|
|
40
40
|
return self unless block
|
|
41
41
|
|
|
42
|
-
# Enable order tracking for mixed_content models
|
|
43
|
-
|
|
42
|
+
# Enable order tracking for ordered and mixed_content models.
|
|
43
|
+
# `ordered?` is true for both `ordered` and `mixed_content`
|
|
44
|
+
# (mixed_content sets @ordered = true in addition to @mixed_content).
|
|
45
|
+
@__order_tracking__ = ordered?
|
|
44
46
|
|
|
45
47
|
# Evaluate the block - use instance_eval for no-receiver style
|
|
46
48
|
# The block's first parameter determines the style:
|
|
@@ -62,6 +64,17 @@ module Lutaml
|
|
|
62
64
|
mapping&.mixed_content? || false
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
# Check if this model has ordered or mixed_content enabled.
|
|
68
|
+
# Both set @ordered = true on the XML mapping; mixed_content
|
|
69
|
+
# additionally sets @mixed_content = true. Order tracking is
|
|
70
|
+
# needed for either mode so the builder can emit elements in
|
|
71
|
+
# setter-call order rather than declaration order.
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def ordered?
|
|
74
|
+
mapping = self.class.mappings_for(:xml, lutaml_register)
|
|
75
|
+
mapping&.ordered? || false
|
|
76
|
+
end
|
|
77
|
+
|
|
65
78
|
private
|
|
66
79
|
|
|
67
80
|
# Intercept method calls to track order for mixed_content
|
|
@@ -71,71 +71,19 @@ module Lutaml
|
|
|
71
71
|
propagated.merge(register: instance.lutaml_register))
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
# Apply a value map to transform a value
|
|
74
|
+
# Apply a value map to transform a value.
|
|
75
75
|
#
|
|
76
|
-
#
|
|
76
|
+
# Delegates to Attribute#apply_value_map. See attribute.rb for the
|
|
77
|
+
# canonical implementation.
|
|
77
78
|
#
|
|
78
79
|
# @param value [Object] The value to transform
|
|
79
80
|
# @param value_map [Hash] The value map configuration
|
|
80
81
|
# @param attr [Attribute] The attribute definition
|
|
81
82
|
# @return [Object] The transformed value
|
|
82
83
|
def apply_value_map(value, value_map, attr)
|
|
83
|
-
|
|
84
|
-
value_for_option(value_map[:nil], attr)
|
|
85
|
-
elsif Utils.empty?(value)
|
|
86
|
-
# Check for new boolean value_map format (from: { empty: true/false })
|
|
87
|
-
# Only use new format if the value is explicitly boolean (TrueClass or FalseClass)
|
|
88
|
-
if value_map[:from] && (value_map[:from][:empty].is_a?(TrueClass) || value_map[:from][:empty].is_a?(FalseClass))
|
|
89
|
-
return value_map[:from][:empty]
|
|
90
|
-
end
|
|
91
|
-
# Check for direct boolean format (rule.value_map(:from) returns { empty: true })
|
|
92
|
-
# Only return directly if it's a boolean value (TrueClass/FalseClass), not a symbol
|
|
93
|
-
if value_map[:empty].is_a?(TrueClass) || value_map[:empty].is_a?(FalseClass)
|
|
94
|
-
return value_map[:empty]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Fall back to legacy value_map format
|
|
98
|
-
value_for_option(value_map[:empty], attr, value)
|
|
99
|
-
elsif Utils.uninitialized?(value)
|
|
100
|
-
# Check for new boolean value_map format (from: { omitted: true/false })
|
|
101
|
-
# Only use new format if the value is explicitly boolean (TrueClass or FalseClass)
|
|
102
|
-
if value_map[:from] && (value_map[:from][:omitted].is_a?(TrueClass) || value_map[:from][:omitted].is_a?(FalseClass))
|
|
103
|
-
return value_map[:from][:omitted]
|
|
104
|
-
end
|
|
105
|
-
# Check for direct boolean format (rule.value_map(:from) returns { omitted: false })
|
|
106
|
-
# Only return directly if it's a boolean value (TrueClass/FalseClass), not a symbol
|
|
107
|
-
if value_map[:omitted].is_a?(TrueClass) || value_map[:omitted].is_a?(FalseClass)
|
|
108
|
-
return value_map[:omitted]
|
|
109
|
-
end
|
|
84
|
+
return value unless attr
|
|
110
85
|
|
|
111
|
-
|
|
112
|
-
value_for_option(value_map[:omitted], attr)
|
|
113
|
-
else
|
|
114
|
-
value
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Get the value for a specific option in a value map
|
|
119
|
-
#
|
|
120
|
-
# @param option [Symbol] The option (:nil, :empty, etc.)
|
|
121
|
-
# @param attr [Attribute] The attribute definition
|
|
122
|
-
# @param empty_value [Object] The empty value to use (optional)
|
|
123
|
-
# @return [Object, nil, UninitializedClass] The appropriate value
|
|
124
|
-
def value_for_option(option, attr, empty_value = nil)
|
|
125
|
-
return nil if option == :nil
|
|
126
|
-
return empty_value || empty_object(attr) if option == :empty
|
|
127
|
-
|
|
128
|
-
Lutaml::Model::UninitializedClass.instance
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Get an empty object for an attribute
|
|
132
|
-
#
|
|
133
|
-
# @param attr [Attribute] The attribute definition
|
|
134
|
-
# @return [String, Array] Empty string or collection
|
|
135
|
-
def empty_object(attr)
|
|
136
|
-
return attr.build_collection if attr.collection?
|
|
137
|
-
|
|
138
|
-
""
|
|
86
|
+
attr.apply_value_map(value, value_map)
|
|
139
87
|
end
|
|
140
88
|
|
|
141
89
|
# Ensure a value is UTF-8 encoded
|
|
@@ -145,6 +93,10 @@ module Lutaml
|
|
|
145
93
|
def ensure_utf8(value)
|
|
146
94
|
case value
|
|
147
95
|
when String
|
|
96
|
+
# Opal strings are JS strings with no per-string encoding;
|
|
97
|
+
# String#encode there doesn't accept MRI's keyword options.
|
|
98
|
+
return value if Lutaml::Model.opal?
|
|
99
|
+
|
|
148
100
|
value.encode("UTF-8", invalid: :replace, undef: :replace,
|
|
149
101
|
replace: "")
|
|
150
102
|
when Array
|
|
@@ -202,9 +202,7 @@ module Lutaml
|
|
|
202
202
|
def validate_attribute!(attr_name)
|
|
203
203
|
attr = self.class.attributes[attr_name]
|
|
204
204
|
value = instance_variable_get(:"@#{attr_name}")
|
|
205
|
-
|
|
206
|
-
self)
|
|
207
|
-
attr.validate_value!(value, lutaml_register, resolver)
|
|
205
|
+
attr.validate_value!(value, lutaml_register, instance_object: self)
|
|
208
206
|
end
|
|
209
207
|
|
|
210
208
|
def key_exist?(hash, key)
|
|
@@ -301,12 +299,11 @@ module Lutaml
|
|
|
301
299
|
return attr_value(attrs, name, attr)
|
|
302
300
|
end
|
|
303
301
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
end
|
|
302
|
+
resolved = attr.default_value(lutaml_register, self)
|
|
303
|
+
return Lutaml::Model::UninitializedClass.instance if Lutaml::Model::Utils.uninitialized?(resolved)
|
|
304
|
+
|
|
305
|
+
using_default_for(name)
|
|
306
|
+
attr.cast_value(resolved, lutaml_register)
|
|
310
307
|
end
|
|
311
308
|
|
|
312
309
|
def resolve_reference_key(ref)
|
|
@@ -50,9 +50,8 @@ instance_object)
|
|
|
50
50
|
return fetch_value(name) if Utils.string_or_symbol_key?(doc, name)
|
|
51
51
|
|
|
52
52
|
if attr
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return resolver.default_value if resolver.default_set?
|
|
53
|
+
resolved = attr.default_value(register, instance_object)
|
|
54
|
+
return resolved unless Utils.uninitialized?(resolved)
|
|
56
55
|
end
|
|
57
56
|
|
|
58
57
|
uninitialized_value
|
data/lib/lutaml/model/store.rb
CHANGED
data/lib/lutaml/model/toml.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Lutaml
|
|
|
16
16
|
Transform = ::Lutaml::Toml::Adapter::Transform
|
|
17
17
|
|
|
18
18
|
def self.detect_toml_adapter
|
|
19
|
-
return nil if Lutaml::Model
|
|
19
|
+
return nil if Lutaml::Model.opal?
|
|
20
20
|
|
|
21
21
|
# Skip tomlib on Windows entirely due to segfault issues
|
|
22
22
|
if Lutaml::Model::RuntimeCompatibility.windows?
|
|
@@ -85,48 +85,26 @@ module Lutaml
|
|
|
85
85
|
protected
|
|
86
86
|
|
|
87
87
|
def apply_value_map(value, value_map, attr)
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Check if attribute is a Boolean type
|
|
106
|
-
attr_type = attr&.type || attr&.unresolved_type
|
|
107
|
-
if attr_type == Lutaml::Model::Type::Boolean
|
|
108
|
-
return value_map[:omitted]
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
value_for_option(value_map[:omitted], attr)
|
|
112
|
-
else
|
|
113
|
-
value
|
|
88
|
+
return attr.apply_value_map(value, value_map) if attr
|
|
89
|
+
|
|
90
|
+
# Attribute-less dispatch for custom-method-only rules. Mirrors
|
|
91
|
+
# Attribute#apply_value_map for the options that need no attribute
|
|
92
|
+
# info: :nil -> nil and :omitted -> uninitialized. A typed :empty needs
|
|
93
|
+
# an Attribute (a crash case pre-consolidation), so the value passes
|
|
94
|
+
# through unchanged.
|
|
95
|
+
key = if value.nil? then :nil
|
|
96
|
+
elsif Utils.empty?(value) then :empty
|
|
97
|
+
elsif Utils.uninitialized?(value) then :omitted
|
|
98
|
+
end
|
|
99
|
+
return value unless key
|
|
100
|
+
|
|
101
|
+
case value_map[key]
|
|
102
|
+
when :nil then nil
|
|
103
|
+
when :empty then value
|
|
104
|
+
else Lutaml::Model::UninitializedClass.instance
|
|
114
105
|
end
|
|
115
106
|
end
|
|
116
107
|
|
|
117
|
-
def value_for_option(option, attr, empty_value = nil)
|
|
118
|
-
return nil if option == :nil
|
|
119
|
-
return empty_value || empty_object(attr) if option == :empty
|
|
120
|
-
|
|
121
|
-
Lutaml::Model::UninitializedClass.instance
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def empty_object(attr)
|
|
125
|
-
return [] if attr.collection?
|
|
126
|
-
|
|
127
|
-
""
|
|
128
|
-
end
|
|
129
|
-
|
|
130
108
|
def mappings_for(format, register = nil)
|
|
131
109
|
context.mappings_for(format, register)
|
|
132
110
|
end
|
data/lib/lutaml/model/type.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Lutaml
|
|
|
16
16
|
autoload :Boolean, "#{__dir__}/type/boolean"
|
|
17
17
|
autoload :Decimal, "#{__dir__}/type/decimal"
|
|
18
18
|
autoload :Reference, "#{__dir__}/type/reference"
|
|
19
|
+
autoload :Union, "#{__dir__}/union"
|
|
19
20
|
autoload :Hash, "#{__dir__}/type/hash"
|
|
20
21
|
autoload :Symbol, "#{__dir__}/type/symbol"
|
|
21
22
|
autoload :Duration, "#{__dir__}/type/duration"
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# Functional xsd:union: an attribute value conforms to one of several
|
|
7
|
+
# declared member types. A single shared class recognized by identity
|
|
8
|
+
# (mirrors Type::Reference); the per-attribute member list lives in the
|
|
9
|
+
# attribute's @options[:union_member_types]. Stateless: serialization is
|
|
10
|
+
# driven by value.class, never by per-instance tracking.
|
|
11
|
+
#
|
|
12
|
+
# This class owns ALL union knowledge: the conformance engine, the
|
|
13
|
+
# definition-time validation, and the per-format mapped-field source.
|
|
14
|
+
# Every serializer touchpoint is a thin identity check that delegates here.
|
|
15
|
+
class Union < Value
|
|
16
|
+
# Options that cannot be combined with a union member list.
|
|
17
|
+
INCOMPATIBLE_OPTIONS = %i[polymorphic raw with child_mappings].freeze
|
|
18
|
+
|
|
19
|
+
# Native Ruby class a parsed/plain-Ruby value already has when it
|
|
20
|
+
# conforms to a scalar member without needing a lexical-space parse.
|
|
21
|
+
NATIVE_CLASSES = {
|
|
22
|
+
Lutaml::Model::Type::Integer => ::Integer,
|
|
23
|
+
Lutaml::Model::Type::Float => ::Float,
|
|
24
|
+
Lutaml::Model::Type::Boolean => [::TrueClass, ::FalseClass],
|
|
25
|
+
Lutaml::Model::Type::String => ::String,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Native classes for value types whose dependency loads lazily
|
|
29
|
+
# (require "bigdecimal" / "date"). Looked up by constant name so the
|
|
30
|
+
# union never force-loads an optional dependency; if such a value
|
|
31
|
+
# exists at runtime, its constant is necessarily already defined.
|
|
32
|
+
# Decimal's native class (BigDecimal) loads lazily (require
|
|
33
|
+
# "bigdecimal"); looked up by constant name so the union never
|
|
34
|
+
# force-loads it. (Decimal is the only supported value type with a
|
|
35
|
+
# lazily-loaded native class.)
|
|
36
|
+
LAZY_NATIVE_CLASS_NAMES = {
|
|
37
|
+
Lutaml::Model::Type::Decimal => :BigDecimal,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
INTEGER_LEXICAL = /\A\s*[+-]?\d+\s*\z/
|
|
41
|
+
# Matches Type::Boolean.cast (XSD true/false/1/0 plus the library's
|
|
42
|
+
# t/f/yes/no/y/n forms), so a union :boolean accepts the same inputs as
|
|
43
|
+
# a plain :boolean attribute.
|
|
44
|
+
BOOLEAN_LEXICAL = /\A(true|false|t|f|yes|no|y|n|1|0)\z/i
|
|
45
|
+
|
|
46
|
+
FLOAT_LEXICAL = lambda do |string|
|
|
47
|
+
Float(string).finite?
|
|
48
|
+
rescue ArgumentError, TypeError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a String input belongs to a scalar member's lexical space.
|
|
53
|
+
# Keyed by member class (identity), parallel to NATIVE_CLASSES.
|
|
54
|
+
LEXICAL_PREDICATES = {
|
|
55
|
+
Lutaml::Model::Type::Integer => ->(s) { s.match?(INTEGER_LEXICAL) },
|
|
56
|
+
Lutaml::Model::Type::Float => FLOAT_LEXICAL,
|
|
57
|
+
Lutaml::Model::Type::Decimal => FLOAT_LEXICAL,
|
|
58
|
+
Lutaml::Model::Type::Boolean => ->(s) { s.match?(BOOLEAN_LEXICAL) },
|
|
59
|
+
Lutaml::Model::Type::String => ->(_s) { true },
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# Sound scalar union members are exactly the value types with a lexical
|
|
63
|
+
# predicate above: each maps to a distinct, stable Ruby runtime class
|
|
64
|
+
# (Integer, Float, BigDecimal, TrueClass/FalseClass, String), so the
|
|
65
|
+
# stateless value.class-driven serialization can always recover the
|
|
66
|
+
# member. Other value types are excluded because they cannot round-trip
|
|
67
|
+
# soundly in a union — e.g. Type::Date.cast can yield a DateTime, and
|
|
68
|
+
# Time / TimeWithoutDate share a Ruby Time class. Models are always OK.
|
|
69
|
+
SUPPORTED_VALUE_TYPES = LEXICAL_PREDICATES.keys.freeze
|
|
70
|
+
|
|
71
|
+
# Lossless numeric widening: a source value type may conform to a
|
|
72
|
+
# broader member. Narrowing (Float -> integer) and unrelated types are
|
|
73
|
+
# absent and therefore rejected. Exact-class matches are handled
|
|
74
|
+
# earlier by the instance_of? check in conform_scalar.
|
|
75
|
+
WIDENS_INTO = {
|
|
76
|
+
Integer => [Float, Decimal],
|
|
77
|
+
Float => [Decimal],
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
# Resolve the first member (in declared order) whose lexical/structural
|
|
82
|
+
# space the value belongs to, and the value cast to that member.
|
|
83
|
+
#
|
|
84
|
+
# @param value [Object] raw value (scalar, Hash sub-tree, or XmlElement)
|
|
85
|
+
# @param members [Array<Class>] resolved members, declared order
|
|
86
|
+
# @param format [Symbol, nil] serialization format being deserialized
|
|
87
|
+
# @param register [Symbol, nil] register for nested-model resolution
|
|
88
|
+
# @return [Array(Class, Object), nil] [member, casted] or nil (no match)
|
|
89
|
+
def conforming_member(value, members, format:, register:)
|
|
90
|
+
return nil if value.nil? || Utils.uninitialized?(value)
|
|
91
|
+
|
|
92
|
+
members.each do |member|
|
|
93
|
+
casted = if model_member?(member)
|
|
94
|
+
conform_model(member, value, format, register)
|
|
95
|
+
else
|
|
96
|
+
conform_scalar(member, value)
|
|
97
|
+
end
|
|
98
|
+
return [member, casted] unless casted == :no_match
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether a compiled mapping rule's attribute is a union — the single
|
|
104
|
+
# identity check every serializer touchpoint shares.
|
|
105
|
+
def rule?(rule)
|
|
106
|
+
rule.attribute_type == self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# An XML child is routed to member resolution (vs the plain-text
|
|
110
|
+
# scalar path) when it has child elements or attributes. Resolution
|
|
111
|
+
# tries model members by key coverage, then falls back to the scalar
|
|
112
|
+
# members on the element's text — so a text-only element with an
|
|
113
|
+
# unrelated attribute (e.g. xml:lang) still resolves to a scalar.
|
|
114
|
+
def xml_structured?(element)
|
|
115
|
+
return false unless element.respond_to?(:children)
|
|
116
|
+
|
|
117
|
+
element.children.any?(&:element?) || element.attributes.any?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Serialize a scalar union value through the value type matching its
|
|
121
|
+
# own class, so output is canonical/portable (a BigDecimal emits
|
|
122
|
+
# "1.5", not a raw Ruby object). value.class drives the type —
|
|
123
|
+
# stateless, identical to how a plain scalar attribute serializes.
|
|
124
|
+
# Falls back to the value as-is when it maps to no known value type.
|
|
125
|
+
def serialize_scalar(value, format)
|
|
126
|
+
type = scalar_type_for(value)
|
|
127
|
+
return value unless type
|
|
128
|
+
|
|
129
|
+
type.new(value).public_send(:"to_#{format}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_members!(members)
|
|
133
|
+
unless members.is_a?(::Array) && !members.empty?
|
|
134
|
+
raise ArgumentError, "union type must be a non-empty array of types"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
resolved = members.map { |member| resolve_member!(member) }
|
|
138
|
+
validate_catch_all_position!(resolved)
|
|
139
|
+
resolved
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_combo!(options)
|
|
143
|
+
present = INCOMPATIBLE_OPTIONS.select { |key| options[key] }
|
|
144
|
+
return if present.empty?
|
|
145
|
+
|
|
146
|
+
raise ArgumentError,
|
|
147
|
+
"union type cannot be combined with: #{present.join(', ')}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def model_member?(member)
|
|
153
|
+
member.is_a?(::Class) && member.include?(Serialize)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Structural (key-coverage) selection: every input key maps to one of
|
|
157
|
+
# the member's mapped fields, with no input key left unmapped. Once
|
|
158
|
+
# selected, field values cast leniently via the member's normal path.
|
|
159
|
+
# An existing member instance conforms as-is, keeping conformance
|
|
160
|
+
# idempotent — values can re-enter casting (e.g. a value transformer
|
|
161
|
+
# builds the member before the cast runs).
|
|
162
|
+
def conform_model(member, value, format, register)
|
|
163
|
+
return value if value.is_a?(member)
|
|
164
|
+
|
|
165
|
+
keys = input_keys(value, format)
|
|
166
|
+
return :no_match if keys.nil? || keys.empty?
|
|
167
|
+
|
|
168
|
+
field_names = member_field_names(member, format, register)
|
|
169
|
+
return :no_match unless keys.all? { |key| field_names.include?(key) }
|
|
170
|
+
|
|
171
|
+
# Plain-Ruby assignment (format nil) builds straight from the
|
|
172
|
+
# attribute hash, mirroring how non-union model attributes accept
|
|
173
|
+
# `Model.new(child: { ... })`; a format builds via the mappings.
|
|
174
|
+
return member.new(value) if format.nil?
|
|
175
|
+
|
|
176
|
+
member.apply_mappings(value, format, register: register)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A scalar conforms if the value already is the member's native Ruby
|
|
180
|
+
# class (covers parsed and plain-Ruby values), or if a String input
|
|
181
|
+
# belongs to the member's space. Lenient built-ins (Integer/Float/
|
|
182
|
+
# Decimal/Boolean/String) are gated by an explicit lexical predicate
|
|
183
|
+
# so their forgiving casts can't false-match; every other value type
|
|
184
|
+
# owns its space via its own (strict) cast — nil/raise means no match.
|
|
185
|
+
def conform_scalar(member, value)
|
|
186
|
+
# Scalar payload: an XML element contributes its text (so a scalar
|
|
187
|
+
# member matches when no model member covers it); Hash and primitive
|
|
188
|
+
# inputs pass through unchanged.
|
|
189
|
+
value = value.text if xml_element?(value)
|
|
190
|
+
|
|
191
|
+
# Exact native class (instance_of?): the value is precisely the
|
|
192
|
+
# member's native Ruby class (covers parsed and plain-Ruby values).
|
|
193
|
+
native = Array(NATIVE_CLASSES[member] || lazy_native_class(member))
|
|
194
|
+
return value if native.any? { |klass| value.instance_of?(klass) }
|
|
195
|
+
|
|
196
|
+
predicate = LEXICAL_PREDICATES[member]
|
|
197
|
+
return :no_match unless predicate
|
|
198
|
+
|
|
199
|
+
# String input: accept only within the member's lexical space.
|
|
200
|
+
if value.is_a?(::String)
|
|
201
|
+
return predicate.call(value) ? member.cast(value) : :no_match
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Native non-string value (e.g. a parsed Integer for a :float
|
|
205
|
+
# member): accept only as a lossless widening into a broader member,
|
|
206
|
+
# so a lenient built-in can't narrow or distort it — :float accepts
|
|
207
|
+
# 42, but :integer won't swallow 3.7 or 3.0. lossless_cast then
|
|
208
|
+
# guards precision (e.g. 2**60+1 is not exactly representable as
|
|
209
|
+
# Float).
|
|
210
|
+
source = scalar_type_for(value)
|
|
211
|
+
return :no_match unless WIDENS_INTO[source]&.include?(member)
|
|
212
|
+
|
|
213
|
+
lossless_cast(member, value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# A native non-string value conforms to a lenient built-in only if the
|
|
217
|
+
# member's cast preserves it (value-equal round-trip), so cross-numeric
|
|
218
|
+
# widening is accepted but narrowing/distortion is not.
|
|
219
|
+
def lossless_cast(member, value)
|
|
220
|
+
casted = member.cast(value)
|
|
221
|
+
casted == value ? casted : :no_match
|
|
222
|
+
rescue StandardError
|
|
223
|
+
:no_match
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def lazy_native_class(member)
|
|
227
|
+
const_name = LAZY_NATIVE_CLASS_NAMES[member]
|
|
228
|
+
return nil unless const_name && Object.const_defined?(const_name)
|
|
229
|
+
|
|
230
|
+
Object.const_get(const_name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The value type whose native Ruby class the value is an instance of
|
|
234
|
+
# (reverse of NATIVE_CLASSES plus the lazily-loaded native classes).
|
|
235
|
+
def scalar_type_for(value)
|
|
236
|
+
NATIVE_CLASSES.each do |type, klasses|
|
|
237
|
+
return type if Array(klasses).any? { |k| value.instance_of?(k) }
|
|
238
|
+
end
|
|
239
|
+
LAZY_NATIVE_CLASS_NAMES.each_key do |type|
|
|
240
|
+
klass = lazy_native_class(type)
|
|
241
|
+
return type if klass && value.instance_of?(klass)
|
|
242
|
+
end
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Input keys by format: Hash keys for key-value; child element local
|
|
247
|
+
# names + attribute names for XML; nil otherwise.
|
|
248
|
+
def input_keys(value, format)
|
|
249
|
+
return value.keys.map(&:to_s) if value.is_a?(::Hash)
|
|
250
|
+
|
|
251
|
+
if format == :xml && xml_element?(value)
|
|
252
|
+
names = value.children.select(&:element?).map(&:unprefixed_name) +
|
|
253
|
+
value.attributes.values.map(&:unprefixed_name)
|
|
254
|
+
return names.map(&:to_s)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def xml_element?(value)
|
|
261
|
+
value.respond_to?(:children) && value.respond_to?(:attributes)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The set of mapped field names for a member in the given format.
|
|
265
|
+
# One source, parameterized by format. A nil format is plain-Ruby
|
|
266
|
+
# assignment, which has no mapping context — match on attribute names.
|
|
267
|
+
def member_field_names(member, format, register)
|
|
268
|
+
return member.attributes.keys.map(&:to_s) if format.nil?
|
|
269
|
+
|
|
270
|
+
mapping = member.mappings_for(format, register)
|
|
271
|
+
rules = if format == :xml
|
|
272
|
+
mapping.elements(register) + mapping.attributes(register)
|
|
273
|
+
else
|
|
274
|
+
mapping.mappings(register)
|
|
275
|
+
end
|
|
276
|
+
rules.flat_map do |rule|
|
|
277
|
+
(rule.multiple_mappings? ? rule.name : [rule.name]).map(&:to_s)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def resolve_member!(member)
|
|
282
|
+
if member.is_a?(::Hash)
|
|
283
|
+
raise ArgumentError,
|
|
284
|
+
"union members must be types or symbols; option hashes " \
|
|
285
|
+
"(e.g. { ref: ... }) are not supported as union members"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
klass =
|
|
289
|
+
begin
|
|
290
|
+
Lutaml::Model::Attribute.cast_type!(member)
|
|
291
|
+
rescue ArgumentError
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
unless klass
|
|
295
|
+
raise ArgumentError, "invalid union member: #{member.inspect}"
|
|
296
|
+
end
|
|
297
|
+
return klass if klass.is_a?(::Class) && klass.include?(Serialize)
|
|
298
|
+
return klass if SUPPORTED_VALUE_TYPES.include?(klass)
|
|
299
|
+
|
|
300
|
+
raise ArgumentError,
|
|
301
|
+
"unsupported union member type: #{member.inspect}. Supported " \
|
|
302
|
+
"scalar members are :string, :integer, :float, :decimal, " \
|
|
303
|
+
":boolean, or a Serializable model"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The catch-all must be the last member overall (not merely the last
|
|
307
|
+
# value type): XML scalar matching reads element.text, so a :string
|
|
308
|
+
# placed before a model member would swallow structured elements too.
|
|
309
|
+
def validate_catch_all_position!(resolved)
|
|
310
|
+
return unless resolved.include?(Lutaml::Model::Type::String)
|
|
311
|
+
return if resolved.last == Lutaml::Model::Type::String
|
|
312
|
+
|
|
313
|
+
raise ArgumentError,
|
|
314
|
+
"a universal catch-all type must be the last union member"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -102,6 +102,12 @@ module Lutaml
|
|
|
102
102
|
snake_case(base_class_name(klass))
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Return the final segment of a `prefix:local` style qualified name,
|
|
106
|
+
# e.g. `"xs:string" -> "string"`. Returns nil for nil input.
|
|
107
|
+
def last_of_split(field)
|
|
108
|
+
field&.split(":")&.last
|
|
109
|
+
end
|
|
110
|
+
|
|
105
111
|
def initialized?(value)
|
|
106
112
|
!value.equal?(UNINITIALIZED)
|
|
107
113
|
end
|
|
@@ -14,10 +14,7 @@ module Lutaml
|
|
|
14
14
|
sub_errors = value.validate
|
|
15
15
|
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
16
16
|
else
|
|
17
|
-
|
|
18
|
-
attr, register, self
|
|
19
|
-
)
|
|
20
|
-
attr.validate_value!(value, register, resolver)
|
|
17
|
+
attr.validate_value!(value, register, instance_object: self)
|
|
21
18
|
end
|
|
22
19
|
rescue Lutaml::Model::CollectionCountOutOfRangeError => e
|
|
23
20
|
errors << e unless attr.choice
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -142,6 +142,8 @@ module Lutaml
|
|
|
142
142
|
autoload :UnknownTypeError, "#{__dir__}/model/error/unknown_type_error"
|
|
143
143
|
autoload :RequiredAttributeMissingError,
|
|
144
144
|
"#{__dir__}/model/error/required_attribute_missing_error"
|
|
145
|
+
autoload :UnionSchemaUnsupportedError,
|
|
146
|
+
"#{__dir__}/model/error/union_schema_unsupported_error"
|
|
145
147
|
autoload :MultipleMappingsError,
|
|
146
148
|
"#{__dir__}/model/error/multiple_mappings_error"
|
|
147
149
|
autoload :CollectionTrueMissingError,
|
|
@@ -235,6 +237,22 @@ module Lutaml
|
|
|
235
237
|
def self.reset_configuration!
|
|
236
238
|
@configuration = nil
|
|
237
239
|
end
|
|
240
|
+
|
|
241
|
+
# Convenience accessors for runtime/platform detection.
|
|
242
|
+
# Delegates to RuntimeCompatibility so call sites can write
|
|
243
|
+
# `Lutaml::Model.opal?` (or `Model.opal?` inside the namespace)
|
|
244
|
+
# instead of the long `Lutaml::Model::RuntimeCompatibility.opal?`.
|
|
245
|
+
def self.opal?
|
|
246
|
+
RuntimeCompatibility.opal?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def self.native?
|
|
250
|
+
RuntimeCompatibility.native?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def self.windows?
|
|
254
|
+
RuntimeCompatibility.windows?
|
|
255
|
+
end
|
|
238
256
|
end
|
|
239
257
|
end
|
|
240
258
|
|