lutaml-model 0.8.56 → 0.8.106

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/scripts/test_installed_gem.rb +58 -0
  3. data/.github/workflows/downstream-performance.yml +28 -3
  4. data/.github/workflows/js-pr-check.yml +4 -0
  5. data/.github/workflows/js-sync.yml +4 -0
  6. data/.github/workflows/opal.yml +9 -0
  7. data/.github/workflows/rake.yml +6 -41
  8. data/.github/workflows/release.yml +41 -0
  9. data/Gemfile +17 -3
  10. data/bench/bench_alloc_audit.rb +56 -0
  11. data/bench/bench_common.rb +11 -0
  12. data/bench/bench_merge_attempts.rb +49 -0
  13. data/docs/_guides/gem-integration.adoc +85 -0
  14. data/docs/_guides/grammar-backed-formats.adoc +69 -0
  15. data/docs/_guides/index.adoc +8 -1
  16. data/docs/_guides/native-engines.adoc +23 -5
  17. data/docs/_guides/schema-generation.adoc +25 -0
  18. data/docs/_guides/type-registers-contexts.adoc +126 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +7 -5
  20. data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
  21. data/lib/lutaml/key_value/data_model/element.rb +5 -1
  22. data/lib/lutaml/key_value/mapping_rule.rb +7 -0
  23. data/lib/lutaml/key_value/transform.rb +29 -24
  24. data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
  25. data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
  26. data/lib/lutaml/key_value/transformation.rb +64 -116
  27. data/lib/lutaml/model/adapter_resolver.rb +7 -3
  28. data/lib/lutaml/model/attribute.rb +74 -15
  29. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  30. data/lib/lutaml/model/configuration.rb +13 -0
  31. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
  33. data/lib/lutaml/model/render_policy.rb +26 -8
  34. data/lib/lutaml/model/schema/base_schema.rb +15 -0
  35. data/lib/lutaml/model/schema/generator/definition.rb +52 -8
  36. data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
  37. data/lib/lutaml/model/schema.rb +4 -2
  38. data/lib/lutaml/model/serializable.rb +33 -0
  39. data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
  40. data/lib/lutaml/model/serialize/builder.rb +22 -0
  41. data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
  42. data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
  43. data/lib/lutaml/model/serialize/initialization.rb +54 -7
  44. data/lib/lutaml/model/serialize/model_import.rb +60 -0
  45. data/lib/lutaml/model/serialize.rb +14 -6
  46. data/lib/lutaml/model/type/time.rb +6 -0
  47. data/lib/lutaml/model/type/value.rb +9 -7
  48. data/lib/lutaml/model/type_resolver.rb +7 -5
  49. data/lib/lutaml/model/utils.rb +18 -5
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/model.rb +7 -0
  52. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
  53. data/lib/lutaml/xml/adapter_loader.rb +5 -2
  54. data/lib/lutaml/xml/format.rb +18 -16
  55. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  56. data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
  57. data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
  58. data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
  59. data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
  60. data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
  61. data/lib/lutaml/xml/leptris.rb +46 -0
  62. data/lib/lutaml/xml/mapping.rb +71 -18
  63. data/lib/lutaml/xml/model_transform.rb +231 -8
  64. data/lib/lutaml/xml/namespace.rb +18 -8
  65. data/lib/lutaml/xml/namespace_collector.rb +17 -1
  66. data/lib/lutaml/xml/namespace_needs.rb +77 -49
  67. data/lib/lutaml/xml/parse_session.rb +7 -19
  68. data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
  69. data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
  70. data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
  71. data/lib/lutaml/xml/transformation.rb +15 -1
  72. data/lib/lutaml/xml/type/serializers.rb +8 -1
  73. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
  74. data/lib/tasks/performance_probe_plan_walk.rb +1 -1
  75. data/lutaml-model.gemspec +7 -4
  76. data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
  77. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
  78. data/spec/lutaml/key_value/transformation_spec.rb +87 -21
  79. data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
  80. data/spec/lutaml/model/collection_validation_spec.rb +265 -0
  81. data/spec/lutaml/model/desc_spec.rb +50 -0
  82. data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
  83. data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
  84. data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
  85. data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
  86. data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
  87. data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
  88. data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
  89. data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
  90. data/spec/lutaml/model/store_spec.rb +6 -2
  91. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  92. data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
  93. data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
  94. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  95. data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
  96. data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
  97. data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
  98. data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
  99. data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
  100. data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
  101. data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
  102. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
  103. data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
  104. data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
  105. data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
  106. data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
  107. data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
  108. data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
  109. data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
  110. data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
  111. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
  112. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  113. data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
  114. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
  115. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
  116. data/spec/person_spec.rb +8 -0
  117. metadata +48 -17
  118. data/lib/lutaml/xml/plan_compiler.rb +0 -332
  119. data/lib/lutaml/xml/plan_hydrator.rb +0 -367
  120. data/lib/lutaml/xml/plan_order.rb +0 -53
  121. data/lib/lutaml/xml/plan_serializer.rb +0 -252
  122. data/lib/lutaml/xml/plan_walk.rb +0 -30
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module GenerateManySpec
6
+ class Source < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ end
9
+
10
+ class Other < Lutaml::Model::Serializable
11
+ attribute :label, :string
12
+ end
13
+
14
+ class Consumer < Lutaml::Model::Serializable
15
+ attribute :inner, Source
16
+ end
17
+ end
18
+
19
+ RSpec.describe Lutaml::Model::Schema::BaseSchema do
20
+ describe ".generate_many" do
21
+ it "covers every root in one document without a top-level $ref" do
22
+ parsed = JSON.parse(
23
+ Lutaml::Model::Schema::JsonSchema.generate_many(
24
+ [GenerateManySpec::Source, GenerateManySpec::Other],
25
+ ),
26
+ )
27
+
28
+ expect(parsed).not_to have_key("$ref")
29
+ expect(parsed["$defs"]).to have_key("GenerateManySpec_Source")
30
+ expect(parsed["$defs"]).to have_key("GenerateManySpec_Other")
31
+ end
32
+
33
+ it "registers a class shared by several roots once" do
34
+ parsed = JSON.parse(
35
+ Lutaml::Model::Schema::JsonSchema.generate_many(
36
+ [GenerateManySpec::Source, GenerateManySpec::Consumer],
37
+ ),
38
+ )
39
+
40
+ expect(parsed["$defs"].keys.size).to eq(2)
41
+ expect(parsed["$defs"]).to have_key("GenerateManySpec_Consumer")
42
+ end
43
+
44
+ it "formats YAML output" do
45
+ yaml = Lutaml::Model::Schema::YamlSchema.generate_many(
46
+ [GenerateManySpec::Source, GenerateManySpec::Other],
47
+ )
48
+
49
+ expect(yaml).to start_with("%YAML")
50
+ end
51
+ end
52
+
53
+ describe "Generator::DefinitionsCollection.from_classes" do
54
+ it "builds one collection covering all roots" do
55
+ collection = Lutaml::Model::Schema::Generator::DefinitionsCollection
56
+ .from_classes([GenerateManySpec::Source, GenerateManySpec::Consumer])
57
+
58
+ expect(collection.to_schema.keys.size).to eq(2)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ require "spec_helper"
7
+
8
+ RSpec.describe "JSON/YAML schema generation regressions (#863, #864, #865, #866)" do
9
+ before do
10
+ stub_const("SchemaRepro::Node", node_class)
11
+ stub_const("SchemaRepro::Outer", outer_class)
12
+ stub_const("SchemaRepro::Relation", relation_class)
13
+ stub_const("SchemaRepro::Simple", simple_class)
14
+ end
15
+
16
+ let(:node_class) do
17
+ klass = Class.new(Lutaml::Model::Serializable) do
18
+ attribute :name, :string
19
+ xml { element "Node" }
20
+ end
21
+ klass.attribute :child, klass
22
+ klass
23
+ end
24
+
25
+ let(:outer_class) do
26
+ Class.new(Lutaml::Model::Serializable) do
27
+ choice(min: 1, max: 1) do
28
+ choice(min: 1, max: 1) do
29
+ attribute :from, :string
30
+ attribute :to, :string
31
+ end
32
+ attribute :at, :string
33
+ end
34
+ xml { element "Outer" }
35
+ end
36
+ end
37
+
38
+ let(:relation_class) do
39
+ Class.new(Lutaml::Model::Serializable) do
40
+ attribute :type, :string
41
+ choice(min: 1, max: 1) do
42
+ attribute :locality, :string, collection: true, initialize_empty: true
43
+ attribute :locality_stack, :string, collection: true, initialize_empty: true
44
+ end
45
+ choice(min: 1, max: 1) do
46
+ attribute :source_locality, :string, collection: true, initialize_empty: true
47
+ attribute :source_locality_stack, :string, collection: true, initialize_empty: true
48
+ end
49
+ xml { element "Relation" }
50
+ end
51
+ end
52
+
53
+ let(:simple_class) do
54
+ Class.new(Lutaml::Model::Serializable) do
55
+ attribute :x, :string
56
+ xml { element "Simple" }
57
+ end
58
+ end
59
+
60
+ describe "#863: recursive models" do
61
+ it "terminates and emits one definition per class" do
62
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Node))
63
+ expect(schema["$defs"].keys).to contain_exactly("SchemaRepro_Node")
64
+ end
65
+
66
+ it "generates through the YAML path" do
67
+ yaml = Lutaml::Model::Schema.to_yaml(SchemaRepro::Node)
68
+ expect(YAML.safe_load(yaml)).to include("$defs")
69
+ end
70
+ end
71
+
72
+ describe "#864: nested choice blocks" do
73
+ it "collapses the nested group into the outer at-most-one set" do
74
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Outer))
75
+ one_of = schema["$defs"]["SchemaRepro_Outer"]["oneOf"]
76
+ requireds = one_of.map { |b| b["allOf"].to_a.filter_map { |s| s["required"]&.first } }
77
+ expect(requireds).to contain_exactly([], %w[from], %w[to], %w[at])
78
+ end
79
+ end
80
+
81
+ describe "#865: independent choices combine conjunctively" do
82
+ it "renders an allOf of oneOf at-most-one groups" do
83
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
84
+ all_of = schema["$defs"]["SchemaRepro_Relation"]["allOf"]
85
+ expect(all_of.length).to eq(2)
86
+ all_of.each do |group|
87
+ expect(group).to have_key("oneOf")
88
+ expect(group["oneOf"].length).to eq(3)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "#869: the serializer's own output validates" do
94
+ it "emits no required keys for initialize_empty choice members" do
95
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
96
+ definition = schema["$defs"]["SchemaRepro_Relation"]
97
+ groups = definition["allOf"] || [definition]
98
+ groups.each do |group|
99
+ group["oneOf"].each do |branch|
100
+ Array(branch["allOf"]).each do |sub|
101
+ expect(sub).not_to have_key("required") if sub.key?("not")
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ it "accepts the document the default instance serializes to" do
108
+ inst = SchemaRepro::Relation.new(type: "includes")
109
+ doc = inst.to_hash
110
+ expect(doc).to eq("type" => "includes")
111
+ schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
112
+ definition = schema["$defs"]["SchemaRepro_Relation"]
113
+ groups = definition["allOf"] || [definition]
114
+ groups.each do |group|
115
+ satisfied = group["oneOf"].count do |branch|
116
+ Array(branch["allOf"]).all? do |sub|
117
+ if sub.key?("not")
118
+ Array(sub["not"]["anyOf"]).none? do |req|
119
+ doc.key?(req["required"].first)
120
+ end
121
+ else
122
+ doc.key?(sub["required"].first)
123
+ end
124
+ end
125
+ end
126
+ expect(satisfied).to eq(1)
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "#866: Schema.to_json / to_yaml delegation" do
132
+ it "accepts a positional options hash" do
133
+ expect { Lutaml::Model::Schema.to_json(SchemaRepro::Simple) }
134
+ .not_to raise_error
135
+ expect { Lutaml::Model::Schema.to_yaml(SchemaRepro::Simple) }
136
+ .not_to raise_error
137
+ end
138
+ end
139
+ end
@@ -161,19 +161,7 @@ RSpec.describe Lutaml::Model::Schema::JsonSchema do
161
161
  "type" => ["string", "null"],
