lutaml-model 0.8.17 → 0.8.18
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 +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
- data/lib/lutaml/model/serialize/builder.rb +15 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +30 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +97 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
require "rng"
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
require "lutaml/model/schema"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Schema::RngCompiler do
|
|
7
|
+
# Reset :default context before each test so generated classes from one
|
|
8
|
+
# context don't leak into another. Matches XSD compiler spec setup.
|
|
9
|
+
before do
|
|
10
|
+
default_ctx = Lutaml::Model::GlobalContext.context(:default)
|
|
11
|
+
if default_ctx
|
|
12
|
+
default_ctx.registry.clear
|
|
13
|
+
Lutaml::Model::Type.register_builtin_types_in(default_ctx.registry)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe ".to_models" do
|
|
18
|
+
context "with the address_book RNG schema (start element + zeroOrMore + ref + text + fragment define)" do
|
|
19
|
+
before do
|
|
20
|
+
stub_const("RngAddressBookSpec", Module.new)
|
|
21
|
+
described_class.to_models(
|
|
22
|
+
schema,
|
|
23
|
+
output_dir: dir,
|
|
24
|
+
create_files: true,
|
|
25
|
+
module_namespace: "RngAddressBookSpec",
|
|
26
|
+
)
|
|
27
|
+
require File.join(dir, "rngaddressbookspec_registry.rb")
|
|
28
|
+
RngAddressBookSpec.register_all
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
after do
|
|
32
|
+
FileUtils.rm_rf(dir)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
let(:dir) { Dir.mktmpdir }
|
|
36
|
+
let(:schema) { File.read("spec/fixtures/xml/schema/rng/address_book.rng") }
|
|
37
|
+
|
|
38
|
+
let(:valid_xml) do
|
|
39
|
+
<<~XML
|
|
40
|
+
<addressBook>
|
|
41
|
+
<card>
|
|
42
|
+
<name>Alice</name>
|
|
43
|
+
<email>alice@example.com</email>
|
|
44
|
+
</card>
|
|
45
|
+
<card>
|
|
46
|
+
<name>Bob</name>
|
|
47
|
+
<email>bob@example.com</email>
|
|
48
|
+
</card>
|
|
49
|
+
</addressBook>
|
|
50
|
+
XML
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "writes per-class files and a central registry" do
|
|
54
|
+
module_dir = File.join(dir, "rngaddressbookspec")
|
|
55
|
+
expect(File).to exist(File.join(dir, "rngaddressbookspec_registry.rb"))
|
|
56
|
+
expect(File).to exist(File.join(module_dir, "address_book.rb"))
|
|
57
|
+
expect(File).to exist(File.join(module_dir, "card.rb"))
|
|
58
|
+
expect(File).to exist(File.join(module_dir, "card_content.rb"))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "loads the AddressBook constant after register_all" do
|
|
62
|
+
expect(defined?(RngAddressBookSpec::AddressBook)).to eq("constant")
|
|
63
|
+
expect(defined?(RngAddressBookSpec::Card)).to eq("constant")
|
|
64
|
+
expect(defined?(RngAddressBookSpec::CardContent)).to eq("constant")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "round-trips a valid XML document through the generated classes" do
|
|
68
|
+
book = RngAddressBookSpec::AddressBook.from_xml(valid_xml)
|
|
69
|
+
expect(book.card.size).to eq(2)
|
|
70
|
+
expect(book.card[0].name).to eq("Alice")
|
|
71
|
+
expect(book.card[1].email).to eq("bob@example.com")
|
|
72
|
+
expect(book.to_xml).to be_xml_equivalent_to(valid_xml)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context "with the book RNG schema (attribute + optional + oneOrMore + group + choice + empty + data)" do
|
|
77
|
+
before do
|
|
78
|
+
stub_const("RngBookSpec", Module.new)
|
|
79
|
+
described_class.to_models(
|
|
80
|
+
schema,
|
|
81
|
+
output_dir: dir,
|
|
82
|
+
create_files: true,
|
|
83
|
+
module_namespace: "RngBookSpec",
|
|
84
|
+
)
|
|
85
|
+
require File.join(dir, "rngbookspec_registry.rb")
|
|
86
|
+
RngBookSpec.register_all
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
after do
|
|
90
|
+
FileUtils.rm_rf(dir)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
let(:dir) { Dir.mktmpdir }
|
|
94
|
+
let(:schema) { File.read("spec/fixtures/xml/schema/rng/book.rng") }
|
|
95
|
+
|
|
96
|
+
let(:hardcover_xml) do
|
|
97
|
+
<<~XML
|
|
98
|
+
<book isbn="978-0-13-110362-7">
|
|
99
|
+
<title>The C Programming Language</title>
|
|
100
|
+
<author>Brian Kernighan</author>
|
|
101
|
+
<author>Dennis Ritchie</author>
|
|
102
|
+
<publisher>Prentice Hall</publisher>
|
|
103
|
+
<year>1988</year>
|
|
104
|
+
<hardcover/>
|
|
105
|
+
</book>
|
|
106
|
+
XML
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
let(:paperback_xml) do
|
|
110
|
+
<<~XML
|
|
111
|
+
<book isbn="978-0-201-83595-3">
|
|
112
|
+
<title>The Mythical Man-Month</title>
|
|
113
|
+
<subtitle>Essays on Software Engineering</subtitle>
|
|
114
|
+
<author>Frederick P. Brooks Jr.</author>
|
|
115
|
+
<publisher>Addison-Wesley</publisher>
|
|
116
|
+
<year>1995</year>
|
|
117
|
+
<paperback/>
|
|
118
|
+
</book>
|
|
119
|
+
XML
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "compiles <attribute> as :attribute kind with map_attribute" do
|
|
123
|
+
book = RngBookSpec::Book.from_xml(hardcover_xml)
|
|
124
|
+
expect(book.isbn).to eq("978-0-13-110362-7")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "handles <oneOrMore> as a collection attribute" do
|
|
128
|
+
book = RngBookSpec::Book.from_xml(hardcover_xml)
|
|
129
|
+
expect(book.author).to eq(["Brian Kernighan", "Dennis Ritchie"])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "leaves <optional> elements absent when not in the XML" do
|
|
133
|
+
book = RngBookSpec::Book.from_xml(hardcover_xml)
|
|
134
|
+
expect(book.subtitle).to be_nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "reads <optional> elements when present" do
|
|
138
|
+
book = RngBookSpec::Book.from_xml(paperback_xml)
|
|
139
|
+
expect(book.subtitle).to eq("Essays on Software Engineering")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "compiles <data type=\"integer\"/> as :integer (slice 3)" do
|
|
143
|
+
book = RngBookSpec::Book.from_xml(hardcover_xml)
|
|
144
|
+
expect(book.year).to eq(1988)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "compiles <choice> alternatives, with empty-bodied elements as classes" do
|
|
148
|
+
book = RngBookSpec::Book.from_xml(hardcover_xml)
|
|
149
|
+
expect(book.hardcover).to be_a(RngBookSpec::Hardcover)
|
|
150
|
+
expect(book.paperback).to be_nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context "with the person RNG schema (start ref + multi-define + cross-references)" do
|
|
155
|
+
before do
|
|
156
|
+
stub_const("RngPersonSpec", Module.new)
|
|
157
|
+
described_class.to_models(
|
|
158
|
+
schema,
|
|
159
|
+
output_dir: dir,
|
|
160
|
+
create_files: true,
|
|
161
|
+
module_namespace: "RngPersonSpec",
|
|
162
|
+
)
|
|
163
|
+
require File.join(dir, "rngpersonspec_registry.rb")
|
|
164
|
+
RngPersonSpec.register_all
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
after do
|
|
168
|
+
FileUtils.rm_rf(dir)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
let(:dir) { Dir.mktmpdir }
|
|
172
|
+
let(:schema) { File.read("spec/fixtures/xml/schema/rng/person.rng") }
|
|
173
|
+
|
|
174
|
+
let(:xml) do
|
|
175
|
+
<<~XML
|
|
176
|
+
<Person>
|
|
177
|
+
<name>Alice</name>
|
|
178
|
+
<age>30</age>
|
|
179
|
+
<Address>
|
|
180
|
+
<street>1 Main St</street>
|
|
181
|
+
<city>Springfield</city>
|
|
182
|
+
</Address>
|
|
183
|
+
</Person>
|
|
184
|
+
XML
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "generates one class per define" do
|
|
188
|
+
expect(defined?(RngPersonSpec::Person)).to eq("constant")
|
|
189
|
+
expect(defined?(RngPersonSpec::Address)).to eq("constant")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "resolves <ref> as a typed attribute pointing to the referenced class" do
|
|
193
|
+
person = RngPersonSpec::Person.from_xml(xml)
|
|
194
|
+
expect(person.address).to be_a(RngPersonSpec::Address)
|
|
195
|
+
expect(person.address.street).to eq("1 Main St")
|
|
196
|
+
expect(person.address.city).to eq("Springfield")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "round-trips XML through the cross-referenced classes" do
|
|
200
|
+
person = RngPersonSpec::Person.from_xml(xml)
|
|
201
|
+
expect(person.to_xml).to be_xml_equivalent_to(xml)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context "with the integer_range RNG schema (restrictions + enumerations as SimpleType subclasses)" do
|
|
206
|
+
before do
|
|
207
|
+
stub_const("RngRestrictionSpec", Module.new)
|
|
208
|
+
described_class.to_models(
|
|
209
|
+
schema,
|
|
210
|
+
output_dir: dir,
|
|
211
|
+
create_files: true,
|
|
212
|
+
module_namespace: "RngRestrictionSpec",
|
|
213
|
+
)
|
|
214
|
+
require File.join(dir, "rngrestrictionspec_registry.rb")
|
|
215
|
+
RngRestrictionSpec.register_all
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
after do
|
|
219
|
+
FileUtils.rm_rf(dir)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
let(:dir) { Dir.mktmpdir }
|
|
223
|
+
let(:schema) { File.read("spec/fixtures/xml/schema/rng/integer_range.rng") }
|
|
224
|
+
|
|
225
|
+
it "generates a SimpleType subclass for <data> with restrictions" do
|
|
226
|
+
src = File.read(File.join(dir, "rngrestrictionspec", "st_integer_range.rb"))
|
|
227
|
+
expect(src).to include("class StIntegerRange < Lutaml::Model::Type::Integer")
|
|
228
|
+
expect(src).to include("options[:min] = 1")
|
|
229
|
+
expect(src).to include("options[:max] = 255")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "generates a SimpleType subclass for <choice> of <value> enumerations" do
|
|
233
|
+
src = File.read(File.join(dir, "rngrestrictionspec", "st_color.rb"))
|
|
234
|
+
expect(src).to include("class StColor < Lutaml::Model::Type::String")
|
|
235
|
+
expect(src).to include('options[:values] = [super("red"), super("green"), super("blue")]')
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "references SimpleTypes by registered symbol from the consuming class" do
|
|
239
|
+
src = File.read(File.join(dir, "rngrestrictionspec", "thing.rb"))
|
|
240
|
+
expect(src).to include("attribute :val, :st_integer_range")
|
|
241
|
+
expect(src).to include("attribute :color, :st_color")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it "accepts XML that satisfies the restrictions" do
|
|
245
|
+
thing = RngRestrictionSpec::Thing.from_xml(
|
|
246
|
+
'<thing val="42"><color>red</color></thing>',
|
|
247
|
+
)
|
|
248
|
+
expect(thing.val).to eq(42)
|
|
249
|
+
expect(thing.color).to eq("red")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "raises MaxBoundError for out-of-range integers" do
|
|
253
|
+
expect do
|
|
254
|
+
RngRestrictionSpec::Thing.from_xml(
|
|
255
|
+
'<thing val="500"><color>red</color></thing>',
|
|
256
|
+
)
|
|
257
|
+
end.to raise_error(Lutaml::Model::Type::MaxBoundError)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "raises InvalidValueError for non-enum values" do
|
|
261
|
+
expect do
|
|
262
|
+
RngRestrictionSpec::Thing.from_xml(
|
|
263
|
+
'<thing val="42"><color>purple</color></thing>',
|
|
264
|
+
)
|
|
265
|
+
end.to raise_error(Lutaml::Model::Type::InvalidValueError)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
context "with inline restrictions (anonymous SimpleType emission)" do
|
|
270
|
+
let(:rng) do
|
|
271
|
+
<<~RNG
|
|
272
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
273
|
+
<start>
|
|
274
|
+
<element name="game">
|
|
275
|
+
<attribute name="age">
|
|
276
|
+
<data type="integer">
|
|
277
|
+
<param name="minInclusive">3</param>
|
|
278
|
+
<param name="maxInclusive">18</param>
|
|
279
|
+
</data>
|
|
280
|
+
</attribute>
|
|
281
|
+
<element name="rating">
|
|
282
|
+
<choice>
|
|
283
|
+
<value>E</value>
|
|
284
|
+
<value>T</value>
|
|
285
|
+
<value>M</value>
|
|
286
|
+
</choice>
|
|
287
|
+
</element>
|
|
288
|
+
</element>
|
|
289
|
+
</start>
|
|
290
|
+
</grammar>
|
|
291
|
+
RNG
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
let(:sources) { described_class.to_models(rng) }
|
|
295
|
+
|
|
296
|
+
it "generates anonymous SimpleTypes named after the host element/attribute" do
|
|
297
|
+
expect(sources.keys).to contain_exactly("Game", "AgeType", "RatingType")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "references inline restrictions by registered type symbol" do
|
|
301
|
+
expect(sources["Game"]).to include("attribute :age, :age_type")
|
|
302
|
+
expect(sources["Game"]).to include("attribute :rating, :rating_type")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "emits the inline integer restriction as a Type::Integer subclass" do
|
|
306
|
+
expect(sources["AgeType"]).to include("class AgeType < Lutaml::Model::Type::Integer")
|
|
307
|
+
expect(sources["AgeType"]).to include("options[:min] = 3")
|
|
308
|
+
expect(sources["AgeType"]).to include("options[:max] = 18")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it "emits the inline enumeration as a Type::String subclass with values" do
|
|
312
|
+
expect(sources["RatingType"]).to include("class RatingType < Lutaml::Model::Type::String")
|
|
313
|
+
expect(sources["RatingType"]).to include('options[:values] = [super("E"), super("T"), super("M")]')
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
context "with the FullNameType regression fixture from issue #9" do
|
|
318
|
+
let(:sources) do
|
|
319
|
+
described_class.to_models(
|
|
320
|
+
File.read("spec/fixtures/xml/schema/rng/full_name.rng"),
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "preserves document order in the XML mappings" do
|
|
325
|
+
expected_order = %w[
|
|
326
|
+
abbrev prefix forename initials surname addition
|
|
327
|
+
completeName biblionote variantname
|
|
328
|
+
]
|
|
329
|
+
mapping_lines = sources["FullNameType"].lines.grep(/map_element "/)
|
|
330
|
+
actual_order = mapping_lines.map { |l| l[/map_element "(\w+)"/, 1] }
|
|
331
|
+
expect(actual_order).to eq(expected_order)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it "wraps grouped mappings in `sequence do ... end` inside the xml block" do
|
|
335
|
+
# The <group> inside <choice> becomes a sequence wrapper in the XML
|
|
336
|
+
# mapping section (matching XSD compiler's behavior).
|
|
337
|
+
xml_block = sources["FullNameType"][/xml do.*?^ end/m]
|
|
338
|
+
expect(xml_block).to include("sequence do")
|
|
339
|
+
seq_block = xml_block[/sequence do.*?end/m]
|
|
340
|
+
order = seq_block.scan(/map_element "(\w+)"/).flatten
|
|
341
|
+
expect(order).to eq(%w[prefix forename initials surname addition])
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it "emits choice alternatives in document order" do
|
|
345
|
+
# The <choice> contains a <group>(prefix, forename, initials, surname,
|
|
346
|
+
# addition) followed by completeName. After flattening (matching the
|
|
347
|
+
# XSD compiler's behavior), all six appear in document order inside
|
|
348
|
+
# the choice block.
|
|
349
|
+
choice_block = sources["FullNameType"][/choice do.*?end/m]
|
|
350
|
+
expect(choice_block).not_to be_nil
|
|
351
|
+
attrs = choice_block.scan(/attribute :(\w+)/).flatten
|
|
352
|
+
expect(attrs).to eq(
|
|
353
|
+
%w[prefix forename formatted_initials surname addition complete_name],
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
context "with a namespaced grammar (slice 4i)" do
|
|
359
|
+
let(:rng) do
|
|
360
|
+
<<~RNG
|
|
361
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0" ns="http://example.com/books">
|
|
362
|
+
<start>
|
|
363
|
+
<element name="book">
|
|
364
|
+
<element name="title"><text/></element>
|
|
365
|
+
</element>
|
|
366
|
+
</start>
|
|
367
|
+
</grammar>
|
|
368
|
+
RNG
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
let(:sources) { described_class.to_models(rng) }
|
|
372
|
+
|
|
373
|
+
it "generates a XmlNamespace subclass for the grammar ns" do
|
|
374
|
+
ns_name = sources.keys.find { |k| k.include?("Namespace") }
|
|
375
|
+
expect(ns_name).not_to be_nil
|
|
376
|
+
expect(sources[ns_name]).to include("< Lutaml::Xml::W3c::XmlNamespace")
|
|
377
|
+
expect(sources[ns_name]).to include('uri "http://example.com/books"')
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
it "references the namespace class from each generated class" do
|
|
381
|
+
expect(sources["Book"]).to match(/namespace \w+Namespace/)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "adds a require_relative for the namespace class" do
|
|
385
|
+
expect(sources["Book"]).to match(/require_relative ".*namespace"/)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
context "with an element namespaced via prefix (no grammar default ns)" do
|
|
390
|
+
let(:sources) do
|
|
391
|
+
described_class.to_models(<<~RNG)
|
|
392
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
393
|
+
<start>
|
|
394
|
+
<element name="root" ns="urn:test">
|
|
395
|
+
<element name="child" ns="urn:test"><text/></element>
|
|
396
|
+
</element>
|
|
397
|
+
</start>
|
|
398
|
+
</grammar>
|
|
399
|
+
RNG
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it "assigns a class-level namespace to the namespaced element model" do
|
|
403
|
+
expect(sources["Root"]).to match(/namespace \w+Namespace/)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it "generates a XmlNamespace subclass carrying the element ns uri" do
|
|
407
|
+
ns_name = sources.keys.find { |k| k.include?("Namespace") }
|
|
408
|
+
expect(ns_name).not_to be_nil
|
|
409
|
+
expect(sources[ns_name]).to include('uri "urn:test"')
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
context "with a child declaring ns='' (a no-namespace override)" do
|
|
414
|
+
let(:sources) do
|
|
415
|
+
described_class.to_models(<<~RNG)
|
|
416
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0" ns="urn:d">
|
|
417
|
+
<start>
|
|
418
|
+
<element name="root">
|
|
419
|
+
<element name="child" ns=""><text/></element>
|
|
420
|
+
</element>
|
|
421
|
+
</start>
|
|
422
|
+
</grammar>
|
|
423
|
+
RNG
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# lutaml expresses namespaces only at class level, so a per-element
|
|
427
|
+
# ns='' override cannot be represented; the guard's job is only to keep
|
|
428
|
+
# ns='' from registering a bogus empty-URI namespace that would clobber
|
|
429
|
+
# the grammar default. It emits exactly the one grammar-default namespace.
|
|
430
|
+
it "keeps the grammar default namespace uri intact" do
|
|
431
|
+
ns_names = sources.keys.select { |k| k.include?("Namespace") }
|
|
432
|
+
expect(ns_names.size).to eq(1)
|
|
433
|
+
expect(sources[ns_names.first]).to include('uri "urn:d"')
|
|
434
|
+
expect(sources[ns_names.first]).not_to include('uri ""')
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
context "with documentation annotations (slice 4h)" do
|
|
439
|
+
let(:rng) do
|
|
440
|
+
<<~RNG
|
|
441
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0"
|
|
442
|
+
xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0">
|
|
443
|
+
<start>
|
|
444
|
+
<element name="book">
|
|
445
|
+
<a:documentation>A book record with title and author.</a:documentation>
|
|
446
|
+
<element name="title">
|
|
447
|
+
<a:documentation>The book title.</a:documentation>
|
|
448
|
+
<text/>
|
|
449
|
+
</element>
|
|
450
|
+
<element name="author">
|
|
451
|
+
<a:documentation>The book author.</a:documentation>
|
|
452
|
+
<text/>
|
|
453
|
+
</element>
|
|
454
|
+
</element>
|
|
455
|
+
</start>
|
|
456
|
+
</grammar>
|
|
457
|
+
RNG
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
let(:sources) { described_class.to_models(rng) }
|
|
461
|
+
|
|
462
|
+
it "emits class-level documentation as a Ruby comment above the class" do
|
|
463
|
+
expect(sources["Book"]).to include("# A book record with title and author.\nclass Book")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it "emits attribute-level documentation as a Ruby comment above the attribute" do
|
|
467
|
+
expect(sources["Book"]).to include("# The book title.\n attribute :title")
|
|
468
|
+
expect(sources["Book"]).to include("# The book author.\n attribute :author")
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
context "with the paragraph RNG schema (<mixed>)" do
|
|
473
|
+
before do
|
|
474
|
+
stub_const("RngParaSpec", Module.new)
|
|
475
|
+
described_class.to_models(
|
|
476
|
+
schema,
|
|
477
|
+
output_dir: dir,
|
|
478
|
+
create_files: true,
|
|
479
|
+
module_namespace: "RngParaSpec",
|
|
480
|
+
)
|
|
481
|
+
require File.join(dir, "rngparaspec_registry.rb")
|
|
482
|
+
RngParaSpec.register_all
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
after do
|
|
486
|
+
FileUtils.rm_rf(dir)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
let(:dir) { Dir.mktmpdir }
|
|
490
|
+
let(:schema) { File.read("spec/fixtures/xml/schema/rng/paragraph.rng") }
|
|
491
|
+
|
|
492
|
+
it "emits mixed_content for the <mixed> wrapper" do
|
|
493
|
+
para_source = File.read(File.join(dir, "rngparaspec", "para.rb"))
|
|
494
|
+
expect(para_source).to include("mixed_content")
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
context "when classes are generated but files are not created" do
|
|
499
|
+
let(:classes_hash) do
|
|
500
|
+
described_class.to_models(
|
|
501
|
+
File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it "returns a Hash of class_name => Ruby source" do
|
|
506
|
+
expect(classes_hash).to be_a(Hash)
|
|
507
|
+
expect(classes_hash.keys).to contain_exactly(
|
|
508
|
+
"AddressBook", "Card", "CardContent"
|
|
509
|
+
)
|
|
510
|
+
classes_hash.each_value { |src| expect(src).to start_with("# frozen_string_literal: true") }
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
context "round-trip with Lutaml::Xml::Schema::RelaxngSchema.generate" do
|
|
515
|
+
before do
|
|
516
|
+
stub_const("RngRtAddress", Class.new(Lutaml::Model::Serializable) do
|
|
517
|
+
attribute :street, :string
|
|
518
|
+
attribute :city, :string
|
|
519
|
+
|
|
520
|
+
xml do
|
|
521
|
+
element "RngRtAddress"
|
|
522
|
+
map_element "street", to: :street
|
|
523
|
+
map_element "city", to: :city
|
|
524
|
+
end
|
|
525
|
+
end)
|
|
526
|
+
|
|
527
|
+
stub_const("RngRtPerson", Class.new(Lutaml::Model::Serializable) do
|
|
528
|
+
attribute :name, :string
|
|
529
|
+
attribute :age, :integer
|
|
530
|
+
attribute :address, RngRtAddress
|
|
531
|
+
|
|
532
|
+
xml do
|
|
533
|
+
element "RngRtPerson"
|
|
534
|
+
map_element "name", to: :name
|
|
535
|
+
map_element "age", to: :age
|
|
536
|
+
map_element "RngRtAddress", to: :address
|
|
537
|
+
end
|
|
538
|
+
end)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
it "round-trips primitive-typed attributes through generate + compile" do
|
|
542
|
+
rng = Lutaml::Xml::Schema::RelaxngSchema.generate(RngRtPerson)
|
|
543
|
+
sources = described_class.to_models(rng)
|
|
544
|
+
|
|
545
|
+
expect(sources["RngRtPerson"]).to include("attribute :name, :string")
|
|
546
|
+
expect(sources["RngRtPerson"]).to include("attribute :age, :integer")
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
it "round-trips typed reference attributes" do
|
|
550
|
+
rng = Lutaml::Xml::Schema::RelaxngSchema.generate(RngRtPerson)
|
|
551
|
+
sources = described_class.to_models(rng)
|
|
552
|
+
|
|
553
|
+
expect(sources["RngRtPerson"]).to include(
|
|
554
|
+
"attribute :rng_rt_address, RngRtAddress",
|
|
555
|
+
)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
context "e2e: namespaced grammar loads + parses namespaced XML" do
|
|
560
|
+
before do
|
|
561
|
+
stub_const("RngNamespacedSpec", Module.new)
|
|
562
|
+
described_class.to_models(
|
|
563
|
+
File.read("spec/fixtures/xml/schema/rng/namespaced.rng"),
|
|
564
|
+
output_dir: dir,
|
|
565
|
+
create_files: true,
|
|
566
|
+
module_namespace: "RngNamespacedSpec",
|
|
567
|
+
)
|
|
568
|
+
require File.join(dir, "rngnamespacedspec_registry.rb")
|
|
569
|
+
RngNamespacedSpec.register_all
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
after do
|
|
573
|
+
FileUtils.rm_rf(dir)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
let(:dir) { Dir.mktmpdir }
|
|
577
|
+
let(:xml) do
|
|
578
|
+
<<~XML
|
|
579
|
+
<book xmlns="http://example.com/books" isbn="978-0-13-110362-7">
|
|
580
|
+
<title>K&R C</title>
|
|
581
|
+
</book>
|
|
582
|
+
XML
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
it "generates a namespace class and uses it in the generated model" do
|
|
586
|
+
expect(defined?(RngNamespacedSpec::ComExampleBooksNamespace)).to eq("constant")
|
|
587
|
+
expect(defined?(RngNamespacedSpec::Book)).to eq("constant")
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it "parses namespaced XML through the generated classes" do
|
|
591
|
+
book = RngNamespacedSpec::Book.from_xml(xml)
|
|
592
|
+
expect(book.isbn).to eq("978-0-13-110362-7")
|
|
593
|
+
expect(book.title).to eq("K&R C")
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it "emits XML in the declared namespace on the root element" do
|
|
597
|
+
book = RngNamespacedSpec::Book.from_xml(xml)
|
|
598
|
+
expect(book.to_xml).to include('xmlns="http://example.com/books"')
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
context "e2e: mixed content loads + parses mixed XML" do
|
|
603
|
+
before do
|
|
604
|
+
stub_const("RngMixedSpec", Module.new)
|
|
605
|
+
described_class.to_models(
|
|
606
|
+
File.read("spec/fixtures/xml/schema/rng/paragraph.rng"),
|
|
607
|
+
output_dir: dir,
|
|
608
|
+
create_files: true,
|
|
609
|
+
module_namespace: "RngMixedSpec",
|
|
610
|
+
)
|
|
611
|
+
require File.join(dir, "rngmixedspec_registry.rb")
|
|
612
|
+
RngMixedSpec.register_all
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
after do
|
|
616
|
+
FileUtils.rm_rf(dir)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
let(:dir) { Dir.mktmpdir }
|
|
620
|
+
|
|
621
|
+
it "loads a Para class with mixed_content enabled" do
|
|
622
|
+
expect(defined?(RngMixedSpec::Para)).to eq("constant")
|
|
623
|
+
para = RngMixedSpec::Para.from_xml(
|
|
624
|
+
"<para>Hello <emph>world</emph>!</para>",
|
|
625
|
+
)
|
|
626
|
+
expect(para).not_to be_nil
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
context "e2e: inline restrictions enforce constraints at parse time" do
|
|
631
|
+
before do
|
|
632
|
+
stub_const("RngInlineSpec", Module.new)
|
|
633
|
+
described_class.to_models(
|
|
634
|
+
rng,
|
|
635
|
+
output_dir: dir,
|
|
636
|
+
create_files: true,
|
|
637
|
+
module_namespace: "RngInlineSpec",
|
|
638
|
+
)
|
|
639
|
+
require File.join(dir, "rnginlinespec_registry.rb")
|
|
640
|
+
RngInlineSpec.register_all
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
after do
|
|
644
|
+
FileUtils.rm_rf(dir)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
let(:dir) { Dir.mktmpdir }
|
|
648
|
+
let(:rng) do
|
|
649
|
+
<<~RNG
|
|
650
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
651
|
+
<start>
|
|
652
|
+
<element name="game">
|
|
653
|
+
<attribute name="age">
|
|
654
|
+
<data type="integer">
|
|
655
|
+
<param name="minInclusive">3</param>
|
|
656
|
+
<param name="maxInclusive">18</param>
|
|
657
|
+
</data>
|
|
658
|
+
</attribute>
|
|
659
|
+
<element name="rating">
|
|
660
|
+
<choice>
|
|
661
|
+
<value>E</value>
|
|
662
|
+
<value>T</value>
|
|
663
|
+
<value>M</value>
|
|
664
|
+
</choice>
|
|
665
|
+
</element>
|
|
666
|
+
</element>
|
|
667
|
+
</start>
|
|
668
|
+
</grammar>
|
|
669
|
+
RNG
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
it "loads anonymous Type::Integer and Type::String subclasses" do
|
|
673
|
+
expect(defined?(RngInlineSpec::AgeType)).to eq("constant")
|
|
674
|
+
expect(defined?(RngInlineSpec::RatingType)).to eq("constant")
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
it "accepts XML satisfying the inline integer + enum restrictions" do
|
|
678
|
+
game = RngInlineSpec::Game.from_xml(
|
|
679
|
+
'<game age="12"><rating>T</rating></game>',
|
|
680
|
+
)
|
|
681
|
+
expect(game.age).to eq(12)
|
|
682
|
+
expect(game.rating).to eq("T")
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
it "rejects XML violating the inline integer minInclusive" do
|
|
686
|
+
expect do
|
|
687
|
+
RngInlineSpec::Game.from_xml('<game age="1"><rating>T</rating></game>')
|
|
688
|
+
end.to raise_error(Lutaml::Model::Type::MinBoundError)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
it "rejects XML with a value outside the inline enumeration" do
|
|
692
|
+
expect do
|
|
693
|
+
RngInlineSpec::Game.from_xml(
|
|
694
|
+
'<game age="12"><rating>X</rating></game>',
|
|
695
|
+
)
|
|
696
|
+
end.to raise_error(Lutaml::Model::Type::InvalidValueError)
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
context "e2e: FullNameType (issue #9) loads + round-trips XML" do
|
|
701
|
+
before do
|
|
702
|
+
stub_const("RngFullNameSpec", Module.new)
|
|
703
|
+
described_class.to_models(
|
|
704
|
+
File.read("spec/fixtures/xml/schema/rng/full_name.rng"),
|
|
705
|
+
output_dir: dir,
|
|
706
|
+
create_files: true,
|
|
707
|
+
module_namespace: "RngFullNameSpec",
|
|
708
|
+
)
|
|
709
|
+
require File.join(dir, "rngfullnamespec_registry.rb")
|
|
710
|
+
RngFullNameSpec.register_all
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
after do
|
|
714
|
+
FileUtils.rm_rf(dir)
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
let(:dir) { Dir.mktmpdir }
|
|
718
|
+
|
|
719
|
+
it "loads the FullNameType class with all referenced defines" do
|
|
720
|
+
%w[FullNameType NameAbbreviation Prefix Forename FormattedInitials
|
|
721
|
+
Surname Addition CompleteName Biblionote Variantname].each do |c|
|
|
722
|
+
expect(RngFullNameSpec.const_defined?(c)).to be(true), "missing #{c}"
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it "parses XML using the group/choice/zeroOrMore structure" do
|
|
727
|
+
xml = <<~XML
|
|
728
|
+
<fullname>
|
|
729
|
+
<abbrev>Dr.</abbrev>
|
|
730
|
+
<prefix>von</prefix>
|
|
731
|
+
<forename>Alice</forename>
|
|
732
|
+
<surname>Smith</surname>
|
|
733
|
+
<biblionote>note 1</biblionote>
|
|
734
|
+
</fullname>
|
|
735
|
+
XML
|
|
736
|
+
obj = RngFullNameSpec::FullNameType.from_xml(xml)
|
|
737
|
+
expect(obj.name_abbreviation.content).to eq("Dr.")
|
|
738
|
+
expect(obj.prefix.first.content).to eq("von")
|
|
739
|
+
expect(obj.forename.first.content).to eq("Alice")
|
|
740
|
+
expect(obj.surname.content).to eq("Smith")
|
|
741
|
+
expect(obj.biblionote.first.content).to eq("note 1")
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
context "e2e: union types load + cast across member types" do
|
|
746
|
+
before do
|
|
747
|
+
stub_const("RngUnionSpec", Module.new)
|
|
748
|
+
described_class.to_models(
|
|
749
|
+
File.read("spec/fixtures/xml/schema/rng/union.rng"),
|
|
750
|
+
output_dir: dir,
|
|
751
|
+
create_files: true,
|
|
752
|
+
module_namespace: "RngUnionSpec",
|
|
753
|
+
)
|
|
754
|
+
require File.join(dir, "rngunionspec_registry.rb")
|
|
755
|
+
RngUnionSpec.register_all
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
after do
|
|
759
|
+
FileUtils.rm_rf(dir)
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
let(:dir) { Dir.mktmpdir }
|
|
763
|
+
|
|
764
|
+
it "generates a Lutaml::Model::Type::Value subclass for the union define" do
|
|
765
|
+
expect(defined?(RngUnionSpec::IntOrString)).to eq("constant")
|
|
766
|
+
expect(RngUnionSpec::IntOrString.ancestors).to include(Lutaml::Model::Type::Value)
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it "casts integer-shaped values to Integer" do
|
|
770
|
+
thing = RngUnionSpec::Thing.from_xml("<thing><count>42</count></thing>")
|
|
771
|
+
expect(thing.count).to eq(42)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it "casts non-integer values as String" do
|
|
775
|
+
thing = RngUnionSpec::Thing.from_xml("<thing><count>hello</count></thing>")
|
|
776
|
+
expect(thing.count).to eq("hello")
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
context "e2e: fixed-value attribute uses default + round-trips" do
|
|
781
|
+
before do
|
|
782
|
+
stub_const("RngFixedSpec", Module.new)
|
|
783
|
+
described_class.to_models(
|
|
784
|
+
File.read("spec/fixtures/xml/schema/rng/fixed_value.rng"),
|
|
785
|
+
output_dir: dir,
|
|
786
|
+
create_files: true,
|
|
787
|
+
module_namespace: "RngFixedSpec",
|
|
788
|
+
)
|
|
789
|
+
require File.join(dir, "rngfixedspec_registry.rb")
|
|
790
|
+
RngFixedSpec.register_all
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
after do
|
|
794
|
+
FileUtils.rm_rf(dir)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
let(:dir) { Dir.mktmpdir }
|
|
798
|
+
|
|
799
|
+
it "applies the default value when the attribute is omitted from XML" do
|
|
800
|
+
thing = RngFixedSpec::Thing.from_xml("<thing><name>X</name></thing>")
|
|
801
|
+
expect(thing.version).to eq("1.0")
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
it "preserves an explicit attribute value" do
|
|
805
|
+
thing = RngFixedSpec::Thing.from_xml(
|
|
806
|
+
'<thing version="1.0"><name>X</name></thing>',
|
|
807
|
+
)
|
|
808
|
+
expect(thing.version).to eq("1.0")
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
context "e2e: load_classes: true (no files) loads classes into a tmp module" do
|
|
813
|
+
let!(:result) do
|
|
814
|
+
stub_const("RngLoadClassesSpec", Module.new)
|
|
815
|
+
described_class.to_models(
|
|
816
|
+
File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
|
|
817
|
+
load_classes: true,
|
|
818
|
+
module_namespace: "RngLoadClassesSpec",
|
|
819
|
+
)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
it "returns generated source per class" do
|
|
823
|
+
expect(result).to be_a(Hash)
|
|
824
|
+
expect(result.keys).to contain_exactly("AddressBook", "Card", "CardContent")
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
it "loads the namespaced module and registers classes" do
|
|
828
|
+
expect(defined?(RngLoadClassesSpec::AddressBook)).to eq("constant")
|
|
829
|
+
expect(defined?(RngLoadClassesSpec::Card)).to eq("constant")
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
it "the loaded classes can parse XML end-to-end" do
|
|
833
|
+
book = RngLoadClassesSpec::AddressBook.from_xml(
|
|
834
|
+
"<addressBook><card><name>X</name><email>x@y</email></card></addressBook>",
|
|
835
|
+
)
|
|
836
|
+
expect(book.card.first.name).to eq("X")
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
context "e2e: list type compiles + tolerates whitespace-separated XML" do
|
|
841
|
+
let(:dir) { Dir.mktmpdir }
|
|
842
|
+
|
|
843
|
+
before do
|
|
844
|
+
stub_const("RngListSpec", Module.new)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
after do
|
|
848
|
+
FileUtils.rm_rf(dir)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
it "compiles a grammar with <list> without crashing" do
|
|
852
|
+
expect do
|
|
853
|
+
described_class.to_models(
|
|
854
|
+
File.read("spec/fixtures/xml/schema/rng/list_type.rng"),
|
|
855
|
+
output_dir: dir,
|
|
856
|
+
create_files: true,
|
|
857
|
+
module_namespace: "RngListSpec",
|
|
858
|
+
)
|
|
859
|
+
require File.join(dir, "rnglistspec_registry.rb")
|
|
860
|
+
RngListSpec.register_all
|
|
861
|
+
end.not_to raise_error
|
|
862
|
+
expect(defined?(RngListSpec::Tags)).to eq("constant")
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
context "with a ref to a fragment define nested inside a choice" do
|
|
867
|
+
# Regression for the import-drop bug: handle_choice (and handle_group)
|
|
868
|
+
# collected imports onto a transient MemberCollector and then discarded
|
|
869
|
+
# them, so the generated container never emitted `import_model` /
|
|
870
|
+
# `require_relative` for the fragment ref.
|
|
871
|
+
let(:sources) do
|
|
872
|
+
described_class.to_models(
|
|
873
|
+
File.read("spec/fixtures/xml/schema/rng/fragment_in_choice.rng"),
|
|
874
|
+
)
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
it "emits import_model and require_relative for the fragment ref" do
|
|
878
|
+
expect(sources["Container"]).to include("import_model FragmentThing")
|
|
879
|
+
expect(sources["Container"]).to include(%(require_relative "fragment_thing"))
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
context "via Schema.from_relaxng entry point" do
|
|
884
|
+
it "delegates through the registered method" do
|
|
885
|
+
result = Lutaml::Model::Schema.from_relaxng(
|
|
886
|
+
File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
|
|
887
|
+
)
|
|
888
|
+
expect(result).to be_a(Hash)
|
|
889
|
+
expect(result.keys).to contain_exactly("AddressBook", "Card", "CardContent")
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
end
|