lutaml-model 0.8.30 → 0.8.31

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b34b7ff545ae437b23517945172d87ca4a954c0c4fad64c15fce8e9f3f14e02c
4
- data.tar.gz: 8db18d5b33eef8ea93d920ee49db17511095c697bbddda129ff6ab6506970ac7
3
+ metadata.gz: 1ffdb7bcf72a5a1f4b3339ed331fe96def70246dd36010d23a469e4d27c7f103
4
+ data.tar.gz: edb32bba950336b77f2257383aec513126d0bdfb53d6fefa7f5eecbf5fe48f74
5
5
  SHA512:
6
- metadata.gz: 7134619f7177e46dfd8558489092814d1b84fdae0e0a8bd2a33c95afbf9fba721d81928a30a4911b8833baccfead7a70bd9bd2a0633c00e4cdcf45b50178a86e
7
- data.tar.gz: fa4f5701225229b3d2f1b1e8a18d00797bf96fad94b99ff2875bf177f5fcfbeee418099a5fd0686245c1dffda6cd57d2f716411b315f5e87731e77f8e7700b51
6
+ metadata.gz: 489d6825726abe7fb758d544938a679c652e1a0eeb5699757eea57884197b4bd4890cda632231772226ac794ec91f5eac3fc21988594c1dba3c1151a1fe33dac
7
+ data.tar.gz: 782cba0c296ffa16381f1f36c5614ca7536e3c7a4e628cef77f71d27a1acf37eada825de229d186ca76115149970eb7a110d04a6a5177d1331132f5f6ba8db10
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "lutaml/key_value/document"
4
+
3
5
  module Lutaml
4
6
  module Json
5
7
  module Adapter
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "../generator_options"
5
+ require_relative "document"
5
6
 
6
7
  module Lutaml
7
8
  module Json
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.30"
5
+ VERSION = "0.8.31"
6
6
  end
7
7
  end
@@ -239,7 +239,21 @@ Lutaml::Model::Serialize.prepend(
239
239
  # ModelImportExt override (root?) must land on ClassMethods too. The other
240
240
  # two (FormatConversion, InstanceMethods) are already prepended unconditionally
241
241
  # above, so Opal's no-double-prepend rule means we don't repeat them here.
242
- if Lutaml::Model.opal?
242
+ #
243
+ # TruffleRuby (34.0.1) has the same propagation gap, on both the include
244
+ # side and the extend/singleton side — a module prepended into an
245
+ # already-extended module does not reach prior extenders' singleton chains
246
+ # either (https://github.com/truffleruby/truffleruby/issues/4452). Without
247
+ # the include-side re-prepends below, Mml.parse et al. die with
248
+ # "undefined method 'pending_plan_root_element='". The extend side
249
+ # additionally needs FormatConversion re-prepended onto Serializable's
250
+ # singleton: otherwise `model PlainClass` runs the no-op base
251
+ # add_format_specific_model_methods and plain (non-Serialize) model classes
252
+ # never receive their XML accessors (encoding=, element_order=, ordered=,
253
+ # mixed, doctype, xml_declaration, raw_schema_location), so
254
+ # ModelTransform#apply_xml_mapping dies at `instance.encoding = ...` —
255
+ # e.g. every relaton ItemData parse via Relaton::Cli.parse_xml.
256
+ if Lutaml::Model.opal? || RUBY_ENGINE == "truffleruby"
243
257
  Lutaml::Model::Serializable.singleton_class.prepend(
244
258
  Lutaml::Xml::Serialization::ModelImportExt,
245
259
  )
@@ -251,6 +265,14 @@ if Lutaml::Model.opal?
251
265
  Lutaml::Model::Serialize::ClassMethods.prepend(
252
266
  Lutaml::Xml::Serialization::ModelImportExt,
253
267
  )
268
+
269
+ # TruffleRuby only: Opal's no-double-prepend rule makes the unconditional
270
+ # ClassMethods.prepend(FormatConversion) above sufficient there.
271
+ if RUBY_ENGINE == "truffleruby"
272
+ Lutaml::Model::Serializable.singleton_class.prepend(
273
+ Lutaml::Xml::Serialization::FormatConversion,
274
+ )
275
+ end
254
276
  end
255
277
 
256
278
  # Register XML-specific attribute override warning names
@@ -323,7 +323,13 @@ module Lutaml
323
323
  strict_match = uri && ns_info[:prefix] &&
324
324
  !ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
325
325
  if strict_match
326
- ["#{uri}:#{name}"].freeze
326
+ # Type-level qualification stays lenient on parse: a W3C
327
+ # unprefixed attribute carries no namespace, and 0.8.19 bound
328
+ # it to the type-qualified rule (lutaml-model#786). Only rule
329
+ # and schema-level qualification is strictly prefixed-only.
330
+ names = ["#{uri}:#{name}"]
331
+ names << name.to_s if ns_info[:via_type_namespace]
332
+ names.freeze
327
333
  else
328
334
  namespaced_names(default_namespace)
329
335
  end
@@ -471,6 +477,7 @@ form_default = :unqualified)
471
477
  if attr && (type_ns_class = attr.type_namespace_class(register)) &&
