lutaml-model 0.8.60 → 0.8.61
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 +7 -6
- data/.github/workflows/rake.yml +5 -2
- data/.github/workflows/release.yml +4 -1
- data/lib/lutaml/model/adapter_resolver.rb +3 -2
- data/lib/lutaml/model/schema/generator/definition.rb +34 -10
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +26 -15
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/plan_hydrator.rb +9 -1
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +104 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +8 -1
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +7 -1
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +49 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60d029c4eab58efd3741553bd22cc4f689164143f6f1fed87d4c6956cf341cfd
|
|
4
|
+
data.tar.gz: aefb886903788a07439e416b2a92ec7a5fe295f231aa6c137b606e6e9621b048
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82a30dcc2a930524ebb5d3d8cdcd9c9eb712f5425105b61101263501af2b66e93e5395e2455925e57d87e9a10689b8dbd1b816e9e0f302e4472ba68f084ed365
|
|
7
|
+
data.tar.gz: 76625bdefe2dde62258b0ec3da4ca257679fa16a9b669ca0a4453108097e9d670109e9b4f77f5b67c0cf76f95e18164edd2b477a885bb3ed5cc73c928174d6a8
|
|
@@ -18,14 +18,15 @@ jobs:
|
|
|
18
18
|
benchmark:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
timeout-minutes: 60
|
|
21
|
-
# Serializes the corpus legs: concurrent legs on shared runners
|
|
22
|
-
# produce contention measurements that misfire the absolute gates
|
|
23
|
-
# (lutaml-model#834: niso 0.921s vs a 0.25s main-passing gate).
|
|
24
|
-
concurrency:
|
|
25
|
-
group: downstream-benchmark
|
|
26
|
-
cancel-in-progress: false
|
|
27
21
|
strategy:
|
|
28
22
|
fail-fast: false
|
|
23
|
+
# Serializes the corpus legs: concurrent legs on shared runners
|
|
24
|
+
# produce contention measurements that misfire the absolute gates
|
|
25
|
+
# (lutaml-model#834: niso 0.921s vs a 0.25s main-passing gate).
|
|
26
|
+
# max-parallel, NOT a job-level concurrency group: with a
|
|
27
|
+
# matrix, every leg lands in the same group and GitHub cancels
|
|
28
|
+
# four of the five at start.
|
|
29
|
+
max-parallel: 1
|
|
29
30
|
matrix:
|
|
30
31
|
include:
|
|
31
32
|
- downstream: xmi
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -23,10 +23,13 @@ jobs:
|
|
|
23
23
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
24
24
|
ruby: ["3.3", "3.4", "4.0"]
|
|
25
25
|
experimental: [false]
|
|
26
|
-
|
|
26
|
+
# ruby head runs on ubuntu/macos only: windows-head cannot build
|
|
27
|
+
# the released ruby-ll C extension (and the vendored forks exist
|
|
28
|
+
# for Opal), so that cell was permanently red with zero signal
|
|
29
|
+
# (#832 follow-up).
|
|
30
|
+
exclude:
|
|
27
31
|
- os: windows-latest
|
|
28
32
|
ruby: head
|
|
29
|
-
experimental: true
|
|
30
33
|
steps:
|
|
31
34
|
- uses: actions/checkout@v6
|
|
32
35
|
with:
|
|
@@ -49,7 +49,10 @@ jobs:
|
|
|
49
49
|
ruby-version: "3.4"
|
|
50
50
|
- name: Resolve the released version
|
|
51
51
|
id: version
|
|
52
|
-
|
|
52
|
+
# From the version file, not git describe: the default checkout
|
|
53
|
+
# is shallow with no tags, so git describe failed and the empty
|
|
54
|
+
# version produced `gem install "lutaml-model:"`.
|
|
55
|
+
run: echo "version=$(ruby -e 'print File.read(%q{lib/lutaml/model/version.rb})[/VERSION\s*=\s*"([^"]+)"/, 1]')" >> "$GITHUB_OUTPUT"
|
|
53
56
|
- name: Install and smoke the released gem
|
|
54
57
|
run: |
|
|
55
58
|
gem install "lutaml-model:${{ steps.version.outputs.version }}" --no-document
|
|
@@ -250,8 +250,9 @@ module Lutaml
|
|
|
250
250
|
adapter, type)
|
|
251
251
|
end
|
|
252
252
|
require adapter_path
|
|
253
|
-
rescue LoadError
|
|
254
|
-
|
|
253
|
+
rescue LoadError => e
|
|
254
|
+
# Chain the cause so a missing engine gem is named, not masked.
|
|
255
|
+
raise UnknownAdapterTypeError.new(adapter, type), cause: e
|
|
255
256
|
end
|
|
256
257
|
|
|
257
258
|
# Load the Moxml adapter for XML and similar formats.
|
|
@@ -23,9 +23,20 @@ module Lutaml
|
|
|
23
23
|
},
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# Choice validation: each choice is its own group. Independent
|
|
27
|
+
# choices combine conjunctively (allOf), so a document may
|
|
28
|
+
# satisfy every group at once; a single choice renders its
|
|
29
|
+
# group directly. Nested choices render as a nested group
|
|
30
|
+
# schema, which a document satisfies through its own
|
|
31
|
+
# oneOf/anyOf (#864, #865).
|
|
27
32
|
if type.choice_attributes.any?
|
|
28
|
-
|
|
33
|
+
if type.choice_attributes.one?
|
|
34
|
+
@schema[name].merge!(choice_schema(type.choice_attributes.first))
|
|
35
|
+
else
|
|
36
|
+
@schema[name]["allOf"] = type.choice_attributes.map do |choice|
|
|
37
|
+
choice_schema(choice)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
@schema
|
|
@@ -33,14 +44,27 @@ module Lutaml
|
|
|
33
44
|
|
|
34
45
|
private
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
# min == max == 1 makes the members mutually exclusive (oneOf);
|
|
48
|
+
# any other range admits several members together (anyOf with
|
|
49
|
+
# per-member requirement).
|
|
50
|
+
def choice_schema(choice)
|
|
51
|
+
key = choice.min == 1 && choice.max == 1 ? "oneOf" : "anyOf"
|
|
52
|
+
{ key => choice_branches(choice) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def choice_branches(choice)
|
|
56
|
+
choice.attributes.map do |member|
|
|
57
|
+
if member.is_a?(Lutaml::Model::Choice)
|
|
58
|
+
choice_schema(member)
|
|
59
|
+
else
|
|
60
|
+
{
|
|
61
|
+
"type" => "object",
|
|
62
|
+
"properties" => PropertiesCollection.from_attributes(
|
|
63
|
+
[member], extract_register_from(type)
|
|
64
|
+
).to_schema,
|
|
65
|
+
"required" => [member.name.to_s],
|
|
66
|
+
}
|
|
67
|
+
end
|
|
44
68
|
end
|
|
45
69
|
end
|
|
46
70
|
|
|
@@ -9,20 +9,18 @@ module Lutaml
|
|
|
9
9
|
include SharedMethods
|
|
10
10
|
|
|
11
11
|
def from_class(klass)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process_attributes(collection, klass)
|
|
16
|
-
end
|
|
12
|
+
collection = new
|
|
13
|
+
register_definition(collection, klass)
|
|
14
|
+
collection
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def process_attributes(collection, klass)
|
|
17
|
+
def process_attributes(collection, klass, seen = {})
|
|
20
18
|
register = extract_register_from(klass)
|
|
21
19
|
klass.attributes.each_value do |attribute|
|
|
22
20
|
if attribute.union?
|
|
23
|
-
process_union_members(collection, attribute)
|
|
21
|
+
process_union_members(collection, attribute, seen)
|
|
24
22
|
elsif attribute.serializable?(register)
|
|
25
|
-
process_attribute(collection, attribute, register)
|
|
23
|
+
process_attribute(collection, attribute, register, seen)
|
|
26
24
|
end
|
|
27
25
|
end
|
|
28
26
|
end
|
|
@@ -30,31 +28,44 @@ module Lutaml
|
|
|
30
28
|
# A union attribute's type is Type::Union, not a model, so collect
|
|
31
29
|
# definitions for each of its Serializable members directly (a
|
|
32
30
|
# union schema emits a $ref per model member).
|
|
33
|
-
def process_union_members(collection, attribute)
|
|
31
|
+
def process_union_members(collection, attribute, seen = {})
|
|
34
32
|
attribute.union_member_types.each do |member|
|
|
35
33
|
next unless member.is_a?(::Class) &&
|
|
36
34
|
member.include?(Lutaml::Model::Serialize)
|
|
37
35
|
|
|
38
|
-
collection
|
|
36
|
+
register_definition(collection, member, seen)
|
|
39
37
|
end
|
|
40
38
|
end
|
|
41
39
|
|
|
42
|
-
def process_attribute(collection, attribute, register)
|
|
40
|
+
def process_attribute(collection, attribute, register, seen = {})
|
|
43
41
|
attr_type = Lutaml::Model::GlobalContext.resolve_type(
|
|
44
42
|
attribute.type, register
|
|
45
43
|
)
|
|
46
|
-
collection
|
|
44
|
+
register_definition(collection, attr_type, seen)
|
|
47
45
|
|
|
48
|
-
process_polymorphic_types(collection, attribute)
|
|
46
|
+
process_polymorphic_types(collection, attribute, seen)
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
def process_polymorphic_types(collection, attribute)
|
|
49
|
+
def process_polymorphic_types(collection, attribute, seen = {})
|
|
52
50
|
return unless attribute.options&.[](:polymorphic)
|
|
53
51
|
|
|
54
52
|
attribute.options[:polymorphic].each do |child|
|
|
55
|
-
collection
|
|
53
|
+
register_definition(collection, child, seen)
|
|
56
54
|
end
|
|
57
55
|
end
|
|
56
|
+
|
|
57
|
+
# Recursive models (Node#child → Node, or longer include
|
|
58
|
+
# cycles) are valid; the seen-set registers each class once
|
|
59
|
+
# and lets property references point at the shared $defs
|
|
60
|
+
# entry instead of recursing (#863).
|
|
61
|
+
def register_definition(collection, klass, seen = {})
|
|
62
|
+
return collection if seen[klass]
|
|
63
|
+
|
|
64
|
+
seen[klass] = true
|
|
65
|
+
collection << Definition.new(klass)
|
|
66
|
+
process_attributes(collection, klass, seen)
|
|
67
|
+
collection
|
|
68
|
+
end
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
attr_reader :definitions
|
data/lib/lutaml/model/schema.rb
CHANGED
|
@@ -52,11 +52,13 @@ module Lutaml
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def self.to_json(klass, options = {})
|
|
55
|
-
|
|
55
|
+
# The generator signatures are keyword-only; splat so callers may
|
|
56
|
+
# pass an options hash positionally (#866).
|
|
57
|
+
JsonSchema.generate(klass, **options)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def self.to_yaml(klass, options = {})
|
|
59
|
-
YamlSchema.generate(klass, options)
|
|
61
|
+
YamlSchema.generate(klass, **options)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# XML-specific methods (to_xsd, to_relaxng, from_xml) are registered
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -19,9 +19,12 @@ module Lutaml
|
|
|
19
19
|
File.join(File.dirname(__FILE__), "adapter", type)
|
|
20
20
|
end
|
|
21
21
|
require adapter_path
|
|
22
|
-
rescue LoadError
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
# Chain the cause: a known adapter whose underlying engine gem is
|
|
24
|
+
# missing (e.g. no nokogiri in the Gemfile) must not present as a
|
|
25
|
+
# bare unknown-type error — the LoadError names the real gap.
|
|
23
26
|
raise Lutaml::Model::UnknownAdapterTypeError.new("xml", type),
|
|
24
|
-
cause:
|
|
27
|
+
cause: e
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# Load the Moxml adapter for XML
|
|
@@ -200,7 +200,15 @@ module Lutaml
|
|
|
200
200
|
case kind
|
|
201
201
|
when :content_deferred
|
|
202
202
|
runs = content_runs(value)
|
|
203
|
-
|
|
203
|
+
if runs.empty?
|
|
204
|
+
# The interpretive pipeline marks every applied rule's
|
|
205
|
+
# attribute set (model_transform apply). Skipping the
|
|
206
|
+
# mark leaves using_default? true, so the serializer's
|
|
207
|
+
# render gate suppresses a mapped reader that derives
|
|
208
|
+
# content from other attributes (#856).
|
|
209
|
+
instance.value_set_for(attr.name)
|
|
210
|
+
next
|
|
211
|
+
end
|
|
204
212
|
|
|
205
213
|
# Non-collection content attrs hold the joined text
|
|
206
214
|
# (the interpretive path assigns element text, the runs
|
|
@@ -0,0 +1,104 @@
|
|
|
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 "renders the nested group as its own oneOf" do
|
|
74
|
+
schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Outer))
|
|
75
|
+
one_of = schema["$defs"]["SchemaRepro_Outer"]["oneOf"]
|
|
76
|
+
at_branch = one_of.find { |b| b["required"] == %w[at] }
|
|
77
|
+
inner = one_of.find { |b| b["oneOf"] }
|
|
78
|
+
expect(at_branch).to be_truthy
|
|
79
|
+
expect(inner["oneOf"].map { |b| b["required"] })
|
|
80
|
+
.to contain_exactly(%w[from], %w[to])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#865: independent choices combine conjunctively" do
|
|
85
|
+
it "renders an allOf of oneOf groups with discriminating required keys" do
|
|
86
|
+
schema = JSON.parse(Lutaml::Model::Schema.to_json(SchemaRepro::Relation))
|
|
87
|
+
all_of = schema["$defs"]["SchemaRepro_Relation"]["allOf"]
|
|
88
|
+
expect(all_of.length).to eq(2)
|
|
89
|
+
all_of.each do |group|
|
|
90
|
+
expect(group).to have_key("oneOf")
|
|
91
|
+
group["oneOf"].each { |branch| expect(branch["required"]).to be_truthy }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#866: Schema.to_json / to_yaml delegation" do
|
|
97
|
+
it "accepts a positional options hash" do
|
|
98
|
+
expect { Lutaml::Model::Schema.to_json(SchemaRepro::Simple) }
|
|
99
|
+
.not_to raise_error
|
|
100
|
+
expect { Lutaml::Model::Schema.to_yaml(SchemaRepro::Simple) }
|
|
101
|
+
.not_to raise_error
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -161,17 +161,24 @@ RSpec.describe Lutaml::Model::Schema::JsonSchema do
|
|
|
161
161
|
"type" => ["string", "null"],
|
|
162
162
|
},
|
|
163
163
|
},
|
|
164
|
-
"
|
|
164
|
+
"anyOf" => [
|
|
165
165
|
{
|
|
166
166
|
"type" => "object",
|
|
167
167
|
"properties" => {
|
|
168
168
|
"email" => {
|
|
169
169
|
"type" => ["string", "null"],
|
|
170
170
|
},
|
|
171
|
+
},
|
|
172
|
+
"required" => ["email"],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"type" => "object",
|
|
176
|
+
"properties" => {
|
|
171
177
|
"phone" => {
|
|
172
178
|
"type" => ["string", "null"],
|
|
173
179
|
},
|
|
174
180
|
},
|
|
181
|
+
"required" => ["phone"],
|
|
175
182
|
},
|
|
176
183
|
],
|
|
177
184
|
},
|
|
@@ -136,17 +136,23 @@ RSpec.describe Lutaml::Model::Schema::YamlSchema do
|
|
|
136
136
|
type:
|
|
137
137
|
- string
|
|
138
138
|
- 'null'
|
|
139
|
-
|
|
139
|
+
anyOf:
|
|
140
140
|
- type: object
|
|
141
141
|
properties:
|
|
142
142
|
email:
|
|
143
143
|
type:
|
|
144
144
|
- string
|
|
145
145
|
- 'null'
|
|
146
|
+
required:
|
|
147
|
+
- email
|
|
148
|
+
- type: object
|
|
149
|
+
properties:
|
|
146
150
|
phone:
|
|
147
151
|
type:
|
|
148
152
|
- string
|
|
149
153
|
- 'null'
|
|
154
|
+
required:
|
|
155
|
+
- phone
|
|
150
156
|
YAML
|
|
151
157
|
end
|
|
152
158
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Xml::AdapterLoader do
|
|
6
|
+
it "chains the LoadError cause instead of masking it" do
|
|
7
|
+
expect do
|
|
8
|
+
described_class.load_adapter_file("xml", "bogus_adapter")
|
|
9
|
+
end.to raise_error(Lutaml::Model::UnknownAdapterTypeError) do |error|
|
|
10
|
+
expect(error.cause).to be_a(LoadError)
|
|
11
|
+
expect(error.cause.message).to include("bogus_adapter")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#858: leptris hydrates :string values as ASCII-8BIT
|
|
6
|
+
# (FFI's :string return tag) while nokogiri returns UTF-8. Values are
|
|
7
|
+
# character data; both adapters must agree on the encoding.
|
|
8
|
+
RSpec.describe "leptris string encoding parity with nokogiri",
|
|
9
|
+
if: defined?(Leptris::XML::Document) do
|
|
10
|
+
let(:xml) do
|
|
11
|
+
<<~XML
|
|
12
|
+
<Relationship Id="rId5" Target="http://example.com/x"/>
|
|
13
|
+
XML
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:klass) do
|
|
17
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
18
|
+
attribute :id, :string
|
|
19
|
+
attribute :target, :string
|
|
20
|
+
|
|
21
|
+
xml do
|
|
22
|
+
element "Relationship"
|
|
23
|
+
map_attribute "Id", to: :id
|
|
24
|
+
map_attribute "Target", to: :target
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
stub_const("LeptrisEncoding::Rel", klass)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "hydrates attribute values as UTF-8" do
|
|
34
|
+
Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
|
|
35
|
+
parsed = klass.from_xml(xml)
|
|
36
|
+
expect(parsed.id.encoding).to eq(Encoding::UTF_8)
|
|
37
|
+
expect(parsed.target.encoding).to eq(Encoding::UTF_8)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) do
|
|
41
|
+
parsed = klass.from_xml(xml)
|
|
42
|
+
expect(parsed.id.encoding).to eq(Encoding::UTF_8)
|
|
43
|
+
expect(parsed.target.encoding).to eq(Encoding::UTF_8)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "hydrates element text as UTF-8" do
|
|
48
|
+
doc = "<Note>café – naïve</Note>"
|
|
49
|
+
text_class = Class.new(Lutaml::Model::Serializable) do
|
|
50
|
+
attribute :text, :string
|
|
51
|
+
|
|
52
|
+
xml do
|
|
53
|
+
element "Note"
|
|
54
|
+
map_content to: :text
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
stub_const("LeptrisEncoding::Note", text_class)
|
|
58
|
+
|
|
59
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) do
|
|
60
|
+
parsed = text_class.from_xml(doc)
|
|
61
|
+
expect(parsed.text.encoding).to eq(Encoding::UTF_8)
|
|
62
|
+
expect(parsed.text).to eq("café – naïve")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -740,4 +740,53 @@ RSpec.describe "XML plan fast path" do
|
|
|
740
740
|
%(<b xmlns="http://example.com/w" val="false"/>),
|
|
741
741
|
).value).to be(false)
|
|
742
742
|
end
|
|
743
|
+
|
|
744
|
+
# lutaml-model#856: a deferred content row with no captured runs
|
|
745
|
+
# must still mark its attribute set — the interpretive pipeline
|
|
746
|
+
# marks every applied rule (model_transform apply). Without the
|
|
747
|
+
# mark, the serializer's default-suppression drops a mapped reader
|
|
748
|
+
# that derives content from other attributes.
|
|
749
|
+
it "renders derived content readers when no text was captured" do
|
|
750
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
751
|
+
attribute :content, :string
|
|
752
|
+
attribute :revision_date, :string
|
|
753
|
+
attribute :draft, :string
|
|
754
|
+
|
|
755
|
+
xml do
|
|
756
|
+
element "version"
|
|
757
|
+
map_content to: :content
|
|
758
|
+
map_element "revision-date", to: :revision_date
|
|
759
|
+
map_element "draft", to: :draft
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
define_method(:content) do
|
|
763
|
+
return @content if @content && !@content.empty?
|
|
764
|
+
|
|
765
|
+
parts = [@draft, @revision_date].compact
|
|
766
|
+
case parts.size
|
|
767
|
+
when 2 then "#{@draft} (#{@revision_date})"
|
|
768
|
+
when 1 then parts.first
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
stub_const("PlanFastPath::DerivedContent856", klass)
|
|
773
|
+
|
|
774
|
+
parsed = nil
|
|
775
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) do
|
|
776
|
+
parsed = klass.from_xml(
|
|
777
|
+
"<version><revision-date>1994-01-01</revision-date><draft>PD</draft></version>",
|
|
778
|
+
)
|
|
779
|
+
expect(parsed.using_default?(:content)).to be(false)
|
|
780
|
+
expect(parsed.to_xml).to eq(
|
|
781
|
+
"<version><revision-date>1994-01-01</revision-date>" \
|
|
782
|
+
"<draft>PD</draft>PD (1994-01-01)</version>",
|
|
783
|
+
)
|
|
784
|
+
end
|
|
785
|
+
Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
|
|
786
|
+
expect(parsed.to_xml).to eq(
|
|
787
|
+
"<version><revision-date>1994-01-01</revision-date>" \
|
|
788
|
+
"<draft>PD</draft>PD (1994-01-01)</version>",
|
|
789
|
+
)
|
|
790
|
+
end
|
|
791
|
+
end
|
|
743
792
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.61
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -1789,6 +1789,7 @@ files:
|
|
|
1789
1789
|
- spec/lutaml/model/root_mappings_spec.rb
|
|
1790
1790
|
- spec/lutaml/model/rule_value_extractor_spec.rb
|
|
1791
1791
|
- spec/lutaml/model/runtime_compatibility_spec.rb
|
|
1792
|
+
- spec/lutaml/model/schema/generation_regressions_spec.rb
|
|
1792
1793
|
- spec/lutaml/model/schema/generator/definitions_collection_spec.rb
|
|
1793
1794
|
- spec/lutaml/model/schema/json_schema_spec.rb
|
|
1794
1795
|
- spec/lutaml/model/schema/json_schema_to_models_spec.rb
|
|
@@ -1903,6 +1904,7 @@ files:
|
|
|
1903
1904
|
- spec/lutaml/xml/adapter/ox_adapter_spec.rb
|
|
1904
1905
|
- spec/lutaml/xml/adapter/rexml_adapter_spec.rb
|
|
1905
1906
|
- spec/lutaml/xml/adapter/xml_namespace_spec.rb
|
|
1907
|
+
- spec/lutaml/xml/adapter_loader_spec.rb
|
|
1906
1908
|
- spec/lutaml/xml/attribute_ambiguity_surface_spec.rb
|
|
1907
1909
|
- spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb
|
|
1908
1910
|
- spec/lutaml/xml/attribute_namespace_disjointness_spec.rb
|
|
@@ -1932,6 +1934,7 @@ files:
|
|
|
1932
1934
|
- spec/lutaml/xml/general_namespace_spec.rb
|
|
1933
1935
|
- spec/lutaml/xml/html_entities_spec.rb
|
|
1934
1936
|
- spec/lutaml/xml/indent_spec.rb
|
|
1937
|
+
- spec/lutaml/xml/leptris_string_encoding_spec.rb
|
|
1935
1938
|
- spec/lutaml/xml/line_ending_spec.rb
|
|
1936
1939
|
- spec/lutaml/xml/mapping_finalization_guard_spec.rb
|
|
1937
1940
|
- spec/lutaml/xml/mapping_rule_spec.rb
|