162
162
  },
163
163
  },
164
- "oneOf" => [
165
- {
166
- "type" => "object",
167
- "properties" => {
168
- "email" => {
169
- "type" => ["string", "null"],
170
- },
171
- "phone" => {
172
- "type" => ["string", "null"],
173
- },
174
- },
175
- },
176
- ],
164
+
177
165
  },
178
166
  },
179
167
  }
@@ -136,17 +136,6 @@ RSpec.describe Lutaml::Model::Schema::YamlSchema do
136
136
  type:
137
137
  - string
138
138
  - 'null'
139
- oneOf:
140
- - type: object
141
- properties:
142
- email:
143
- type:
144
- - string
145
- - 'null'
146
- phone:
147
- type:
148
- - string
149
- - 'null'
150
139
  YAML
151
140
  end
152
141
 
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require_relative "../../../../lib/lutaml/model"
5
+ require "lutaml/xml/adapter/nokogiri_adapter"
6
+
7
+ # Runtimes without a source compiler (Opal) define the attribute
8
+ # accessors and the state-defaults seed from blocks instead of a String
9
+ # class_eval. These specs force that block form on MRI and check it
10
+ # behaves exactly like the compiled form.
11
+ module AttributeDefinitionBlockFormSpec
12
+ module_function
13
+
14
+ def define_model(klass)
15
+ klass.class_eval do
16
+ attribute :name, :string
17
+ attribute :count, :integer
18
+ attribute :tags, :string, collection: true
19
+ # "char" is an enum shorthand, so the second :char definition
20
+ # redefines the setter but keeps the first Attribute's accessor.
21
+ attribute :align, :string, values: %w[char left]
22
+ attribute :char, :integer
23
+ attribute :char, :string
24
+ # String names, mutated once defined: the accessors must keep
25
+ # reading the ivar named at definition time.
26
+ label = +"label"
27
+ notes = +"notes"
28
+ attribute label, :integer
29
+ attribute notes, :string, collection: true
30
+ label << "x"
31
+ notes << "x"
32
+
33
+ # A model may define its own #send; the setters must not dispatch
34
+ # through it.
35
+ define_method(:send) { |*| "domain-method" }
36
+
37
+ xml do
38
+ element "item"
39
+ ordered
40
+ map_attribute "name", to: :name
41
+ map_element "count", to: :count
42
+ map_element "tag", to: :tags
43
+ map_element "char", to: :char
44
+ end
45
+
46
+ key_value do
47
+ map "name", to: :name
48
+ map "count", to: :count
49
+ map "tags", to: :tags
50
+ map "char", to: :char
51
+ end
52
+ end
53
+ klass
54
+ end
55
+
56
+ # A model overriding the core methods the block form might dispatch
57
+ # through: the accessors must behave as the compiled ones do.
58
+ def override_model(klass)
59
+ klass.class_eval do
60
+ attribute :count, :integer
61
+ attribute :tags, :string, collection: true
62
+ %i[__send__ instance_variable_get instance_variable_set].each do |m|
63
+ define_method(m) { |*| :overridden }
64
+ end
65
+ end
66
+ m = klass.new
67
+ m.count = "5"
68
+ m.tags = %w[a]
69
+ m.tags("b")
70
+ # The collection getter itself reads through the overridden method,
71
+ # so check what was stored.
72
+ stored = ::Kernel.instance_method(:instance_variable_get)
73
+ [stored.bind_call(m, :@count), stored.bind_call(m, :@tags)]
74
+ end
75
+
76
+ # A model whose scope defines its own Array: appends must resolve the
77
+ # constant where the compiled getter does.
78
+ # Checked on the defining class and on a subclass that shadows Array,
79
+ # with the defining class overriding const_get.
80
+ def array_scope_model(klass)
81
+ klass.const_set(:Array, Class.new(::Array))
82
+ klass.class_eval { attribute :tags, :string, collection: true }
83
+ klass.define_singleton_method(:const_get) { |*| raise "const_get called" }
84
+ sub = Class.new(klass)
85
+ sub.const_set(:Array, ::Array)
86
+ [klass, sub].map do |k|
87
+ m = k.new
88
+ m.tags = %w[a]
89
+ m.tags("b")
90
+ ::Kernel.instance_method(:instance_variable_get).bind_call(m, :@tags)
91
+ end
92
+ end
93
+
94
+ # Raw-state readers after the state defaults compiled for a smaller
95
+ # attribute set: an attribute imported later, and one declared later,
96
+ # must both get a reader (#933).
97
+ def late_raw_readers(klass)
98
+ klass.class_eval { attribute :name, :string }
99
+ klass.new
100
+ imported = Class.new(Lutaml::Model::Serializable) do
101
+ attribute :br, :string
102
+ end
103
+ klass.import_model_attributes(imported)
104
+ klass.class_eval { attribute :late, :string, collection: true }
105
+ m = klass.new
106
+ m.br = "b"
107
+ readers = %i[name br late].map do |a|
108
+ raw = m.public_send(:"__lutaml_raw_#{a}")
109
+ [a, Lutaml::Model::Utils.uninitialized?(raw), raw.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) || raw]
110
+ end
111
+ readers << register_choice_raw_reader
112
+ end
113
+
114
+ # A choice import into a non-default register merges attribute records
115
+ # without define_attribute_methods: only the state-defaults
116
+ # compilation gives the imported name its raw reader.
117
+ def register_choice_raw_reader
118
+ register = Lutaml::Model::Register.new(:raw_reader_block_form_spec)
119
+ Lutaml::Model::GlobalRegister.register(register)
120
+ imported = Class.new(Lutaml::Model::Serializable) do
121
+ attribute :br, :string, collection: true
122
+ end
123
+ klass = Class.new(Lutaml::Model::Serializable) do
124
+ attribute :name, :string
125
+ choice { import_model_attributes(imported, :raw_reader_block_form_spec) }
126
+ end
127
+ klass.allocate_for_deserialization(:raw_reader_block_form_spec).__lutaml_raw_br
128
+ .equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
129
+ ensure
130
+ Lutaml::Model::GlobalRegister.unregister(:raw_reader_block_form_spec)
131
+ end
132
+
133
+ def build_model
134
+ define_model(Class.new(Lutaml::Model::Serializable))
135
+ end
136
+
137
+ def element_order(model)
138
+ model.element_order.map { |e| [e.type, e.name, e.text_content] }
139
+ end
140
+
141
+ def arity_error(model, name)
142
+ model.public_send(name, "1", "2")
143
+ nil
144
+ rescue ArgumentError => e
145
+ e.message
146
+ end
147
+
148
+ def observe(klass)
149
+ fresh = klass.new
150
+ set = klass.new
151
+ set.name = "a"
152
+ set.count = "5"
153
+ set.tags("x")
154
+ set.tags("y")
155
+ set.char = "7"
156
+ # A parsed instance with no tags still holds the lazy sentinel.
157
+ emptied = klass.from_xml('<item name="z"/>')
158
+ sentinel_before_nil = emptied.instance_variable_get(:@tags)
159
+ .equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
160
+ emptied.tags = nil
161
+ xml = set.to_xml
162
+ parsed = klass.from_xml(xml)
163
+
164
+ seed = klass.__send__(:compiled_state_defaults_name!, :default)
165
+ seeded = klass.new.__send__(seed)
166
+
167
+ {
168
+ seed_return: [seeded.class, seeded.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)],
169
+ fresh_scalar_uninitialized: Lutaml::Model::Utils.uninitialized?(
170
+ fresh.instance_variable_get(:@name),
171
+ ),
172
+ fresh_collection_sentinel: fresh.instance_variable_get(:@tags)
173
+ .equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION),
174
+ fresh_readers: [fresh.name, fresh.count, fresh.tags],
175
+ set_readers: [set.name, set.count, set.tags, set.char],
176
+ set_using_default: %i[name count tags].map { |a| set.using_default?(a) },
177
+ sentinel_before_nil: sentinel_before_nil,
178
+ nil_keeps_sentinel: emptied.instance_variable_get(:@tags)
179
+ .equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION),
180
+ getter_with_arg: klass.new.tap { |m| m.count("3") }.count,
181
+ assign_parsed_skips_cast: klass.new.tap { |m| m.__assign_parsed_count = "3" }.count,
182
+ built_order: element_order(klass.new do |m|
183
+ m.count = "1"
184
+ m.tags = %w[p q]
185
+ m.tags("r")
186
+ end),
187
+ string_names: klass.new.tap do |m|
188
+ m.label = "4"
189
+ m.notes("n")
190
+ end.then { |m| [m.label, m.notes, m.instance_variables.grep(/label|notes/).sort] },
191
+ two_args: %i[count tags label notes].map { |a| arity_error(klass.new, a) },
192
+ xml: xml,
193
+ json: set.to_json,
194
+ parsed: [parsed.name, parsed.count, parsed.tags, parsed.char],
195
+ parsed_equal: parsed == set,
196
+ }
197
+ end
198
+ end
199
+
200
+ RSpec.describe Lutaml::Model::Serialize::AttributeDefinition do
201
+ let(:compiled) { AttributeDefinitionBlockFormSpec.observe(AttributeDefinitionBlockFormSpec.build_model) }
202
+
203
+ it "compiles accessors from source by default on MRI" do
204
+ expect(described_class.source_compilation?).to be(true)
205
+ end
206
+
207
+ it "defines the block form without String class_eval and matches the compiled form" do
208
+ expected = compiled
209
+ allow(described_class).to receive(:source_compilation?).and_return(false)
210
+ klass = Class.new(Lutaml::Model::Serializable)
211
+ allow(klass).to receive(:class_eval).and_wrap_original do |original, *args, &block|
212
+ raise "String class_eval used: #{args.first}" if args.first.is_a?(String)
213
+
214
+ original.call(*args, &block)
215
+ end
216
+ AttributeDefinitionBlockFormSpec.define_model(klass)
217
+
218
+ expect(AttributeDefinitionBlockFormSpec.observe(klass)).to eq(expected)
219
+ end
220
+
221
+ it "does not dispatch through a model's own core methods" do
222
+ expected = AttributeDefinitionBlockFormSpec.override_model(Class.new(Lutaml::Model::Serializable))
223
+ allow(described_class).to receive(:source_compilation?).and_return(false)
224
+ actual = AttributeDefinitionBlockFormSpec.override_model(Class.new(Lutaml::Model::Serializable))
225
+
226
+ expect(actual).to eq(expected)
227
+ end
228
+
229
+ it "resolves Array in the model's scope when appending" do
230
+ expected = AttributeDefinitionBlockFormSpec.array_scope_model(Class.new(Lutaml::Model::Serializable))
231
+ allow(described_class).to receive(:source_compilation?).and_return(false)
232
+ actual = AttributeDefinitionBlockFormSpec.array_scope_model(Class.new(Lutaml::Model::Serializable))
233
+
234
+ expect(actual).to eq(expected)
235
+ end
236
+
237
+ it "defines raw-state readers for imported and late attributes as the compiled form does" do
238
+ expected = AttributeDefinitionBlockFormSpec.late_raw_readers(Class.new(Lutaml::Model::Serializable))
239
+ allow(described_class).to receive(:source_compilation?).and_return(false)
240
+ actual = AttributeDefinitionBlockFormSpec.late_raw_readers(Class.new(Lutaml::Model::Serializable))
241
+
242
+ expect(actual).to eq(expected)
243
+ end
244
+ end
@@ -86,14 +86,18 @@ RSpec.describe Lutaml::Model::Store do
86
86
  end