472
478
  type_ns_class != :blank
473
479
  result = build_namespace_result_from_class(type_ns_class)
480
+ result[:via_type_namespace] = true
474
481
  # CRITICAL W3C FLAG: Mark when attribute is in same namespace as parent with :unqualified
475
482
  # Serialization code will check this to omit prefix per attributeFormDefault
476
483
  if type_ns_class.uri == parent_ns_class&.uri && form_default == :unqualified
@@ -985,9 +985,41 @@ effective_register = lutaml_register)
985
985
  parent_ns_class = nil
986
986
  form_default = :unqualified
987
987
  end
988
- rule.attribute_match_names(attr, effective_register, parent_ns_class,
989
- form_default,
990
- options[:default_namespace])
988
+ names = rule.attribute_match_names(attr, effective_register,
989
+ parent_ns_class, form_default,
990
+ options[:default_namespace])
991
+
992
+ # #786 lenient alias (["uri:name", "name"] from a type-qualified
993
+ # rule): the unprefixed form must not steal the value when a
994
+ # sibling namespace-less rule owns the bare local name (#744
995
+ # disjointness). No sibling — the alias keeps 0.8.19 leniency.
996
+ if names.size == 2 && !names[1].include?(":") &&
997
+ instance_is_serialize &&
998
+ unqualified_sibling_attribute_rule?(instance, rule,
999
+ effective_register)
1000
+ names = names.first(1)
1001
+ end
1002
+ names
1003
+ end
1004
+
1005
+ # Whether another attribute rule with the same local name resolves
1006
+ # namespace-less (no rule namespace, no value-type namespace) —
1007
+ # the #744 owner of the unprefixed source form.
1008
+ def unqualified_sibling_attribute_rule?(instance, rule,
1009
+ effective_register)
1010
+ mapping = instance.class.mappings_for(:xml, effective_register)
1011
+ return false unless mapping
1012
+
1013
+ attrs = instance.class.attributes(effective_register)
1014
+ mapping.mappings.any? do |sib|
1015
+ next false unless sib.attribute? && !sib.equal?(rule)
1016
+ next false unless sib.name.to_s == rule.name.to_s
1017
+ next false if sib.namespace_set?
1018
+
1019
+ sib_attr = attrs[sib.to]
1020
+ type_ns = sib_attr&.type_namespace_class(effective_register)
1021
+ type_ns.nil? || type_ns == :blank
1022
+ end
991
1023
  end
992
1024
 
993
1025
  # [namespace_class, attribute_form_default] for the instance's model,
@@ -140,9 +140,25 @@ parent_element_form_default)
140
140
  actual_class = resolve_polymorphic_class(rule, value, is_polymorphic,
141
141
  is_polymorphic_subtype)
142
142
 
143
+ # Dispatch on value's actual class when it is a Serializable that
144
+ # differs from the declared attribute_type. This covers ad-hoc
145
+ # polymorphism (caller assigns an unrelated Serializable to a typed
146
+ # slot) and prevents the wrong transformation being applied
147
+ # silently. Subsumes is_polymorphic_subtype; preserved here for
148
+ # the resolve_polymorphic_class call above.
149
+ union || is_polymorphic ||
150
+ (value.is_a?(Lutaml::Model::Serialize) &&
151
+ value.class != rule.attribute_type)
152
+
143
153
  # Get transformation for the actual class. Unions resolve the member
144
154
  # from the value's own class, like polymorphism.
