lutaml-model 0.8.17 → 0.8.19
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/builder-dsl.adoc +127 -0
- 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 +23 -11
- data/lib/lutaml/model/serialize/builder.rb +70 -2
- data/lib/lutaml/model/serialize/initialization.rb +3 -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 +104 -4
- 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 +41 -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/serialize/builder_spec.rb +358 -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 +99 -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,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Multi-define grammar with cross-references. Exercises the "one class per
|
|
4
|
+
define" path with a ref-from-start and nested element references.
|
|
5
|
+
-->
|
|
6
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
|
|
7
|
+
<start>
|
|
8
|
+
<ref name="Person"/>
|
|
9
|
+
</start>
|
|
10
|
+
<define name="Person">
|
|
11
|
+
<element name="Person">
|
|
12
|
+
<element name="name"><data type="string"/></element>
|
|
13
|
+
<element name="age"><data type="integer"/></element>
|
|
14
|
+
<ref name="Address"/>
|
|
15
|
+
</element>
|
|
16
|
+
</define>
|
|
17
|
+
<define name="Address">
|
|
18
|
+
<element name="Address">
|
|
19
|
+
<element name="street"><data type="string"/></element>
|
|
20
|
+
<element name="city"><data type="string"/></element>
|
|
21
|
+
</element>
|
|
22
|
+
</define>
|
|
23
|
+
</grammar>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
|
|
3
|
+
<start>
|
|
4
|
+
<element name="thing">
|
|
5
|
+
<element name="count"><ref name="IntOrString"/></element>
|
|
6
|
+
</element>
|
|
7
|
+
</start>
|
|
8
|
+
<define name="IntOrString">
|
|
9
|
+
<choice>
|
|
10
|
+
<data type="integer"/>
|
|
11
|
+
<data type="string"/>
|
|
12
|
+
</choice>
|
|
13
|
+
</define>
|
|
14
|
+
</grammar>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
3
|
+
xmlns="http://example.com/address"
|
|
4
|
+
targetNamespace="http://example.com/address"
|
|
5
|
+
elementFormDefault="qualified">
|
|
6
|
+
<xs:element name="address">
|
|
7
|
+
<xs:complexType>
|
|
8
|
+
<xs:sequence>
|
|
9
|
+
<xs:element name="city" type="xs:string"/>
|
|
10
|
+
</xs:sequence>
|
|
11
|
+
</xs:complexType>
|
|
12
|
+
</xs:element>
|
|
13
|
+
</xs:schema>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
3
|
+
<xs:element name="contact">
|
|
4
|
+
<xs:complexType>
|
|
5
|
+
<xs:choice>
|
|
6
|
+
<xs:element name="email" type="xs:string"/>
|
|
7
|
+
<xs:element name="phone" type="xs:string"/>
|
|
8
|
+
</xs:choice>
|
|
9
|
+
</xs:complexType>
|
|
10
|
+
</xs:element>
|
|
11
|
+
</xs:schema>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
3
|
+
<xs:element name="person">
|
|
4
|
+
<xs:complexType>
|
|
5
|
+
<xs:sequence>
|
|
6
|
+
<xs:element name="name" type="xs:string"/>
|
|
7
|
+
<xs:element name="age" type="xs:integer"/>
|
|
8
|
+
</xs:sequence>
|
|
9
|
+
</xs:complexType>
|
|
10
|
+
</xs:element>
|
|
11
|
+
</xs:schema>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
3
|
+
<xs:element name="person">
|
|
4
|
+
<xs:complexType>
|
|
5
|
+
<xs:sequence>
|
|
6
|
+
<xs:element name="name" type="xs:string"/>
|
|
7
|
+
<xs:element name="age">
|
|
8
|
+
<xs:simpleType>
|
|
9
|
+
<xs:restriction base="xs:integer">
|
|
10
|
+
<xs:maxInclusive value="150"/>
|
|
11
|
+
</xs:restriction>
|
|
12
|
+
</xs:simpleType>
|
|
13
|
+
</xs:element>
|
|
14
|
+
</xs:sequence>
|
|
15
|
+
</xs:complexType>
|
|
16
|
+
</xs:element>
|
|
17
|
+
</xs:schema>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
3
|
+
<xs:element name="product">
|
|
4
|
+
<xs:complexType>
|
|
5
|
+
<xs:sequence>
|
|
6
|
+
<xs:element name="sku">
|
|
7
|
+
<xs:simpleType>
|
|
8
|
+
<xs:restriction base="xs:string">
|
|
9
|
+
<xs:pattern value="[A-Z]{3}-[0-9]{4}"/>
|
|
10
|
+
</xs:restriction>
|
|
11
|
+
</xs:simpleType>
|
|
12
|
+
</xs:element>
|
|
13
|
+
<xs:element name="price" type="xs:decimal"/>
|
|
14
|
+
<xs:element name="released" type="xs:date"/>
|
|
15
|
+
<xs:element name="tag" type="xs:string" minOccurs="0" maxOccurs="3"/>
|
|
16
|
+
<xs:element name="note" type="xs:string" minOccurs="0"/>
|
|
17
|
+
<xs:element name="dimensions" minOccurs="0">
|
|
18
|
+
<xs:complexType>
|
|
19
|
+
<xs:sequence>
|
|
20
|
+
<xs:element name="width" type="xs:integer"/>
|
|
21
|
+
<xs:element name="height" type="xs:integer"/>
|
|
22
|
+
</xs:sequence>
|
|
23
|
+
</xs:complexType>
|
|
24
|
+
</xs:element>
|
|
25
|
+
</xs:sequence>
|
|
26
|
+
<xs:attribute name="id" type="xs:string" use="required"/>
|
|
27
|
+
<xs:attribute name="status">
|
|
28
|
+
<xs:simpleType>
|
|
29
|
+
<xs:restriction base="xs:string">
|
|
30
|
+
<xs:enumeration value="draft"/>
|
|
31
|
+
<xs:enumeration value="published"/>
|
|
32
|
+
</xs:restriction>
|
|
33
|
+
</xs:simpleType>
|
|
34
|
+
</xs:attribute>
|
|
35
|
+
</xs:complexType>
|
|
36
|
+
</xs:element>
|
|
37
|
+
</xs:schema>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
4
|
+
def attribute(type, **opts)
|
|
5
|
+
described_class.new("attr", type, **opts)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:uninit) { Lutaml::Model::UninitializedClass.instance }
|
|
9
|
+
|
|
10
|
+
describe "nil handling" do
|
|
11
|
+
it "maps nil through value_map[:nil] => :nil" do
|
|
12
|
+
expect(attribute(:string).apply_value_map(nil, { nil: :nil })).to be_nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "maps nil through value_map[:nil] => :empty to '' (scalar)" do
|
|
16
|
+
expect(attribute(:string).apply_value_map(nil, { nil: :empty })).to eq("")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "maps nil through value_map[:nil] => :empty to [] (collection)" do
|
|
20
|
+
expect(
|
|
21
|
+
attribute(:string, collection: true).apply_value_map(nil, { nil: :empty }),
|
|
22
|
+
).to eq([])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "maps nil through value_map[:nil] => :empty to a custom collection class instance" do
|
|
26
|
+
# Regression: pre-consolidation Serialize::ValueMapping#empty_object used
|
|
27
|
+
# attr.build_collection (preserving custom collection class). The
|
|
28
|
+
# consolidated Attribute#apply_value_map must also use build_collection,
|
|
29
|
+
# not literal [], so attributes typed with a Lutaml::Model::Collection
|
|
30
|
+
# subclass don't silently downgrade to plain Array during model init.
|
|
31
|
+
member_class = Class.new(Lutaml::Model::Serializable) do
|
|
32
|
+
attribute :name, :string
|
|
33
|
+
end
|
|
34
|
+
custom_collection_class = Class.new(Lutaml::Model::Collection) do
|
|
35
|
+
instances :items, member_class
|
|
36
|
+
end
|
|
37
|
+
attr = described_class.new("items", member_class, collection: custom_collection_class)
|
|
38
|
+
result = attr.apply_value_map(nil, { nil: :empty })
|
|
39
|
+
expect(result).to be_a(custom_collection_class)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "maps nil through value_map[:nil] => :omitted to UninitializedClass" do
|
|
43
|
+
expect(attribute(:string).apply_value_map(nil, { nil: :omitted })).to eq(uninit)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "empty handling" do
|
|
48
|
+
it "maps '' through value_map[:empty] => :nil" do
|
|
49
|
+
expect(attribute(:string).apply_value_map("", { empty: :nil })).to be_nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "maps '' through value_map[:empty] => :empty preserving '' for scalar" do
|
|
53
|
+
expect(attribute(:string).apply_value_map("", { empty: :empty })).to eq("")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "maps [] through value_map[:empty] => :empty preserving [] for collection" do
|
|
57
|
+
expect(
|
|
58
|
+
attribute(:string, collection: true).apply_value_map([], { empty: :empty }),
|
|
59
|
+
).to eq([])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "maps {} through value_map[:empty] => :empty preserving {} for Hash attribute" do
|
|
63
|
+
# Regression: must preserve the input type when source key matches the
|
|
64
|
+
# :empty mapping. Original value_for_option used `empty_value ||
|
|
65
|
+
# empty_object(attr)` which returned the truthy {} unchanged.
|
|
66
|
+
expect(attribute(:hash).apply_value_map({}, { empty: :empty })).to eq({})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "omitted handling" do
|
|
71
|
+
it "maps UninitializedClass through value_map[:omitted] => :nil" do
|
|
72
|
+
expect(attribute(:string).apply_value_map(uninit, { omitted: :nil })).to be_nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "maps UninitializedClass through value_map[:omitted] => :empty" do
|
|
76
|
+
expect(attribute(:string).apply_value_map(uninit, { omitted: :empty })).to eq("")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "does NOT route UninitializedClass through the :empty branch" do
|
|
80
|
+
# Regression: Utils.empty?(UninitializedClass.instance) is false per
|
|
81
|
+
# utils.rb:132-137. Ensure dispatch order matches the originals: uninit
|
|
82
|
+
# values go to :omitted, not :empty.
|
|
83
|
+
expect(
|
|
84
|
+
attribute(:string).apply_value_map(uninit, { empty: :nil, omitted: :empty }),
|
|
85
|
+
).to eq("")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe "Boolean bare-form (Boolean attribute type required)" do
|
|
90
|
+
it "returns false directly for Boolean attribute, value_map[:empty] = false" do
|
|
91
|
+
expect(attribute(:boolean).apply_value_map("", { empty: false })).to be(false)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "returns true directly for Boolean attribute, value_map[:omitted] = true" do
|
|
95
|
+
expect(attribute(:boolean).apply_value_map(uninit, { omitted: true })).to be(true)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "does NOT apply bare-boolean form to a non-Boolean attribute (documented tightening)" do
|
|
99
|
+
# Plan §"Hidden behavior change" — bare-boolean form requires Boolean type.
|
|
100
|
+
# Falls through to the :empty option dispatch, which for an unknown option
|
|
101
|
+
# returns UninitializedClass.instance.
|
|
102
|
+
result = attribute(:string).apply_value_map("", { empty: false })
|
|
103
|
+
expect(result).to eq(uninit)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe "Boolean nested-form (value_map[:from][:empty] / [:omitted])" do
|
|
108
|
+
it "returns false from value_map[:from][:empty] unconditionally for Boolean attribute" do
|
|
109
|
+
expect(attribute(:boolean).apply_value_map("", { from: { empty: false } })).to be(false)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "returns true from value_map[:from][:omitted] unconditionally for Boolean attribute" do
|
|
113
|
+
expect(attribute(:boolean).apply_value_map(uninit, { from: { omitted: true } })).to be(true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "returns false from nested form even for non-Boolean attribute (no type gate)" do
|
|
117
|
+
expect(attribute(:string).apply_value_map("", { from: { empty: false } })).to be(false)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "pass-through" do
|
|
122
|
+
it "returns the value untouched when present and non-empty" do
|
|
123
|
+
expect(attribute(:string).apply_value_map("hello", {})).to eq("hello")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "returns Boolean false untouched (real value, not 'empty')" do
|
|
127
|
+
expect(attribute(:boolean).apply_value_map(false, {})).to be(false)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "returns Boolean true untouched" do
|
|
131
|
+
expect(attribute(:boolean).apply_value_map(true, {})).to be(true)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -216,9 +216,19 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
216
216
|
age_attr.send(:validate_options!, { pattern: /[A-Za-z ]/ })
|
|
217
217
|
end.to raise_error(
|
|
218
218
|
StandardError,
|
|
219
|
-
"Invalid option `pattern` given for `age`, `pattern` is only
|
|
219
|
+
"Invalid option `pattern` given for `age`, `pattern` is only " \
|
|
220
|
+
"allowed for :string type or a union including a :string member",
|
|
220
221
|
)
|
|
221
222
|
end
|
|
223
|
+
|
|
224
|
+
it "allows pattern on a union that includes a :string member" do
|
|
225
|
+
attr = described_class.new(
|
|
226
|
+
"code", Lutaml::Model::Type::Union,
|
|
227
|
+
union_member_types: %i[integer string], pattern: /\A\d+\z/
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
expect(attr.pattern).to eq(/\A\d+\z/)
|
|
231
|
+
end
|
|
222
232
|
end
|
|
223
233
|
|
|
224
234
|
describe "#derived?" do
|
|
@@ -249,46 +259,193 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
249
259
|
end
|
|
250
260
|
end
|
|
251
261
|
|
|
252
|
-
describe "#default
|
|
262
|
+
describe "#default" do
|
|
253
263
|
let(:register) { Lutaml::Model::Config.default_register }
|
|
254
|
-
|
|
255
|
-
def create_resolver(attribute)
|
|
256
|
-
Lutaml::Model::Services::DefaultValueResolver.new(attribute, register,
|
|
257
|
-
nil)
|
|
258
|
-
end
|
|
264
|
+
let(:instance) { nil }
|
|
259
265
|
|
|
260
266
|
context "when default is not set" do
|
|
261
267
|
let(:attribute) { described_class.new("name", :string) }
|
|
262
268
|
|
|
263
269
|
it "returns uninitialized" do
|
|
264
|
-
expect(
|
|
270
|
+
expect(attribute.default(register, instance)).to be(Lutaml::Model::UninitializedClass.instance)
|
|
265
271
|
end
|
|
266
272
|
end
|
|
267
273
|
|
|
268
274
|
context "when default is set as a proc" do
|
|
269
275
|
it "returns the value" do
|
|
270
276
|
attribute = described_class.new("name", :string, default: -> { "John" })
|
|
271
|
-
expect(
|
|
277
|
+
expect(attribute.default(register, instance)).to eq("John")
|
|
272
278
|
end
|
|
273
279
|
|
|
274
280
|
it "returns the value casted to correct type" do
|
|
275
281
|
file = Pathname.new("avatar.png")
|
|
276
282
|
attribute = described_class.new("image", :string, default: -> { file })
|
|
277
283
|
|
|
278
|
-
expect(
|
|
284
|
+
expect(attribute.default(register, instance)).to eq("avatar.png")
|
|
279
285
|
end
|
|
280
286
|
end
|
|
281
287
|
|
|
282
288
|
context "when default is set as value" do
|
|
283
289
|
it "returns the value" do
|
|
284
290
|
attribute = described_class.new("name", :string, default: "John Doe")
|
|
285
|
-
expect(
|
|
291
|
+
expect(attribute.default(register, instance)).to eq("John Doe")
|
|
286
292
|
end
|
|
287
293
|
|
|
288
294
|
it "returns the value casted to correct type" do
|
|
289
295
|
attribute = described_class.new("age", :integer, default: "24")
|
|
290
|
-
expect(
|
|
296
|
+
expect(attribute.default(register, instance)).to eq(24)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "casts a String default to Integer for :integer attribute" do
|
|
300
|
+
attribute = described_class.new("count", :integer, default: "42")
|
|
301
|
+
expect(attribute.default(register, instance)).to eq(42)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
context "with instance context" do
|
|
306
|
+
# Exercise instance_exec: the proc reads `name` from the instance,
|
|
307
|
+
# which is only resolvable when the proc is executed in the
|
|
308
|
+
# instance's context (`instance_object.instance_exec(&proc)`).
|
|
309
|
+
let(:model_class) do
|
|
310
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
311
|
+
attribute :name, :string
|
|
312
|
+
attribute :greeting, :string, default: -> { "Hello, #{name}" }
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it "executes and casts the proc in instance context" do
|
|
317
|
+
instance = model_class.new(name: "Alice")
|
|
318
|
+
attribute = model_class.attributes[:greeting]
|
|
319
|
+
expect(attribute.default(register, instance)).to eq("Hello, Alice")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "bypasses the default cache when instance_object is given" do
|
|
323
|
+
attribute = model_class.attributes[:greeting]
|
|
324
|
+
first = attribute.default(register, model_class.new(name: "Alice"))
|
|
325
|
+
second = attribute.default(register, model_class.new(name: "Bob"))
|
|
326
|
+
expect(first).to eq("Hello, Alice")
|
|
327
|
+
expect(second).to eq("Hello, Bob")
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
describe "#default_value" do
|
|
333
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
334
|
+
let(:instance) { nil }
|
|
335
|
+
|
|
336
|
+
context "when default is a static value" do
|
|
337
|
+
it "returns the static value uncast" do
|
|
338
|
+
attribute = described_class.new("name", :string, default: "John")
|
|
339
|
+
expect(attribute.default_value(register, instance)).to eq("John")
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
context "when default is a proc without instance context" do
|
|
344
|
+
it "executes the proc" do
|
|
345
|
+
attribute = described_class.new("count", :integer, default: -> { 42 })
|
|
346
|
+
expect(attribute.default_value(register, instance)).to eq(42)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
context "when default is a proc with instance context" do
|
|
351
|
+
let(:model_class) do
|
|
352
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
353
|
+
attribute :name, :string
|
|
354
|
+
attribute :greeting, :string, default: -> { "Hello, #{name}" }
|
|
355
|
+
end
|
|
291
356
|
end
|
|
357
|
+
|
|
358
|
+
it "executes the proc in the instance context (reads instance attribute)" do
|
|
359
|
+
instance = model_class.new(name: "Alice")
|
|
360
|
+
attribute = model_class.attributes[:greeting]
|
|
361
|
+
expect(attribute.default_value(register, instance)).to eq("Hello, Alice")
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
describe "#default_set?" do
|
|
367
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
368
|
+
let(:instance) { nil }
|
|
369
|
+
|
|
370
|
+
it "returns false when default is not set" do
|
|
371
|
+
attribute = described_class.new("name", :string)
|
|
372
|
+
expect(attribute.default_set?(register, instance)).to be(false)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it "returns true when default is a static value" do
|
|
376
|
+
attribute = described_class.new("name", :string, default: "John")
|
|
377
|
+
expect(attribute.default_set?(register, instance)).to be(true)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
it "returns true when default is a proc" do
|
|
381
|
+
attribute = described_class.new("name", :string, default: -> { "John" })
|
|
382
|
+
expect(attribute.default_set?(register, instance)).to be(true)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
describe "#validate_value!" do
|
|
387
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
388
|
+
let(:model) { Class.new(Lutaml::Model::Serializable).new }
|
|
389
|
+
|
|
390
|
+
context "when value is nil and the attribute has no default" do
|
|
391
|
+
it "treats nil-no-default as a permissible non-required value" do
|
|
392
|
+
attr = described_class.new("name", :string)
|
|
393
|
+
expect { attr.validate_value!(nil, register, instance_object: model) }
|
|
394
|
+
.not_to raise_error
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
context "when value is nil and the attribute has a default" do
|
|
399
|
+
it "uses the default value for validation" do
|
|
400
|
+
attr = described_class.new("name", :string, default: "John")
|
|
401
|
+
expect { attr.validate_value!(nil, register, instance_object: model) }
|
|
402
|
+
.not_to raise_error
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it "executes a proc default in the instance context (instance_exec preserved)" do
|
|
406
|
+
# Regression: validate_value! must thread instance_object through
|
|
407
|
+
# default(register, instance_object) which calls instance_exec on
|
|
408
|
+
# the Proc. Verify by having the Proc read an instance attribute.
|
|
409
|
+
model_class = Class.new(Lutaml::Model::Serializable) do
|
|
410
|
+
attribute :prefix, :string, values: %w[Mr Ms Dr]
|
|
411
|
+
attribute :name, :string, values: %w[Alice Bob],
|
|
412
|
+
default: -> { "#{prefix}_default" }
|
|
413
|
+
end
|
|
414
|
+
instance = model_class.new(prefix: "Dr")
|
|
415
|
+
attr = model_class.attributes[:name]
|
|
416
|
+
expect { attr.validate_value!(nil, register, instance_object: instance) }
|
|
417
|
+
.to raise_error(Lutaml::Model::InvalidValueError) do |error|
|
|
418
|
+
# The error carries the value that was used for validation.
|
|
419
|
+
# If instance_exec ran, value is "Dr_default"; if not, error
|
|
420
|
+
# would never fire (proc would crash before reaching validation).
|
|
421
|
+
expect(error.message).to include("Dr_default")
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
context "when value is uninitialized" do
|
|
427
|
+
it "short-circuits without raising" do
|
|
428
|
+
uninit = Lutaml::Model::UninitializedClass.instance
|
|
429
|
+
attr = described_class.new("name", :string)
|
|
430
|
+
expect { attr.validate_value!(uninit, register, instance_object: model) }
|
|
431
|
+
.not_to raise_error
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
describe "Model initialization with proc defaults" do
|
|
437
|
+
# Regression: Serialize#determine_value previously called default_set?
|
|
438
|
+
# + default separately, executing the Proc twice for each omitted
|
|
439
|
+
# attribute. Verify single-execution by counting side effects.
|
|
440
|
+
it "executes a proc default exactly once when the attribute is omitted" do
|
|
441
|
+
counter = 0
|
|
442
|
+
model_class = Class.new(Lutaml::Model::Serializable) do
|
|
443
|
+
attribute :count, :integer, default: -> { counter += 1 }
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
instance = model_class.new
|
|
447
|
+
expect(instance.count).to eq(1)
|
|
448
|
+
expect(counter).to eq(1)
|
|
292
449
|
end
|
|
293
450
|
end
|
|
294
451
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
# Guard: the generated Opal boot manifest must stay in sync with the autoloads
|
|
7
|
+
# in lib/lutaml/**. Opal ignores `autoload`, so lutaml_model_boot.rb explicitly
|
|
8
|
+
# requires every autoload target; a deleted/renamed file leaves a stale require
|
|
9
|
+
# that only fails the Opal CI job (`rake spec:opal`) — MRI's autoload hides it.
|
|
10
|
+
# Regenerating here and comparing catches that drift in the fast MRI suite.
|
|
11
|
+
RSpec.describe "Opal boot manifest" do
|
|
12
|
+
let(:generator) do
|
|
13
|
+
File.expand_path("../../../lib/compat/opal/generate_boot.rb", __dir__)
|
|
14
|
+
end
|
|
15
|
+
let(:manifest) do
|
|
16
|
+
File.expand_path("../../../lib/compat/opal/lutaml_model_boot.rb", __dir__)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "matches the generator output (regenerate: ruby lib/compat/opal/generate_boot.rb)" do
|
|
20
|
+
Dir.mktmpdir do |dir|
|
|
21
|
+
out = File.join(dir, "boot.rb")
|
|
22
|
+
ok = system({ "OPAL_BOOT_OUTPUT" => out }, RbConfig.ruby, generator,
|
|
23
|
+
out: File::NULL, err: File::NULL)
|
|
24
|
+
expect(ok).to be(true), "generate_boot.rb failed to run"
|
|
25
|
+
expect(File.read(manifest)).to eq(File.read(out)),
|
|
26
|
+
"lib/compat/opal/lutaml_model_boot.rb is out of date — run " \
|
|
27
|
+
"`ruby lib/compat/opal/generate_boot.rb` after adding/removing autoloads"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -323,6 +323,24 @@ RSpec.describe Lutaml::Model::Collection do
|
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
+
context "when a range collection allows zero elements" do
|
|
327
|
+
it "validates when the collection is given as empty" do
|
|
328
|
+
kiln = CollectionTests::Kiln.new(attributes.merge(pots: []))
|
|
329
|
+
|
|
330
|
+
expect { kiln.validate! }.not_to raise_error
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "validates when the collection is omitted entirely" do
|
|
334
|
+
kiln = CollectionTests::Kiln.new(attributes.except(:pots))
|
|
335
|
+
|
|
336
|
+
expect { kiln.validate! }.not_to raise_error
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "does not flag the empty min-zero collection when built with no arguments" do
|
|
340
|
+
expect { CollectionTests::Kiln.new.validate! }.not_to raise_error
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
326
344
|
context "when collection with unbounded maximum exceeds minimum" do
|
|
327
345
|
let(:valid_attributes) do
|
|
328
346
|
attributes.merge(operators: ["John", "Jane", "Jim", "Jessica"])
|