87
87
 
88
88
  it "does not mix indices across different classes" do
89
- model_class.new(id: "a")
90
- other_class.new(id: "a")
89
+ # Hold strong references: Store keeps WeakRefs, so unreferenced
90
+ # instances can be collected between registration and resolve,
91
+ # making this example fail only under in-suite GC pressure.
92
+ a = model_class.new(id: "a")
93
+ b = other_class.new(id: "a")
91
94
 
92
95
  result = described_class.resolve(model_class, :id, "a")
93
96
  expect(result).to be_a(model_class)
94
97
 
95
98
  result2 = described_class.resolve(other_class, :id, "a")
96
99
  expect(result2).to be_a(other_class)
100
+ expect([a, b]).to all(be_a(Lutaml::Model::Serializable))
97
101
  end
98
102
 
99
103
  it "registering class B does not iterate class A's indices" do
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Register ids and type-context ids are two namespaces that
6
+ # GlobalContext#resolve_type bridges implicitly: an id present only in the
7
+ # Register namespace (a flavor's `lutaml_default_register`) resolves to a
8
+ # nil type context, and every resolution shape must treat that as a
9
+ # legitimate "no namespace" input instead of crashing (metanorma-jis#531).
10
+ RSpec.describe "type resolution through a register id with no registered type context" do
11
+ let(:collection_class) do
12
+ Class.new(Lutaml::Model::Collection) do
13
+ instances :entries, :string
14
+ end
15
+ end
16
+
17
+ it "resolves collection instance types under a nil context" do
18
+ collection = collection_class.new(["a"], lutaml_register: :jis_document)
19
+
20
+ expect(collection.entries).to eq(["a"])
21
+ end
22
+
23
+ it "resolves built-in symbol names under a nil context" do
24
+ resolved = Lutaml::Model::GlobalContext.resolve_type(:string, :jis_document)
25
+
26
+ expect(resolved).to eq(Lutaml::Model::Type::String)
27
+ end
28
+
29
+ it "raises UnknownTypeError for unknown names under a nil context" do
30
+ expect do
31
+ Lutaml::Model::GlobalContext.resolve_type(:no_such_type, :jis_document)
32
+ end.to raise_error(Lutaml::Model::UnknownTypeError)
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module WriterAssigned
6
+ class Plain < Lutaml::Model::Serializable
7
+ attribute :code, :string
8
+ attribute :text, :string
9
+
10
+ xml do
11
+ element "probe"
12
+ map_element "code", to: :code
13
+ map_element "text", to: :text
14
+ end
15
+
16
+ def code=(val)
17
+ super
18
+ self.text = "derived-#{val}" if val
19
+ end
20
+ end
21
+
22
+ class Defaulted < Lutaml::Model::Serializable
23
+ attribute :code, :string
24
+ attribute :text, :string, default: -> { "" }
25
+
26
+ xml do
27
+ element "probe"
28
+ map_element "code", to: :code
29
+ map_element "text", to: :text
30
+ end
31
+
32
+ def code=(val)
33
+ super
34
+ self.text = "derived-#{val}" if val
35
+ end
36
+ end
37
+ end
38
+
39
+ # lutaml-model#922: a custom writer that assigns a *different* attribute
40
+ # mid-parse (relaton's Relaton::Bib::ICS populates <text> from the Isoics
41
+ # dataset inside code=) must survive the deserializer's unmatched-rule
42
+ # handling: the default seeding in Initialization#instantiate and the
43
+ # unmatched-rule replay must never overwrite a value a writer assigned.
44
+ RSpec.describe "attributes assigned inside another attribute's writer" do
45
+ %i[Plain Defaulted].each do |klass_name|
46
+ describe "with #{klass_name.to_s.downcase} text attribute" do
47
+ let(:klass) { WriterAssigned.const_get(klass_name) }
48
+
49
+ it "keeps the derived value through from_xml" do
50
+ instance = klass.from_xml("<probe><code>67.060</code></probe>")
51
+
52
+ expect(instance.text).to eq("derived-67.060")
53
+ end
54
+
55
+ it "renders the derived value on to_xml" do
56
+ instance = klass.from_xml("<probe><code>67.060</code></probe>")
57
+
58
+ expect(instance.to_xml).to include("<text>derived-67.060</text>")
59
+ end
60
+ end
61
+ end
62
+ end