145
- child_transformation = if union
155
+ # Unions and unrelated Serializables dispatch on the value's
156
+ # own class (subsumes the subtype case); polymorphic config
157
+ # keeps its resolved-class dispatch (the class_map target).
158
+ child_transformation = if union ||
159
+ (!is_polymorphic &&
160
+ value.is_a?(Lutaml::Model::Serialize) &&
161
+ value.class != rule.attribute_type)
146
162
  value.class.transformation_for(:xml,
147
163
  register)
148
164
  elsif is_polymorphic || is_polymorphic_subtype
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "lutaml/key_value/document"
4
+
3
5
  module Lutaml
4
6
  module Yaml
5
7
  module Adapter
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "document"
4
+
3
5
  require "date"
4
6
  require "yaml"
5
7
 
@@ -36,9 +36,13 @@ module Lutaml
36
36
  @attributes
37
37
  end
38
38
 
39
- require "yeptris"
40
- require "yeptris/yaml"
41
- Yeptris::YAML.dump(attributes_to_serialize)
39
+ # Generation must match the standard (Psych) adapter's output
40
+ # byte-for-byte: document separator, quoting of digit-leading
41
+ # strings, flow styles. The neutral Yeptris::YAML.dump is
42
+ # deliberately headerless, so parity goes through the
43
+ # Psych-compatible face of the same engine.
44
+ require "yeptris/psych"
45
+ Yeptris::Psych.dump(attributes_to_serialize)
42
46
  end
43
47
  end
