lutaml-model 0.8.16 → 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/.github/workflows/release.yml +0 -3
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +76 -12
- 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/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- data/docs/_pages/validation.adoc +65 -0
- data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
- data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
- data/docs/namespace-management.adoc +2 -0
- data/docs/xml-schema-qualification.md +6 -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/jsonld.rb +1 -4
- 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/choice.rb +34 -0
- data/lib/lutaml/model/compiled_rule.rb +7 -0
- 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 +19 -0
- data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
- data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
- data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
- data/lib/lutaml/rdf.rb +3 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_element.rb +2 -2
- 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 +59 -31
- 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/lib/lutaml/yamlld/adapter.rb +25 -0
- data/lib/lutaml/yamlld.rb +25 -0
- 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/integration/multi_format_spec.rb +23 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +177 -13
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/custom_model_spec.rb +4 -4
- data/spec/lutaml/model/mixed_content_spec.rb +173 -2
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +33 -3
- 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/uninitialized_class_spec.rb +1 -1
- 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/model/xsd_form_default_patterns_spec.rb +2 -2
- data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
- data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
- data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +15 -1
- data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
- data/spec/lutaml/xml/mapping_spec.rb +4 -4
- data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
- data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
- 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/serializable_namespace_spec.rb +6 -6
- data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
- data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
- 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/lutaml/yamlld/adapter_spec.rb +56 -0
- data/spec/lutaml/yamlld/registration_spec.rb +33 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +108 -37
- 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,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>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "lutaml/turtle"
|
|
5
5
|
require "lutaml/jsonld"
|
|
6
|
+
require "lutaml/yamlld"
|
|
6
7
|
|
|
7
8
|
RSpec.describe "Multi-format model" do
|
|
8
9
|
before do
|
|
@@ -103,4 +104,26 @@ RSpec.describe "Multi-format model" do
|
|
|
103
104
|
expect(turtle).to include("@prefix")
|
|
104
105
|
end
|
|
105
106
|
end
|
|
107
|
+
|
|
108
|
+
describe "YAML-LD format" do
|
|
109
|
+
it "serializes with @type and @id" do
|
|
110
|
+
yamlld = instance.to_yamlld
|
|
111
|
+
parsed = YAML.safe_load(yamlld)
|
|
112
|
+
expect(parsed["@type"]).to eq("skos:Concept")
|
|
113
|
+
expect(parsed["@id"]).to eq("http://example.org/concept/42")
|
|
114
|
+
expect(parsed["prefLabel"]).to eq("test")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "round-trips" do
|
|
118
|
+
restored = MultiFormatModel.from_yamlld(instance.to_yamlld)
|
|
119
|
+
expect(restored.name).to eq("test")
|
|
120
|
+
expect(restored.code).to eq("42")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "produces the same data model as JSON-LD" do
|
|
124
|
+
from_yamlld = YAML.safe_load(instance.to_yamlld)
|
|
125
|
+
from_jsonld = JSON.parse(instance.to_jsonld)
|
|
126
|
+
expect(from_yamlld).to eq(from_jsonld)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
106
129
|
end
|
|
@@ -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
|
|
@@ -191,7 +191,14 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
191
191
|
|
|
192
192
|
Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
|
|
193
193
|
it "return true if option is `#{option}`" do
|
|
194
|
-
|
|
194
|
+
if option == :xsd_type
|
|
195
|
+
expect do
|
|
196
|
+
result = validate_options.call({ option => "value" })
|
|
197
|
+
expect(result).to be(true)
|
|
198
|
+
end.to output.to_stderr
|
|
199
|
+
else
|
|
200
|
+
expect(validate_options.call({ option => "value" })).to be(true)
|
|
201
|
+
end
|
|
195
202
|
end
|
|
196
203
|
end
|
|
197
204
|
|
|
@@ -209,9 +216,19 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
209
216
|
age_attr.send(:validate_options!, { pattern: /[A-Za-z ]/ })
|
|
210
217
|
end.to raise_error(
|
|
211
218
|
StandardError,
|
|
212
|
-
"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",
|
|
213
221
|
)
|
|
214
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
|
|
215
232
|
end
|
|
216
233
|
|
|
217
234
|
describe "#derived?" do
|
|
@@ -242,46 +259,193 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
242
259
|
end
|
|
243
260
|
end
|
|
244
261
|
|
|
245
|
-
describe "#default
|
|
262
|
+
describe "#default" do
|
|
246
263
|
let(:register) { Lutaml::Model::Config.default_register }
|
|
247
|
-
|
|
248
|
-
def create_resolver(attribute)
|
|
249
|
-
Lutaml::Model::Services::DefaultValueResolver.new(attribute, register,
|
|
250
|
-
nil)
|
|
251
|
-
end
|
|
264
|
+
let(:instance) { nil }
|
|
252
265
|
|
|
253
266
|
context "when default is not set" do
|
|
254
267
|
let(:attribute) { described_class.new("name", :string) }
|
|
255
268
|
|
|
256
269
|
it "returns uninitialized" do
|
|
257
|
-
expect(
|
|
270
|
+
expect(attribute.default(register, instance)).to be(Lutaml::Model::UninitializedClass.instance)
|
|
258
271
|
end
|
|
259
272
|
end
|
|
260
273
|
|
|
261
274
|
context "when default is set as a proc" do
|
|
262
275
|
it "returns the value" do
|
|
263
276
|
attribute = described_class.new("name", :string, default: -> { "John" })
|
|
264
|
-
expect(
|
|
277
|
+
expect(attribute.default(register, instance)).to eq("John")
|
|
265
278
|
end
|
|
266
279
|
|
|
267
280
|
it "returns the value casted to correct type" do
|
|
268
281
|
file = Pathname.new("avatar.png")
|
|
269
282
|
attribute = described_class.new("image", :string, default: -> { file })
|
|
270
283
|
|
|
271
|
-
expect(
|
|
284
|
+
expect(attribute.default(register, instance)).to eq("avatar.png")
|
|
272
285
|
end
|
|
273
286
|
end
|
|
274
287
|
|
|
275
288
|
context "when default is set as value" do
|
|
276
289
|
it "returns the value" do
|
|
277
290
|
attribute = described_class.new("name", :string, default: "John Doe")
|
|
278
|
-
expect(
|
|
291
|
+
expect(attribute.default(register, instance)).to eq("John Doe")
|
|
279
292
|
end
|
|
280
293
|
|
|
281
294
|
it "returns the value casted to correct type" do
|
|
282
295
|
attribute = described_class.new("age", :integer, default: "24")
|
|
283
|
-
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
|
|
284
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)
|
|
285
449
|
end
|
|
286
450
|
end
|
|
287
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
|