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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/Gemfile +4 -0
  4. data/README.adoc +39 -0
  5. data/docs/conversion_caching.adoc +143 -0
  6. data/lib/compat/opal/lutaml_model_boot.rb +15 -0
  7. data/lib/lutaml/model/attribute.rb +89 -0
  8. data/lib/lutaml/model/config.rb +13 -0
  9. data/lib/lutaml/model/configuration.rb +23 -0
  10. data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
  11. data/lib/lutaml/model/error/length_error.rb +19 -0
  12. data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
  13. data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
  14. data/lib/lutaml/model/error/max_length_error.rb +20 -0
  15. data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
  16. data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
  17. data/lib/lutaml/model/error/min_length_error.rb +20 -0
  18. data/lib/lutaml/model/error/restriction_error.rb +8 -0
  19. data/lib/lutaml/model/error/total_digits_error.rb +20 -0
  20. data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
  21. data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
  22. data/lib/lutaml/model/error/type.rb +2 -0
  23. data/lib/lutaml/model/restriction_validation.rb +437 -0
  24. data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
  25. data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
  26. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
  27. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
  28. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
  29. data/lib/lutaml/model/schema/templates.rb +2 -0
  30. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
  31. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
  32. data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
  33. data/lib/lutaml/model/serialize/builder.rb +4 -5
  34. data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
  35. data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
  36. data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
  37. data/lib/lutaml/model/serialize/initialization.rb +4 -2
  38. data/lib/lutaml/model/serialize.rb +61 -0
  39. data/lib/lutaml/model/services/type/validator.rb +23 -0
  40. data/lib/lutaml/model/type/string.rb +28 -3
  41. data/lib/lutaml/model/type/value.rb +231 -0
  42. data/lib/lutaml/model/type.rb +2 -0
  43. data/lib/lutaml/model/validation.rb +2 -1
  44. data/lib/lutaml/model/version.rb +1 -1
  45. data/lib/lutaml/model.rb +16 -0
  46. data/lib/lutaml/xml/model_transform.rb +47 -6
  47. data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
  48. data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
  49. data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
  50. data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
  51. data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
  52. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
  53. data/lib/lutaml/xml/transformation.rb +2 -0
  54. data/lib/lutaml/xml/transformation_support.rb +1 -0
  55. data/spec/fixtures/xml/restriction_facets.xsd +46 -0
  56. data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
  57. data/spec/lutaml/model/attribute_spec.rb +7 -2
  58. data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
  59. data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
  60. data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
  61. data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
  62. data/spec/lutaml/model/phantom_value_spec.rb +799 -0
  63. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
  64. data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
  65. data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
  66. data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
  67. data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
  68. data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
  69. data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
  70. data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
  71. metadata +27 -1
@@ -1,6 +1,6 @@
1
1
  require "spec_helper"
2
2
  require "lutaml/model/schema"
3
- require "nokogiri"
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
- # Regression guard for issue #717: the generated XSD bound the XSD namespace
58
- # only as the default xmlns while referencing built-in types with an
59
- # undeclared "xs:" prefix, so no XSD processor could load the output.
60
- describe "generated XSD validity (issue #717)" do
61
- it "produces a schema Nokogiri can load (no target namespace)" do
62
- klass = Class.new(Lutaml::Model::Serializable) do
63
- attribute :id, :string
64
- xml do
65
- root "address"
66
- map_element "id", to: :id
67
- end
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
- xsd = Lutaml::Model::Schema.to_xsd(klass)
71
-
72
- expect(xsd).to include('<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"')
73
- expect(xsd).to include('type="xs:string"')
74
- expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
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
- xsd = Lutaml::Model::Schema.to_xsd(klass)
93
-
94
- expect(xsd).to include('xmlns:xs="http://www.w3.org/2001/XMLSchema"')
95
- expect(xsd).to include('targetNamespace="http://example.com/addr"')
96
- expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
97
- end
98
-
99
- # The hole #717 fell through: a nested named-type reference under a target
100
- # namespace must be prefixed, or it resolves to no-namespace and fails.
101
- it "qualifies named-type references under a target namespace" do
102
- ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
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
- xsd = Lutaml::Model::Schema.to_xsd(doc)
127
-
128
- expect(xsd).to include('xmlns:ex="http://example.com/addr"')
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
- xsd = Lutaml::Model::Schema.to_xsd(outer)
155
-
156
- expect(xsd).to include('type="AddressType"')
157
- expect(xsd).not_to include('type=":AddressType"')
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
- # A nested model in its OWN (different) namespace is imported and referenced
191
- # by its own prefix, NOT folded into the root namespace. (A schema document
192
- # has a single targetNamespace, so its type lives in its own schema.)
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
- xsd = Lutaml::Model::Schema.to_xsd(doc)
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
- expect(xsd).to include('xmlns:tns="http://example.com/np"')
254
- expect(xsd).to include('type="tns:RootType"')
255
- expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
256
- end
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
- # Two different namespaces bound to the same prefix are unresolvable; the
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
- expect { Lutaml::Model::Schema.to_xsd(parent) }
288
- .to raise_error(Lutaml::Model::Error, /prefix 'p' is bound to two/)
289
- end
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
- xsd = nil
312
- expect { xsd = Lutaml::Model::Schema.to_xsd(rec_a) }.not_to raise_error
313
- expect(xsd).to include('<xs:complexType name="AType">')
314
- expect(xsd).to include('<xs:complexType name="BType">')
315
- end
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
- # A foreign namespace with no usable prefix (nil, empty, or the reserved
318
- # "xs") cannot be referenced by a prefixed QName and must not borrow the
319
- # target prefix — raise instead.
320
- [nil, "", "xs"].each do |bad_prefix|
321
- it "raises for a foreign namespace with unusable prefix #{bad_prefix.inspect}" do
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 "child"
335
- namespace foreign
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
- parent = Class.new(Lutaml::Model::Serializable) do
341
- attribute :child, child
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 "parent"
344
- namespace ex_ns
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 { Lutaml::Model::Schema.to_xsd(parent) }
350
- .to raise_error(Lutaml::Model::Error, /foreign namespace .* usable prefix/)
321
+ expect(described_class.generate(holder))
322
+ .to include('<xs:pattern value="\\\\z"/>')
351
323
  end
