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.
- checksums.yaml +4 -4
- data/.github/scripts/test_installed_gem.rb +58 -0
- data/.github/workflows/downstream-performance.yml +28 -3
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +9 -0
- data/.github/workflows/rake.yml +6 -41
- data/.github/workflows/release.yml +41 -0
- data/Gemfile +17 -3
- data/bench/bench_alloc_audit.rb +56 -0
- data/bench/bench_common.rb +11 -0
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/gem-integration.adoc +85 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +8 -1
- data/docs/_guides/native-engines.adoc +23 -5
- data/docs/_guides/schema-generation.adoc +25 -0
- data/docs/_guides/type-registers-contexts.adoc +126 -0
- data/lib/compat/opal/lutaml_model_boot.rb +7 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/key_value/data_model/element.rb +5 -1
- data/lib/lutaml/key_value/mapping_rule.rb +7 -0
- data/lib/lutaml/key_value/transform.rb +29 -24
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
- data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
- data/lib/lutaml/key_value/transformation.rb +64 -116
- data/lib/lutaml/model/adapter_resolver.rb +7 -3
- data/lib/lutaml/model/attribute.rb +74 -15
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
- data/lib/lutaml/model/render_policy.rb +26 -8
- data/lib/lutaml/model/schema/base_schema.rb +15 -0
- data/lib/lutaml/model/schema/generator/definition.rb +52 -8
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/serializable.rb +33 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
- data/lib/lutaml/model/serialize/initialization.rb +54 -7
- data/lib/lutaml/model/serialize/model_import.rb +60 -0
- data/lib/lutaml/model/serialize.rb +14 -6
- data/lib/lutaml/model/type/time.rb +6 -0
- data/lib/lutaml/model/type/value.rb +9 -7
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/utils.rb +18 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/format.rb +18 -16
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
- data/lib/lutaml/xml/leptris.rb +46 -0
- data/lib/lutaml/xml/mapping.rb +71 -18
- data/lib/lutaml/xml/model_transform.rb +231 -8
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_collector.rb +17 -1
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +7 -19
- data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lib/lutaml/xml/type/serializers.rb +8 -1
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/lutaml-model.gemspec +7 -4
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
- data/spec/lutaml/key_value/transformation_spec.rb +87 -21
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- data/spec/lutaml/model/desc_spec.rb +50 -0
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
- data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
- data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
- data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- data/spec/person_spec.rb +8 -0
- metadata +48 -17
- data/lib/lutaml/xml/plan_compiler.rb +0 -332
- data/lib/lutaml/xml/plan_hydrator.rb +0 -367
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -252
- 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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|