44
48
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The adapter resolver loads adapter files by raw path (load_adapter_file),
4
+ # bypassing the autoload tree that normally defines Document before
5
+ # StandardAdapter references it at class-definition time. Each deep adapter
6
+ # require must therefore be self-sufficient.
7
+ RSpec.describe "raw adapter requires" do
8
+ it "loads the json yeptris adapter without the namespace preloaded" do
9
+ out = `bundle exec ruby -e 'require "lutaml/json/adapter/yeptris_adapter"; puts Lutaml::Json::Adapter::YeptrisAdapter.name' 2>&1`
10
+ expect(out).to include("Lutaml::Json::Adapter::YeptrisAdapter")
11
+ end
12
+
13
+ it "loads the yaml yeptris adapter without the namespace preloaded" do
14
+ out = `bundle exec ruby -e 'require "lutaml/yaml/adapter/yeptris_adapter"; puts Lutaml::Yaml::Adapter::YeptrisAdapter.name' 2>&1`
15
+ expect(out).to include("Lutaml::Yaml::Adapter::YeptrisAdapter")
16
+ end
17
+
18
+ it "loads the json standard adapter standalone" do
19
+ out = `bundle exec ruby -e 'require "lutaml/json/adapter/standard_adapter"; puts Lutaml::Json::Adapter::StandardAdapter.name' 2>&1`
20
+ expect(out).to include("Lutaml::Json::Adapter::StandardAdapter")
21
+ end
22
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_string: true
2
+
3
+ require "spec_helper"
4
+
5
+ # When a caller assigns an unrelated Lutaml::Model::Serializable (not a
6
+ # declared subtype) to a typed slot, serialisation must use the value's own
7
+ # class mapping rules. The prior behaviour reused the declared
8
+ # attribute_type's child_transformation regardless of value.class, which
9
+ # silently applied the wrong rules and raised NoMethodError on whichever
10
+ # Ruby attribute name differed.
11
+ #
12
+ # See BUGREPORT.element-builder-dispatch-on-value-class.md for the original
13
+ # analysis and sts-ruby PR #47 for the downstream trigger.
14
+ RSpec.describe "ElementBuilder cross-class dispatch" do
15
+ let(:declared_type) do
16
+ Class.new(Lutaml::Model::Serializable) do
17
+ attribute :name, :string
18
+
19
+ xml do
20
+ root "declared"
21
+ map_element "name", to: :name
22
+ end
23
+ end
24
+ end
25
+
26
+ let(:actual_type) do
27
+ Class.new(Lutaml::Model::Serializable) do
28
+ attribute :different_name, :string
29
+
30
+ xml do
31
+ root "actual"
32
+ map_element "different-name", to: :different_name
33
+ end
34
+ end
35
+ end
36
+
37
+ let(:holder_class) do
38
+ declared = declared_type
39
+ Class.new(Lutaml::Model::Serializable) do
40
+ attribute :child, declared
41
+
42
+ xml do
43
+ root "holder"
44
+ map_element "child", to: :child
45
+ end
46
+ end
47
+ end
48
+
49
+ it "serialises an unrelated Serializable using its own mapping" do
50
+ holder = holder_class.new
51
+ holder.child = actual_type.new(different_name: "x")
52
+ xml = holder.to_xml
53
+
54
+ parsed = Nokogiri::XML(xml)
55
+ expect(parsed.at_css("different-name").content).to eq("x")
56
+ end
57
+
58
+ it "preserves the declared-type fast path for matching classes" do
59
+ holder = holder_class.new
60
+ holder.child = declared_type.new(name: "y")
61
+ xml = holder.to_xml
62
+
63
+ parsed = Nokogiri::XML(xml)
64
+ expect(parsed.at_css("name").content).to eq("y")
65
+ end
66
+
67
+ it "round-trips the XML output (parsing yields declared type by design)" do
68
+ # Deserialisation cannot infer the runtime-assigned class without a
69
+ # discriminator; it instantiates the declared type. The XML round-trip
70
+ # itself must still be lossless.
71
+ holder = holder_class.new
72
+ holder.child = actual_type.new(different_name: "round-trip")
73
+ xml = holder.to_xml
74
+
75
+ reparsed = holder_class.from_xml(xml)
76
+ expect(reparsed.child).to be_a(declared_type)
77
+ expect(xml).to include("<different-name>round-trip</different-name>")
78
+ end
79
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model"
5
+
6
+ # Regression spec for the XML accessor wiring of plain (non-Serialize)
7
+ # model classes declared via `model PlainClass`.
8
+ #
9
+ # The accessors are installed by
10
+ # FormatConversion#add_format_specific_model_methods, which must override
11
+ # the no-op base through Serialize::ClassMethods' prepend chain. Engines
12
+ # that do not propagate a prepend into an already-extended module to prior
13
+ # extenders' singleton chains (TruffleRuby —
14
+ # https://github.com/truffleruby/truffleruby/issues/4452) silently lose the
15
+ # override; this spec fails there without the format.rb re-prepend.
16
+ RSpec.describe "Plain model class XML accessors" do
17
+ let(:plain_class) { Class.new }
18
+
19
+ let(:wrapper_class) do
20
+ pc = plain_class
21
+ Class.new(Lutaml::Model::Serializable) do
22
+ model pc
23
+
24
+ xml do
25
+ element "thing"
26
+ end
27
+
28
+ def self.name
29
+ "PlainModelWrapper"
30
+ end
31
+ end
32
+ end
33
+
34
+ it "installs the XML accessors on the plain model class" do
35
+ wrapper_class # force the `model` declaration
36
+
37
+ %i[encoding encoding= doctype= element_order= ordered= mixed=
38
+ xml_declaration= raw_schema_location=].each do |m|
39
+ expect(plain_class.method_defined?(m)).to(be(true),
40
+ "expected #{plain_class} to define ##{m}")
41
+ end
42
+ end
43
+
44
+ it "parses XML into the plain model instance" do
45
+ instance = wrapper_class.from_xml("<thing/>")
46
+
47
+ expect(instance).to be_a(plain_class)
48
+ expect(instance.element_order).not_to be_nil
49
+ end
50
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # An attribute rule with no namespace of its own whose VALUE TYPE is
6
+ # namespace-qualified (Lutaml::Xml::Type::Configurable) must keep
7
+ # binding unprefixed source attributes — per W3C Namespaces an
8
+ # unprefixed attribute carries no namespace, and 0.8.19 matched it
9
+ # leniently. The #758 strict prefixed-only match applies to rule- and
10
+ # schema-level qualification, not to the value type's (lutaml-model#786).
11
+ module TypeQualifiedAttributeLeniencySpec
12
+ class W14Ns < Lutaml::Xml::Namespace
13
+ uri "http://schemas.microsoft.com/office/word/2010/wordml"
14
+ prefix_default "w14"
15
+ element_form_default :qualified
16
+ attribute_form_default :qualified
17
+ end
18
+
19
+ class W14String < Lutaml::Model::Type::String
20
+ include Lutaml::Xml::Type::Configurable
21
+
22
+ xml do
23
+ namespace W14Ns
24
+ end
25
+ end
26
+
27
+ class WNs < Lutaml::Xml::Namespace
28
+ uri "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
29
+ prefix_default "w"
30
+ element_form_default :qualified
31
+ end
32
+
33
+ W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
34
+ W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml"
35
+
36
+ class Bookmark < Lutaml::Model::Serializable
37
+ attribute :id, :string
38
+ attribute :v, W14String
39
+
40
+ xml do
41
+ element "bookmarkStart"
42
+ namespace WNs
43
+ map_attribute "id", to: :id
44
+ map_attribute "displacedByCustomXml", to: :v
45
+ end
46
+ end
47
+ end
48
+
49
+ RSpec.describe "type-qualified attribute rules" do
50
+ it "bind unprefixed source attributes (W3C: unprefixed = no namespace)" do
51
+ xml = %(<w:bookmarkStart xmlns:w="#{TypeQualifiedAttributeLeniencySpec::W_NS}" displacedByCustomXml="next" id="0"/>)
52
+ doc = TypeQualifiedAttributeLeniencySpec::Bookmark.from_xml(xml)
53
+
54
+ expect(doc.v).to eq("next")
55
+ expect(doc.id).to eq("0")
56
+ expect(doc.to_xml).to include(%(w14:displacedByCustomXml="next"))
57
+ end
58
+
59
+ it "keep binding prefixed source attributes in the type namespace" do
60
+ xml = %(<w:bookmarkStart xmlns:w="#{TypeQualifiedAttributeLeniencySpec::W_NS}" xmlns:w14="#{TypeQualifiedAttributeLeniencySpec::W14_NS}" w14:displacedByCustomXml="next" id="0"/>)
61
+ doc = TypeQualifiedAttributeLeniencySpec::Bookmark.from_xml(xml)
62
+
63
+ expect(doc.v).to eq("next")
64
+ expect(doc.to_xml).to include(%(w14:displacedByCustomXml="next"))
65
+ end
66
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.30
4
+ version: 0.8.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1734,6 +1734,7 @@ files:
1734
1734
  - spec/lutaml/model/ordered_content_spec.rb