352
- end
353
324
 
354
- # Namespaces used only INSIDE an imported foreign type belong to that
355
- # type's own schema document — they must not be declared, imported, or
356
- # validated here.
357
- it "ignores namespaces nested inside an imported foreign subtree" do
358
- inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
359
- uri "http://c.example" # deliberately no prefix_default
360
- end
361
- w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
362
- uri "http://w.example/w"
363
- prefix_default "w"
364
- schema_location "widget.xsd"
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
- it "raises when a foreign namespace has no schema_location" do
404
- w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
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
- it "downgrades foreign-namespace errors to warnings under skip_validation" do
435
- w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
436
- uri "http://w-warn.example/w"
437
- prefix_default "w" # no schema_location -> error without skip_validation
438
- end
439
- ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
440
- uri "http://ex.example/ex"
441
- prefix_default "ex"
442
- end
443
- widget = Class.new(Lutaml::Model::Serializable) do
444
- attribute :label, :string
445
- xml do
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
- # Prefix errors emit QNames that resolve to the WRONG namespace, so they
473
- # stay hard errors even under skip_validation.
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
- # A Type::Value attribute type that declares its own namespace resolves
506
- # through that namespace's imported schema.
507
- it "declares and imports a Type::Value attribute type's namespace" do
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
- # skip_validation matches main's behavior for namespaced custom types;
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
- expect(xsd).to include('xmlns:gml="http://www.opengis.net/gml"')
530
- expect(xsd).to include('<xs:import namespace="http://www.opengis.net/gml" schemaLocation="gml.xsd"/>')
531
- expect(xsd).to include('type="gml:MeasureType"')
532
- end
533
-
534
- it "synthesizes a non-colliding prefix when a foreign namespace claims tns" do
535
- no_pfx_target = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
536
- uri "http://example.com/np" # no prefix_default -> synthesized
537
- end
538
- tns_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
539
- uri "http://t.example"
540
- prefix_default "tns"
541
- schema_location "t.xsd"
542
- end
543
- child = Class.new(Lutaml::Model::Serializable) do
544
- attribute :v, :string
545
- xml do
546
- root "child"
547
- namespace tns_ns
548
- type_name "TType"
549
- map_element "v", to: :v
550
- end
551
- end
552
- doc = Class.new(Lutaml::Model::Serializable) do
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
- xsd = Lutaml::Model::Schema.to_xsd(doc)
394
+ it "emits the tighter merged bound without a spurious raise" do
395
+ schema = described_class.generate(SchemaGeneration::MergedLayers)
565
396
 
566
- expect(xsd).to include('xmlns:tns="http://t.example"')
567
- expect(xsd).to include('xmlns:tns1="http://example.com/np"')
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
- xsd = Lutaml::Model::Schema.to_xsd(outer)
401
+ it "emits the pre-#191 values:/pattern: options as facets" do
402
+ schema = described_class.generate(SchemaGeneration::LegacyOptions)
603
403
 
604
- # Inlined, not imported: its content appears here, no <xs:import> for it.
605
- expect(xsd).to include('name="label"')
606
- expect(xsd).not_to include("http://inner.example/in")
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
- expect(xsd).to include('name="label"')
639
- expect(xsd).not_to include("<xs:import")
640
- end
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
- end
669
- target_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
670
- uri "http://doc.example/doc"
671
- prefix_default "doc"
672
- end
673
- doc = Class.new(Lutaml::Model::Serializable) do
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
- end
420
+ schema = described_class.generate(model)
683
421
 
684
- # Raises even under skip_validation — a wrong-namespace QName is never
685
- # recoverable, matching the foreign prefix-collision behavior.
686
- expect do
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
- xsd = Lutaml::Model::Schema.to_xsd(root)
734
-
735
- # No spurious import/raise for the foreign child reachable only through
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
- mid = Class.new(Lutaml::Model::Serializable) do
757
- attribute :child, child
431
+ model = Class.new(Lutaml::Model::Serializable) do
432
+ attribute :s, enum_type, values: %w[c d]
758
433
  xml do
759
- root "mid"
760
- map_element "child", to: :child
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
- xsd = Lutaml::Model::Schema.to_xsd(root)
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
- # When the root schema has no targetNamespace, the foreign-prefix error must
788
- # not interpolate a nil target into a confusing "'' schema" string.
789
- it "reports the no-namespace schema clearly in the foreign-prefix error" do
790
- foreign_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
791
- uri "http://foreign.example/f"
792
- prefix_default "xs" # reserved -> unusable prefix, triggers the error
793
- end
794
- child = Class.new(Lutaml::Model::Serializable) do
795
- attribute :v, :string
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
- end
453
+ schema = described_class.generate(model)
810
454
 
811
- expect { Lutaml::Model::Schema.to_xsd(root) }
812
- .to raise_error(Lutaml::Model::Error, /this no-namespace schema/)
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