lutaml-model 0.8.32 → 0.8.34
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/.rubocop.yml +5 -0
- data/Gemfile +4 -0
- data/README.adoc +39 -0
- data/docs/conversion_caching.adoc +143 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/model/attribute.rb +89 -0
- data/lib/lutaml/model/config.rb +13 -0
- data/lib/lutaml/model/configuration.rb +23 -0
- data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
- data/lib/lutaml/model/error/length_error.rb +19 -0
- data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/restriction_error.rb +8 -0
- data/lib/lutaml/model/error/total_digits_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type.rb +2 -0
- data/lib/lutaml/model/restriction_validation.rb +437 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
- data/lib/lutaml/model/schema/templates.rb +2 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
- data/lib/lutaml/model/serialize/builder.rb +4 -5
- data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
- data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
- data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
- data/lib/lutaml/model/serialize/initialization.rb +4 -2
- data/lib/lutaml/model/serialize.rb +61 -0
- data/lib/lutaml/model/services/type/validator.rb +23 -0
- data/lib/lutaml/model/type/string.rb +28 -3
- data/lib/lutaml/model/type/value.rb +231 -0
- data/lib/lutaml/model/type.rb +2 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +16 -0
- data/lib/lutaml/xml/model_transform.rb +47 -6
- data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
- data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
- data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/xml/transformation.rb +2 -0
- data/lib/lutaml/xml/transformation_support.rb +1 -0
- data/spec/fixtures/xml/restriction_facets.xsd +46 -0
- data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
- data/spec/lutaml/model/attribute_spec.rb +7 -2
- data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
- data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
- data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
- data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
- data/spec/lutaml/model/phantom_value_spec.rb +799 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
- data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
- data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
- data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
- data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
- data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
- data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
- metadata +27 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
require "lutaml/model/schema"
|
|
3
|
-
require "
|
|
3
|
+
require "bigdecimal"
|
|
4
4
|
|
|
5
5
|
module SchemaGeneration
|
|
6
6
|
class Glaze < Lutaml::Model::Serializable
|
|
@@ -14,6 +14,136 @@ module SchemaGeneration
|
|
|
14
14
|
attribute :glaze, Glaze
|
|
15
15
|
attribute :materials, Lutaml::Model::Type::String, collection: true
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
class Percent < Lutaml::Model::Type::Integer
|
|
19
|
+
inclusive min: 0, max: 100
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Money < Lutaml::Model::Type::Decimal
|
|
23
|
+
exclusive min: BigDecimal("1.5")
|
|
24
|
+
total_digits 5
|
|
25
|
+
fraction_digits 2
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Code < Lutaml::Model::Type::String
|
|
29
|
+
length min: 2, max: 8
|
|
30
|
+
enumeration "AB", "CD"
|
|
31
|
+
white_space :collapse
|
|
32
|
+
pattern(/[A-Z]+/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Tag < Lutaml::Model::Type::String
|
|
36
|
+
length min: 1, max: 3
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Two accumulated patterns are conjunctive (a value must match BOTH). XSD has
|
|
40
|
+
# no way to express that in one restriction (sibling <xs:pattern> is OR), so
|
|
41
|
+
# exporting it must fail fast rather than silently emit a weaker OR schema.
|
|
42
|
+
class MultiPattern < Lutaml::Model::Type::String
|
|
43
|
+
pattern(/\A\d+\z/)
|
|
44
|
+
pattern(/\A.{4}\z/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class MultiPatternHolder < Lutaml::Model::Serializable
|
|
48
|
+
attribute :field, MultiPattern
|
|
49
|
+
xml do
|
|
50
|
+
root "MultiPatternHolder"
|
|
51
|
+
map_element "field", to: :field
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# XSD patterns are implicitly whole-string anchored, so the Ruby \A...\z
|
|
56
|
+
# anchors must be stripped on export or Nokogiri rejects the pattern.
|
|
57
|
+
class Anchored < Lutaml::Model::Type::String
|
|
58
|
+
pattern(/\A[A-Z]{3}\z/)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class AnchoredHolder < Lutaml::Model::Serializable
|
|
62
|
+
attribute :field, Anchored
|
|
63
|
+
xml do
|
|
64
|
+
root "AnchoredHolder"
|
|
65
|
+
map_element "field", to: :field
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A lookahead is not expressible in XSD's regexp subset; export must raise
|
|
70
|
+
# rather than emit invalid XSD.
|
|
71
|
+
class Lookahead < Lutaml::Model::Type::String
|
|
72
|
+
pattern(/(?=.*[0-9])[a-z0-9]+/)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class LookaheadHolder < Lutaml::Model::Serializable
|
|
76
|
+
attribute :field, Lookahead
|
|
77
|
+
xml do
|
|
78
|
+
root "LookaheadHolder"
|
|
79
|
+
map_element "field", to: :field
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A case-insensitive flag cannot be expressed in XSD; export must raise
|
|
84
|
+
# rather than silently emit a case-sensitive pattern.
|
|
85
|
+
class FlaggedPattern < Lutaml::Model::Type::String
|
|
86
|
+
pattern(/[a-z]+/i)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class FlaggedHolder < Lutaml::Model::Serializable
|
|
90
|
+
attribute :field, FlaggedPattern
|
|
91
|
+
xml do
|
|
92
|
+
root "FlaggedHolder"
|
|
93
|
+
map_element "field", to: :field
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Layer-1 attribute facets (no Layer-2 constrained type) must still emit an
|
|
98
|
+
# inline xs:restriction so the schema matches what the model enforces.
|
|
99
|
+
class Layered < Lutaml::Model::Serializable
|
|
100
|
+
attribute :age, :integer, min: 0, max: 120
|
|
101
|
+
attribute :code, :string, min_length: 2, max_length: 8
|
|
102
|
+
xml do
|
|
103
|
+
root "Layered"
|
|
104
|
+
map_element "age", to: :age
|
|
105
|
+
map_element "code", to: :code
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The pre-#191 values:/pattern: options are enforced at runtime and must also
|
|
110
|
+
# reach the exported schema.
|
|
111
|
+
class LegacyOptions < Lutaml::Model::Serializable
|
|
112
|
+
attribute :state, :string, values: %w[on off]
|
|
113
|
+
attribute :ref, :string, pattern: /[A-Z]{2}/
|
|
114
|
+
xml do
|
|
115
|
+
root "LegacyOptions"
|
|
116
|
+
map_element "state", to: :state
|
|
117
|
+
map_element "ref", to: :ref
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A Layer-1 option (max: 50) that tightens a Layer-2 type (Percent, max 100):
|
|
122
|
+
# the merge is consistent, so export must emit the tighter bound, not raise.
|
|
123
|
+
class MergedLayers < Lutaml::Model::Serializable
|
|
124
|
+
attribute :n, Percent, max: 50
|
|
125
|
+
xml do
|
|
126
|
+
root "MergedLayers"
|
|
127
|
+
map_element "n", to: :n
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Record < Lutaml::Model::Serializable
|
|
132
|
+
attribute :code, Code
|
|
133
|
+
attribute :percent, Percent
|
|
134
|
+
attribute :money, Money
|
|
135
|
+
attribute :tags, Tag, collection: true
|
|
136
|
+
attribute :note, Lutaml::Model::Type::String
|
|
137
|
+
|
|
138
|
+
xml do
|
|
139
|
+
root "Record"
|
|
140
|
+
map_attribute "code", to: :code
|
|
141
|
+
map_element "percent", to: :percent
|
|
142
|
+
map_element "money", to: :money
|
|
143
|
+
map_element "tags", to: :tags
|
|
144
|
+
map_element "note", to: :note
|
|
145
|
+
end
|
|
146
|
+
end
|
|
17
147
|
end
|
|
18
148
|
|
|
19
149
|
RSpec.describe Lutaml::Xml::Schema::XsdSchema do
|
|
@@ -52,764 +182,279 @@ RSpec.describe Lutaml::Xml::Schema::XsdSchema do
|
|
|
52
182
|
|
|
53
183
|
expect(schema).to eq(expected_schema)
|
|
54
184
|
end
|
|
55
|
-
end
|
|
56
185
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
186
|
+
context "with constrained value types (issue #191)" do
|
|
187
|
+
subject(:schema) do
|
|
188
|
+
described_class.generate(SchemaGeneration::Record, pretty: true)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Indentation depends on nesting depth; compare with leading whitespace
|
|
192
|
+
# stripped so the assertions pin structure and order, not column counts.
|
|
193
|
+
let(:unindented) { schema.gsub(/^ +/, "") }
|
|
194
|
+
|
|
195
|
+
it "inlines an xs:restriction with facets in canonical order for an attribute" do
|
|
196
|
+
expect(unindented).to include(<<~XSD.chomp)
|
|
197
|
+
<xs:attribute name="code">
|
|
198
|
+
<xs:simpleType>
|
|
199
|
+
<xs:restriction base="xs:string">
|
|
200
|
+
<xs:minLength value="2"/>
|
|
201
|
+
<xs:maxLength value="8"/>
|
|
202
|
+
<xs:enumeration value="AB"/>
|
|
203
|
+
<xs:enumeration value="CD"/>
|
|
204
|
+
<xs:whiteSpace value="collapse"/>
|
|
205
|
+
<xs:pattern value="[A-Z]+"/>
|
|
206
|
+
</xs:restriction>
|
|
207
|
+
</xs:simpleType>
|
|
208
|
+
</xs:attribute>
|
|
209
|
+
XSD
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "emits integer inclusive bounds on a simple element" do
|
|
213
|
+
expect(unindented).to include(<<~XSD.chomp)
|
|
214
|
+
<xs:element name="percent">
|
|
215
|
+
<xs:simpleType>
|
|
216
|
+
<xs:restriction base="xs:integer">
|
|
217
|
+
<xs:minInclusive value="0"/>
|
|
218
|
+
<xs:maxInclusive value="100"/>
|
|
219
|
+
</xs:restriction>
|
|
220
|
+
</xs:simpleType>
|
|
221
|
+
</xs:element>
|
|
222
|
+
XSD
|
|
68
223
|
end
|
|
69
224
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
it "produces a schema Nokogiri can load (with target namespace)" do
|
|
78
|
-
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
79
|
-
uri "http://example.com/addr"
|
|
80
|
-
prefix_default "ex"
|
|
81
|
-
element_form_default :qualified
|
|
82
|
-
end
|
|
83
|
-
klass = Class.new(Lutaml::Model::Serializable) do
|
|
84
|
-
attribute :street, :string
|
|
85
|
-
xml do
|
|
86
|
-
root "address"
|
|
87
|
-
namespace ns
|
|
88
|
-
map_element "street", to: :street
|
|
89
|
-
end
|
|
225
|
+
it "renders decimal bounds and digit facets with exact lexical values" do
|
|
226
|
+
expect(schema).to include('<xs:restriction base="xs:decimal">')
|
|
227
|
+
expect(schema).to include('<xs:minExclusive value="1.5"/>')
|
|
228
|
+
expect(schema).to include('<xs:totalDigits value="5"/>')
|
|
229
|
+
expect(schema).to include('<xs:fractionDigits value="2"/>')
|
|
90
230
|
end
|
|
91
231
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
uri "http://example.com/addr"
|
|
104
|
-
prefix_default "ex"
|
|
105
|
-
element_form_default :qualified
|
|
106
|
-
end
|
|
107
|
-
widget = Class.new(Lutaml::Model::Serializable) do
|
|
108
|
-
attribute :label, :string
|
|
109
|
-
xml do
|
|
110
|
-
root "widget"
|
|
111
|
-
type_name "WidgetType"
|
|
112
|
-
map_element "label", to: :label
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
116
|
-
attribute :widget, widget
|
|
117
|
-
attribute :title, :string
|
|
118
|
-
xml do
|
|
119
|
-
root "doc"
|
|
120
|
-
namespace ns
|
|
121
|
-
map_element "widget", to: :widget
|
|
122
|
-
map_element "title", to: :title
|
|
123
|
-
end
|
|
232
|
+
it "inlines the restriction on the item of a constrained collection" do
|
|
233
|
+
expect(unindented).to include(<<~XSD.chomp)
|
|
234
|
+
<xs:element name="item">
|
|
235
|
+
<xs:simpleType>
|
|
236
|
+
<xs:restriction base="xs:string">
|
|
237
|
+
<xs:minLength value="1"/>
|
|
238
|
+
<xs:maxLength value="3"/>
|
|
239
|
+
</xs:restriction>
|
|
240
|
+
</xs:simpleType>
|
|
241
|
+
</xs:element>
|
|
242
|
+
XSD
|
|
124
243
|
end
|
|
125
244
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
expect(xsd).to include('type="ex:WidgetType"')
|
|
130
|
-
expect(xsd).to include('<xs:complexType name="WidgetType">')
|
|
131
|
-
expect(xsd).not_to include("<xs:import")
|
|
132
|
-
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Guard against over-eager prefixing: with no namespace, named-type refs
|
|
136
|
-
# must stay unqualified (they resolve to no-namespace, which is valid).
|
|
137
|
-
it "leaves named-type references unqualified when there is no namespace" do
|
|
138
|
-
inner = Class.new(Lutaml::Model::Serializable) do
|
|
139
|
-
attribute :city, :string
|
|
140
|
-
xml do
|
|
141
|
-
root "address"
|
|
142
|
-
type_name "AddressType"
|
|
143
|
-
map_element "city", to: :city
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
outer = Class.new(Lutaml::Model::Serializable) do
|
|
147
|
-
attribute :address, inner
|
|
148
|
-
xml do
|
|
149
|
-
root "person"
|
|
150
|
-
map_element "address", to: :address
|
|
151
|
-
end
|
|
245
|
+
it "leaves an unconstrained type as a flat type reference" do
|
|
246
|
+
expect(schema).to include('<xs:element name="note" type="xs:string"/>')
|
|
247
|
+
expect(schema).not_to match(/name="code"[^>]*type=/)
|
|
152
248
|
end
|
|
153
249
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
it "qualifies named-type references for collections under a target namespace" do
|
|
162
|
-
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
163
|
-
uri "http://example.com/addr"
|
|
164
|
-
prefix_default "ex"
|
|
165
|
-
element_form_default :qualified
|
|
166
|
-
end
|
|
167
|
-
item = Class.new(Lutaml::Model::Serializable) do
|
|
168
|
-
attribute :label, :string
|
|
169
|
-
xml do
|
|
170
|
-
root "item"
|
|
171
|
-
type_name "ItemType"
|
|
172
|
-
map_element "label", to: :label
|
|
173
|
-
end
|
|
250
|
+
it "fails fast on conjunctive patterns rather than emitting OR siblings" do
|
|
251
|
+
expect do
|
|
252
|
+
described_class.generate(SchemaGeneration::MultiPatternHolder)
|
|
253
|
+
end.to raise_error(Lutaml::Model::Error, /conjunctive pattern/)
|
|
174
254
|
end
|
|
175
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
176
|
-
attribute :items, item, collection: true
|
|
177
|
-
xml do
|
|
178
|
-
root "doc"
|
|
179
|
-
namespace ns
|
|
180
|
-
map_element "item", to: :items
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
185
|
-
|
|
186
|
-
expect(xsd).to include('type="ex:ItemType"')
|
|
187
|
-
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
188
255
|
end
|
|
189
256
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
it "imports and prefixes a nested model that declares a different namespace" do
|
|
194
|
-
w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
195
|
-
uri "http://w.example/w"
|
|
196
|
-
prefix_default "w"
|
|
197
|
-
element_form_default :qualified
|
|
198
|
-
schema_location "widget.xsd"
|
|
199
|
-
end
|
|
200
|
-
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
201
|
-
uri "http://ex.example/ex"
|
|
202
|
-
prefix_default "ex"
|
|
203
|
-
element_form_default :qualified
|
|
204
|
-
end
|
|
205
|
-
widget = Class.new(Lutaml::Model::Serializable) do
|
|
206
|
-
attribute :label, :string
|
|
207
|
-
xml do
|
|
208
|
-
root "widget"
|
|
209
|
-
namespace w_ns
|
|
210
|
-
type_name "WidgetType"
|
|
211
|
-
map_element "label", to: :label
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
215
|
-
attribute :widget, widget
|
|
216
|
-
xml do
|
|
217
|
-
root "doc"
|
|
218
|
-
namespace ex_ns
|
|
219
|
-
map_element "widget", to: :widget
|
|
220
|
-
end
|
|
221
|
-
end
|
|
257
|
+
context "with a Ruby pattern exported to XSD (issue #191)" do
|
|
258
|
+
it "strips the Ruby whole-string anchors" do
|
|
259
|
+
schema = described_class.generate(SchemaGeneration::AnchoredHolder)
|
|
222
260
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# both namespaces declared, foreign one imported, ref uses its own prefix
|
|
226
|
-
expect(xsd).to include('xmlns:w="http://w.example/w"')
|
|
227
|
-
expect(xsd).to include('targetNamespace="http://ex.example/ex"')
|
|
228
|
-
expect(xsd).to include('<xs:import namespace="http://w.example/w" schemaLocation="widget.xsd"/>')
|
|
229
|
-
expect(xsd).to include('type="w:WidgetType"')
|
|
230
|
-
# the foreign type is NOT defined in this document
|
|
231
|
-
expect(xsd).not_to include('<xs:complexType name="WidgetType">')
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# A target namespace whose class declares no prefix_default still needs a
|
|
235
|
-
# usable prefix for named-type QNames; one is synthesised (tns).
|
|
236
|
-
it "synthesises a prefix for a target namespace with no prefix_default" do
|
|
237
|
-
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
238
|
-
uri "http://example.com/np"
|
|
239
|
-
element_form_default :qualified
|
|
261
|
+
expect(schema).to include('<xs:pattern value="[A-Z]{3}"/>')
|
|
240
262
|
end
|
|
241
|
-
klass = Class.new(Lutaml::Model::Serializable) do
|
|
242
|
-
attribute :id, :string
|
|
243
|
-
xml do
|
|
244
|
-
root "root"
|
|
245
|
-
namespace ns
|
|
246
|
-
type_name "RootType"
|
|
247
|
-
map_element "id", to: :id
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
it "emits a pattern Nokogiri accepts as valid XSD" do
|
|
265
|
+
schema = described_class.generate(SchemaGeneration::AnchoredHolder)
|
|
266
|
+
value = schema[/<xs:pattern value="([^"]*)"/, 1]
|
|
267
|
+
wrapped = <<~XSD
|
|
268
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
269
|
+
<xs:simpleType name="T">
|
|
270
|
+
<xs:restriction base="xs:string">
|
|
271
|
+
<xs:pattern value="#{value}"/>
|
|
272
|
+
</xs:restriction>
|
|
273
|
+
</xs:simpleType>
|
|
274
|
+
</xs:schema>
|
|
275
|
+
XSD
|
|
257
276
|
|
|
258
|
-
|
|
259
|
-
# generator raises rather than emit a silently-wrong QName.
|
|
260
|
-
it "raises when two namespaces share a prefix" do
|
|
261
|
-
a_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
262
|
-
uri "http://a.example"
|
|
263
|
-
prefix_default "p"
|
|
264
|
-
end
|
|
265
|
-
b_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
266
|
-
uri "http://b.example"
|
|
267
|
-
prefix_default "p"
|
|
268
|
-
end
|
|
269
|
-
child = Class.new(Lutaml::Model::Serializable) do
|
|
270
|
-
attribute :v, :string
|
|
271
|
-
xml do
|
|
272
|
-
root "child"
|
|
273
|
-
namespace b_ns
|
|
274
|
-
type_name "BType"
|
|
275
|
-
map_element "v", to: :v
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
parent = Class.new(Lutaml::Model::Serializable) do
|
|
279
|
-
attribute :child, child
|
|
280
|
-
xml do
|
|
281
|
-
root "parent"
|
|
282
|
-
namespace a_ns
|
|
283
|
-
map_element "child", to: :child
|
|
284
|
-
end
|
|
277
|
+
expect { Nokogiri::XML::Schema(wrapped) }.not_to raise_error
|
|
285
278
|
end
|
|
286
279
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# A recursive model (A -> B -> A) must terminate and define both types.
|
|
292
|
-
it "handles recursive model references without overflowing" do
|
|
293
|
-
rec_a = Class.new(Lutaml::Model::Serializable)
|
|
294
|
-
rec_b = Class.new(Lutaml::Model::Serializable) do
|
|
295
|
-
attribute :a, rec_a
|
|
296
|
-
xml do
|
|
297
|
-
root "b"
|
|
298
|
-
type_name "BType"
|
|
299
|
-
map_element "a", to: :a
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
rec_a.class_eval do
|
|
303
|
-
attribute :b, rec_b
|
|
304
|
-
xml do
|
|
305
|
-
root "a"
|
|
306
|
-
type_name "AType"
|
|
307
|
-
map_element "b", to: :b
|
|
308
|
-
end
|
|
280
|
+
it "raises for a construct XSD cannot express" do
|
|
281
|
+
expect do
|
|
282
|
+
described_class.generate(SchemaGeneration::LookaheadHolder)
|
|
283
|
+
end.to raise_error(Lutaml::Model::Error, /not expressible/)
|
|
309
284
|
end
|
|
310
285
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
286
|
+
it "raises for a regexp flag XSD cannot express" do
|
|
287
|
+
expect do
|
|
288
|
+
described_class.generate(SchemaGeneration::FlaggedHolder)
|
|
289
|
+
end.to raise_error(Lutaml::Model::Error, /i\/m\/x flags/)
|
|
290
|
+
end
|
|
316
291
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
foreign = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
323
|
-
uri "http://foreign.example"
|
|
324
|
-
prefix_default bad_prefix unless bad_prefix.nil?
|
|
325
|
-
element_form_default :qualified
|
|
326
|
-
end
|
|
327
|
-
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
328
|
-
uri "http://ex.example/ex"
|
|
329
|
-
prefix_default "ex"
|
|
330
|
-
end
|
|
331
|
-
child = Class.new(Lutaml::Model::Serializable) do
|
|
332
|
-
attribute :v, :string
|
|
292
|
+
it "raises for a lazy quantifier XSD cannot express" do
|
|
293
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
294
|
+
attribute :field, Class.new(Lutaml::Model::Type::String) {
|
|
295
|
+
pattern(/a+?/)
|
|
296
|
+
}
|
|
333
297
|
xml do
|
|
334
|
-
root "
|
|
335
|
-
|
|
336
|
-
type_name "ForeignType"
|
|
337
|
-
map_element "v", to: :v
|
|
298
|
+
root "LazyHolder"
|
|
299
|
+
map_element "field", to: :field
|
|
338
300
|
end
|
|
339
301
|
end
|
|
340
|
-
|
|
341
|
-
|
|
302
|
+
|
|
303
|
+
expect { described_class.generate(holder) }
|
|
304
|
+
.to raise_error(Lutaml::Model::Error, /not expressible in XSD/)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Backslash-run parity: `\\z` is an escaped backslash + literal "z", not
|
|
308
|
+
# the `\z` anchor, so it must survive stripping (and be valid XSD) rather
|
|
309
|
+
# than being truncated to a dangling backslash and spuriously rejected.
|
|
310
|
+
it "keeps a literal escaped-backslash before z (even run)" do
|
|
311
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
312
|
+
attribute :field, Class.new(Lutaml::Model::Type::String) {
|
|
313
|
+
pattern(/\\z/)
|
|
314
|
+
}
|
|
342
315
|
xml do
|
|
343
|
-
root "
|
|
344
|
-
|
|
345
|
-
map_element "child", to: :child
|
|
316
|
+
root "BackslashZ"
|
|
317
|
+
map_element "field", to: :field
|
|
346
318
|
end
|
|
347
319
|
end
|
|
348
320
|
|
|
349
|
-
expect
|
|
350
|
-
.to
|
|
321
|
+
expect(described_class.generate(holder))
|
|
322
|
+
.to include('<xs:pattern value="\\\\z"/>')
|
|
351
323
|
end
|
|
352
|
-
end
|
|
353
324
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
end
|
|
366
|
-
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
367
|
-
uri "http://ex.example/ex"
|
|
368
|
-
prefix_default "ex"
|
|
369
|
-
end
|
|
370
|
-
inner = Class.new(Lutaml::Model::Serializable) do
|
|
371
|
-
attribute :v, :string
|
|
372
|
-
xml do
|
|
373
|
-
root "inner"
|
|
374
|
-
namespace inner_ns
|
|
375
|
-
type_name "InnerType"
|
|
376
|
-
map_element "v", to: :v
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
widget = Class.new(Lutaml::Model::Serializable) do
|
|
380
|
-
attribute :inner, inner
|
|
381
|
-
xml do
|
|
382
|
-
root "widget"
|
|
383
|
-
namespace w_ns
|
|
384
|
-
type_name "WidgetType"
|
|
385
|
-
map_element "inner", to: :inner
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
389
|
-
attribute :widget, widget
|
|
390
|
-
xml do
|
|
391
|
-
root "doc"
|
|
392
|
-
namespace ex_ns
|
|
393
|
-
map_element "widget", to: :widget
|
|
325
|
+
# `\\$` is an escaped backslash followed by the end anchor `$` (even run
|
|
326
|
+
# before `$`), so the anchor strips, leaving the literal backslash.
|
|
327
|
+
it "strips a trailing $ anchor after an escaped backslash" do
|
|
328
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
329
|
+
attribute :field, Class.new(Lutaml::Model::Type::String) {
|
|
330
|
+
pattern(/\\$/)
|
|
331
|
+
}
|
|
332
|
+
xml do
|
|
333
|
+
root "BackslashDollar"
|
|
334
|
+
map_element "field", to: :field
|
|
335
|
+
end
|
|
394
336
|
end
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
398
|
-
|
|
399
|
-
expect(xsd).to include('type="w:WidgetType"')
|
|
400
|
-
expect(xsd).not_to include("http://c.example")
|
|
401
|
-
end
|
|
402
337
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
uri "http://w-noloc.example/w"
|
|
406
|
-
prefix_default "w" # no schema_location
|
|
338
|
+
expect(described_class.generate(holder))
|
|
339
|
+
.to include('<xs:pattern value="\\\\"/>')
|
|
407
340
|
end
|
|
408
|
-
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
409
|
-
uri "http://ex.example/ex"
|
|
410
|
-
prefix_default "ex"
|
|
411
|
-
end
|
|
412
|
-
widget = Class.new(Lutaml::Model::Serializable) do
|
|
413
|
-
attribute :label, :string
|
|
414
|
-
xml do
|
|
415
|
-
root "widget"
|
|
416
|
-
namespace w_ns
|
|
417
|
-
type_name "WidgetType"
|
|
418
|
-
map_element "label", to: :label
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
422
|
-
attribute :widget, widget
|
|
423
|
-
xml do
|
|
424
|
-
root "doc"
|
|
425
|
-
namespace ex_ns
|
|
426
|
-
map_element "widget", to: :widget
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
expect { Lutaml::Model::Schema.to_xsd(doc) }
|
|
431
|
-
.to raise_error(Lutaml::Model::Error, /schema_location/)
|
|
432
|
-
end
|
|
433
341
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
root "widget"
|
|
447
|
-
namespace w_ns
|
|
448
|
-
type_name "WidgetType"
|
|
449
|
-
map_element "label", to: :label
|
|
450
|
-
end
|
|
451
|
-
end
|
|
452
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
453
|
-
attribute :widget, widget
|
|
454
|
-
xml do
|
|
455
|
-
root "doc"
|
|
456
|
-
namespace ex_ns
|
|
457
|
-
map_element "widget", to: :widget
|
|
342
|
+
# A long run of backslashes must be counted correctly (and in linear time,
|
|
343
|
+
# not the polynomial backtracking a run-quantifier regex would incur). An
|
|
344
|
+
# even run before `z` is all escaped pairs -> literal, kept verbatim.
|
|
345
|
+
it "handles a long even backslash run in linear time" do
|
|
346
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
347
|
+
attribute :field, Class.new(Lutaml::Model::Type::String) {
|
|
348
|
+
pattern(/\\\\\\\\z/) # 8 source backslashes -> 4 literal, then z
|
|
349
|
+
}
|
|
350
|
+
xml do
|
|
351
|
+
root "LongBackslash"
|
|
352
|
+
map_element "field", to: :field
|
|
353
|
+
end
|
|
458
354
|
end
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
xsd = nil
|
|
462
|
-
expect { xsd = Lutaml::Model::Schema.to_xsd(doc, skip_validation: true) }
|
|
463
|
-
.to output(/schema_location/).to_stderr
|
|
464
|
-
|
|
465
|
-
# Best-effort output still declares the usable prefix its QNames use,
|
|
466
|
-
# and imports the namespace (location-less).
|
|
467
|
-
expect(xsd).to include('xmlns:w="http://w-warn.example/w"')
|
|
468
|
-
expect(xsd).to include('type="w:WidgetType"')
|
|
469
|
-
expect(xsd).to include('<xs:import namespace="http://w-warn.example/w"/>')
|
|
470
|
-
end
|
|
471
355
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
it "does not downgrade prefix errors under skip_validation" do
|
|
475
|
-
no_pfx = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
476
|
-
uri "http://p-hard.example" # no prefix_default
|
|
477
|
-
end
|
|
478
|
-
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
479
|
-
uri "http://ex.example/ex"
|
|
480
|
-
prefix_default "ex"
|
|
481
|
-
end
|
|
482
|
-
child = Class.new(Lutaml::Model::Serializable) do
|
|
483
|
-
attribute :v, :string
|
|
484
|
-
xml do
|
|
485
|
-
root "child"
|
|
486
|
-
namespace no_pfx
|
|
487
|
-
type_name "PType"
|
|
488
|
-
map_element "v", to: :v
|
|
489
|
-
end
|
|
356
|
+
expect(described_class.generate(holder))
|
|
357
|
+
.to include('<xs:pattern value="\\\\\\\\\\\\\\\\z"/>')
|
|
490
358
|
end
|
|
491
|
-
parent = Class.new(Lutaml::Model::Serializable) do
|
|
492
|
-
attribute :child, child
|
|
493
|
-
xml do
|
|
494
|
-
root "parent"
|
|
495
|
-
namespace ex_ns
|
|
496
|
-
map_element "child", to: :child
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
expect do
|
|
501
|
-
Lutaml::Model::Schema.to_xsd(parent, skip_validation: true)
|
|
502
|
-
end.to raise_error(Lutaml::Model::Error, /usable prefix/)
|
|
503
359
|
end
|
|
504
360
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
gml_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
509
|
-
uri "http://www.opengis.net/gml"
|
|
510
|
-
prefix_default "gml"
|
|
511
|
-
schema_location "gml.xsd"
|
|
512
|
-
end
|
|
513
|
-
measure = Class.new(Lutaml::Model::Type::String) do
|
|
514
|
-
xml { namespace gml_ns }
|
|
515
|
-
xsd_type "gml:MeasureType"
|
|
516
|
-
end
|
|
517
|
-
doc = Class.new(Lutaml::Model::Serializable) do
|
|
518
|
-
attribute :measure, measure
|
|
519
|
-
xml do
|
|
520
|
-
root "doc"
|
|
521
|
-
map_element "measure", to: :measure
|
|
522
|
-
end
|
|
361
|
+
context "with Layer-1 attribute facets (issue #191)" do
|
|
362
|
+
subject(:schema) do
|
|
363
|
+
described_class.generate(SchemaGeneration::Layered, pretty: true)
|
|
523
364
|
end
|
|
524
365
|
|
|
525
|
-
|
|
526
|
-
# accepting them in validation is a deferred follow-up.
|
|
527
|
-
xsd = Lutaml::Model::Schema.to_xsd(doc, skip_validation: true)
|
|
366
|
+
let(:unindented) { schema.gsub(/^ +/, "") }
|
|
528
367
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
attribute :child, child
|
|
554
|
-
attribute :id, :string
|
|
555
|
-
xml do
|
|
556
|
-
root "doc"
|
|
557
|
-
namespace no_pfx_target
|
|
558
|
-
type_name "DocType"
|
|
559
|
-
map_element "child", to: :child
|
|
560
|
-
map_element "id", to: :id
|
|
561
|
-
end
|
|
368
|
+
it "inlines an xs:restriction for attribute min/max bounds" do
|
|
369
|
+
expect(unindented).to include(<<~XSD.chomp)
|
|
370
|
+
<xs:element name="age">
|
|
371
|
+
<xs:simpleType>
|
|
372
|
+
<xs:restriction base="xs:integer">
|
|
373
|
+
<xs:minInclusive value="0"/>
|
|
374
|
+
<xs:maxInclusive value="120"/>
|
|
375
|
+
</xs:restriction>
|
|
376
|
+
</xs:simpleType>
|
|
377
|
+
</xs:element>
|
|
378
|
+
XSD
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it "inlines an xs:restriction for attribute length bounds" do
|
|
382
|
+
expect(unindented).to include(<<~XSD.chomp)
|
|
383
|
+
<xs:element name="code">
|
|
384
|
+
<xs:simpleType>
|
|
385
|
+
<xs:restriction base="xs:string">
|
|
386
|
+
<xs:minLength value="2"/>
|
|
387
|
+
<xs:maxLength value="8"/>
|
|
388
|
+
</xs:restriction>
|
|
389
|
+
</xs:simpleType>
|
|
390
|
+
</xs:element>
|
|
391
|
+
XSD
|
|
562
392
|
end
|
|
563
393
|
|
|
564
|
-
|
|
394
|
+
it "emits the tighter merged bound without a spurious raise" do
|
|
395
|
+
schema = described_class.generate(SchemaGeneration::MergedLayers)
|
|
565
396
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
expect(xsd).to include('type="tns1:DocType"')
|
|
569
|
-
expect(xsd).to include('type="tns:TType"')
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
# A foreign-namespaced nested model with NO type_name is inlined into this
|
|
573
|
-
# document (like a same-namespace model), not imported — so its namespace
|
|
574
|
-
# must not trigger the foreign-import schema_location requirement. The
|
|
575
|
-
# namespace walk and the element walk must agree that it is not a boundary.
|
|
576
|
-
it "does not require schema_location for an inlined (type_name-less) foreign model" do
|
|
577
|
-
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
578
|
-
uri "http://inner.example/in"
|
|
579
|
-
prefix_default "in" # no schema_location, no type_name below
|
|
580
|
-
end
|
|
581
|
-
outer_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
582
|
-
uri "http://outer.example/out"
|
|
583
|
-
prefix_default "out"
|
|
584
|
-
end
|
|
585
|
-
inner = Class.new(Lutaml::Model::Serializable) do
|
|
586
|
-
attribute :label, :string
|
|
587
|
-
xml do
|
|
588
|
-
root "inner"
|
|
589
|
-
namespace inner_ns
|
|
590
|
-
map_element "label", to: :label
|
|
591
|
-
end
|
|
592
|
-
end
|
|
593
|
-
outer = Class.new(Lutaml::Model::Serializable) do
|
|
594
|
-
attribute :inner, inner
|
|
595
|
-
xml do
|
|
596
|
-
root "outer"
|
|
597
|
-
namespace outer_ns
|
|
598
|
-
map_element "inner", to: :inner
|
|
599
|
-
end
|
|
397
|
+
expect(schema).to include('<xs:minInclusive value="0"/>')
|
|
398
|
+
expect(schema).to include('<xs:maxInclusive value="50"/>')
|
|
600
399
|
end
|
|
601
400
|
|
|
602
|
-
|
|
401
|
+
it "emits the pre-#191 values:/pattern: options as facets" do
|
|
402
|
+
schema = described_class.generate(SchemaGeneration::LegacyOptions)
|
|
603
403
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
it "does not emit a spurious import for an inlined foreign model with a schema_location" do
|
|
610
|
-
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
611
|
-
uri "http://inner2.example/in"
|
|
612
|
-
prefix_default "in"
|
|
613
|
-
schema_location "inner.xsd" # present, but model has no type_name
|
|
614
|
-
end
|
|
615
|
-
outer_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
616
|
-
uri "http://outer2.example/out"
|
|
617
|
-
prefix_default "out"
|
|
404
|
+
expect(schema).to include('<xs:enumeration value="on"/>')
|
|
405
|
+
expect(schema).to include('<xs:enumeration value="off"/>')
|
|
406
|
+
expect(schema).to include('<xs:pattern value="[A-Z]{2}"/>')
|
|
618
407
|
end
|
|
619
|
-
inner = Class.new(Lutaml::Model::Serializable) do
|
|
620
|
-
attribute :label, :string
|
|
621
|
-
xml do
|
|
622
|
-
root "inner"
|
|
623
|
-
namespace inner_ns
|
|
624
|
-
map_element "label", to: :label
|
|
625
|
-
end
|
|
626
|
-
end
|
|
627
|
-
outer = Class.new(Lutaml::Model::Serializable) do
|
|
628
|
-
attribute :inner, inner
|
|
629
|
-
xml do
|
|
630
|
-
root "outer"
|
|
631
|
-
namespace outer_ns
|
|
632
|
-
map_element "inner", to: :inner
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
xsd = Lutaml::Model::Schema.to_xsd(outer)
|
|
637
408
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
# A prefix bound to two different namespaces — a foreign model vs a
|
|
643
|
-
# Type::Value type sharing a prefix_default — is unresolvable and must
|
|
644
|
-
# raise, not silently keep the first binding.
|
|
645
|
-
it "raises when a foreign model and a Type::Value share a prefix for different namespaces" do
|
|
646
|
-
model_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
647
|
-
uri "http://model.example/gml"
|
|
648
|
-
prefix_default "gml"
|
|
649
|
-
schema_location "model.xsd"
|
|
650
|
-
end
|
|
651
|
-
type_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
652
|
-
uri "http://type.example/gml"
|
|
653
|
-
prefix_default "gml"
|
|
654
|
-
schema_location "type.xsd"
|
|
655
|
-
end
|
|
656
|
-
measure = Class.new(Lutaml::Model::Type::String) do
|
|
657
|
-
xml { namespace type_ns }
|
|
658
|
-
xsd_type "gml:MeasureType"
|
|
659
|
-
end
|
|
660
|
-
widget = Class.new(Lutaml::Model::Serializable) do
|
|
661
|
-
attribute :label, :string
|
|
662
|
-
xml do
|
|
663
|
-
root "widget"
|
|
664
|
-
namespace model_ns
|
|
665
|
-
type_name "WidgetType"
|
|
666
|
-
map_element "label", to: :label
|
|
409
|
+
it "intersects a values: option with a Layer-2 enumeration" do
|
|
410
|
+
enum_type = Class.new(Lutaml::Model::Type::String) do
|
|
411
|
+
enumeration "a", "b", "c"
|
|
667
412
|
end
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
attribute :size, measure
|
|
675
|
-
attribute :widget, widget
|
|
676
|
-
xml do
|
|
677
|
-
root "doc"
|
|
678
|
-
namespace target_ns
|
|
679
|
-
map_element "size", to: :size
|
|
680
|
-
map_element "widget", to: :widget
|
|
413
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
414
|
+
attribute :s, enum_type, values: %w[a b]
|
|
415
|
+
xml do
|
|
416
|
+
root "R"
|
|
417
|
+
map_element "s", to: :s
|
|
418
|
+
end
|
|
681
419
|
end
|
|
682
|
-
|
|
420
|
+
schema = described_class.generate(model)
|
|
683
421
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
Lutaml::Model::Schema.to_xsd(doc, skip_validation: true)
|
|
688
|
-
end.to raise_error(Lutaml::Model::Error, /two different namespaces/)
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
# A collection of a type_name-less model renders as a placeholder `item`
|
|
692
|
-
# element, so nothing inside it is emitted — the namespace walk must not
|
|
693
|
-
# descend into it and import a foreign type the schema never references.
|
|
694
|
-
it "does not import namespaces reachable only through a placeholder collection" do
|
|
695
|
-
child_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
696
|
-
uri "http://child.example/ch"
|
|
697
|
-
prefix_default "ch" # foreign + type_name + NO schema_location
|
|
698
|
-
end
|
|
699
|
-
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
700
|
-
uri "http://binner.example/b"
|
|
701
|
-
prefix_default "bin"
|
|
702
|
-
end
|
|
703
|
-
child = Class.new(Lutaml::Model::Serializable) do
|
|
704
|
-
attribute :v, :string
|
|
705
|
-
xml do
|
|
706
|
-
root "child"
|
|
707
|
-
namespace child_ns
|
|
708
|
-
type_name "ChildType"
|
|
709
|
-
map_element "v", to: :v
|
|
710
|
-
end
|
|
711
|
-
end
|
|
712
|
-
inner = Class.new(Lutaml::Model::Serializable) do
|
|
713
|
-
attribute :child, child
|
|
714
|
-
xml do
|
|
715
|
-
root "inner"
|
|
716
|
-
namespace inner_ns
|
|
717
|
-
map_element "child", to: :child
|
|
718
|
-
end
|
|
719
|
-
end
|
|
720
|
-
root_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
721
|
-
uri "http://root.example/r"
|
|
722
|
-
prefix_default "r"
|
|
723
|
-
end
|
|
724
|
-
root = Class.new(Lutaml::Model::Serializable) do
|
|
725
|
-
attribute :inners, inner, collection: true
|
|
726
|
-
xml do
|
|
727
|
-
root "root"
|
|
728
|
-
namespace root_ns
|
|
729
|
-
map_element "inner", to: :inners
|
|
730
|
-
end
|
|
422
|
+
expect(schema).to include('<xs:enumeration value="a"/>')
|
|
423
|
+
expect(schema).to include('<xs:enumeration value="b"/>')
|
|
424
|
+
expect(schema).not_to include('<xs:enumeration value="c"/>')
|
|
731
425
|
end
|
|
732
426
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
# the placeholder collection.
|
|
737
|
-
expect(xsd).not_to include("http://child.example/ch")
|
|
738
|
-
expect(xsd).not_to include("http://binner.example/b")
|
|
739
|
-
end
|
|
740
|
-
|
|
741
|
-
# Locks the deliberate skip-before-cycle-guard ordering: a model referenced
|
|
742
|
-
# BOTH as a placeholder collection and as a single inlined attribute must
|
|
743
|
-
# still be inlined via the single path — the placeholder reference must not
|
|
744
|
-
# consume the model's cycle-guard slot and mask the emitted one. Regardless
|
|
745
|
-
# of attribute order, the inlined model's nested named type stays defined.
|
|
746
|
-
%i[collection_first single_first].each do |order|
|
|
747
|
-
it "defines a nested type of a model also referenced as a placeholder collection (#{order})" do
|
|
748
|
-
child = Class.new(Lutaml::Model::Serializable) do
|
|
749
|
-
attribute :v, :string
|
|
750
|
-
xml do
|
|
751
|
-
root "child"
|
|
752
|
-
type_name "CType"
|
|
753
|
-
map_element "v", to: :v
|
|
754
|
-
end
|
|
427
|
+
it "raises when values: is disjoint from the Layer-2 enumeration" do
|
|
428
|
+
enum_type = Class.new(Lutaml::Model::Type::String) do
|
|
429
|
+
enumeration "a", "b"
|
|
755
430
|
end
|
|
756
|
-
|
|
757
|
-
attribute :
|
|
431
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
432
|
+
attribute :s, enum_type, values: %w[c d]
|
|
758
433
|
xml do
|
|
759
|
-
root "
|
|
760
|
-
map_element "
|
|
761
|
-
end
|
|
762
|
-
end
|
|
763
|
-
root = Class.new(Lutaml::Model::Serializable) do
|
|
764
|
-
if order == :collection_first
|
|
765
|
-
attribute :many, mid, collection: true
|
|
766
|
-
attribute :one, mid
|
|
767
|
-
else
|
|
768
|
-
attribute :one, mid
|
|
769
|
-
attribute :many, mid, collection: true
|
|
770
|
-
end
|
|
771
|
-
xml do
|
|
772
|
-
root "root"
|
|
773
|
-
map_element "many", to: :many
|
|
774
|
-
map_element "one", to: :one
|
|
434
|
+
root "R"
|
|
435
|
+
map_element "s", to: :s
|
|
775
436
|
end
|
|
776
437
|
end
|
|
777
438
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
# The single-inline path references CType, so CType must be defined —
|
|
781
|
-
# even when the placeholder collection references `mid` first.
|
|
782
|
-
expect(xsd).to include('<xs:complexType name="CType">')
|
|
783
|
-
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
439
|
+
expect { described_class.generate(model) }
|
|
440
|
+
.to raise_error(ArgumentError, /enumeration allows no values/)
|
|
784
441
|
end
|
|
785
|
-
end
|
|
786
442
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
xml do
|
|
797
|
-
root "child"
|
|
798
|
-
namespace foreign_ns
|
|
799
|
-
type_name "ChildType"
|
|
800
|
-
map_element "v", to: :v
|
|
801
|
-
end
|
|
802
|
-
end
|
|
803
|
-
root = Class.new(Lutaml::Model::Serializable) do
|
|
804
|
-
attribute :child, child
|
|
805
|
-
xml do
|
|
806
|
-
root "root" # no namespace -> target_uri is nil
|
|
807
|
-
map_element "child", to: :child
|
|
443
|
+
# An explicit `min: nil` means "no bound"; it must not leave a nil facet
|
|
444
|
+
# that emits an empty <xs:restriction> around the element.
|
|
445
|
+
it "treats an explicit nil bound as absent (no empty restriction)" do
|
|
446
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
447
|
+
attribute :x, :integer, min: nil
|
|
448
|
+
xml do
|
|
449
|
+
root "R"
|
|
450
|
+
map_element "x", to: :x
|
|
451
|
+
end
|
|
808
452
|
end
|
|
809
|
-
|
|
453
|
+
schema = described_class.generate(model)
|
|
810
454
|
|
|
811
|
-
|
|
812
|
-
.
|
|
455
|
+
expect(schema).to include('<xs:element name="x" type="xs:integer"/>')
|
|
456
|
+
expect(schema).not_to include("<restriction")
|
|
457
|
+
end
|
|
813
458
|
end
|
|
814
459
|
end
|
|
815
460
|
end
|