1735
1735
  - spec/lutaml/model/polymorphic_spec.rb
1736
1736
  - spec/lutaml/model/processing_instruction_spec.rb
1737
+ - spec/lutaml/model/raw_adapter_require_spec.rb
1737
1738
  - spec/lutaml/model/raw_element_spec.rb
1738
1739
  - spec/lutaml/model/register_key_value_spec.rb
1739
1740
  - spec/lutaml/model/register_methods_spec.rb
@@ -1859,6 +1860,7 @@ files:
1859
1860
  - spec/lutaml/xml/conformance/xml_namespaces_spec.rb
1860
1861
  - spec/lutaml/xml/conformance/xml_schema_instance_spec.rb
1861
1862
  - spec/lutaml/xml/content_model_validation_spec.rb
1863
+ - spec/lutaml/xml/cross_class_dispatch_spec.rb
1862
1864
  - spec/lutaml/xml/data_model_spec.rb
1863
1865
  - spec/lutaml/xml/decisions/decision_engine_spec.rb
1864
1866
  - spec/lutaml/xml/decisions/decision_spec.rb
@@ -1914,6 +1916,7 @@ files:
1914
1916
  - spec/lutaml/xml/ordered_method_spec.rb
1915
1917
  - spec/lutaml/xml/out_of_namespace_parsing_spec.rb
1916
1918
  - spec/lutaml/xml/pipeline_integration_spec.rb
1919
+ - spec/lutaml/xml/plain_model_accessors_spec.rb
1917
1920
  - spec/lutaml/xml/plan_fast_path_spec.rb
1918
1921
  - spec/lutaml/xml/plan_walk_spec.rb
1919
1922
  - spec/lutaml/xml/prefix_control_spec.rb
@@ -1954,6 +1957,7 @@ files:
1954
1957
  - spec/lutaml/xml/type_namespace_prefix_spec.rb
1955
1958
  - spec/lutaml/xml/type_namespace_resolver_spec.rb
1956
1959
  - spec/lutaml/xml/type_namespace_roundtrip_spec.rb
1960
+ - spec/lutaml/xml/type_qualified_attribute_leniency_spec.rb
1957
1961
  - spec/lutaml/xml/validate_xml_with_constructs_spec.rb
1958
1962
  - spec/lutaml/xml/validate_xml_with_spec.rb
1959
1963
  - spec/lutaml/xml/w3c_types_spec.rb