lutaml-model 0.8.19 → 0.8.23
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/dependent-tests.yml +1 -1
- data/.github/workflows/rake.yml +1 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +133 -72
- data/bench/bench_common.rb +8 -5
- data/bench/bench_niso.rb +1 -2
- data/bench/gate_config.rb +4 -1
- data/benchmark/serialization_benchmark.rb +4 -4
- data/docs/_guides/value-transformations.adoc +27 -0
- data/docs/_guides/xml-mapping.adoc +31 -0
- data/lib/compat/opal/generate_boot.rb +3 -1
- data/lib/compat/opal/lutaml_model_boot.rb +10 -7
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/lutaml/hash_format/format.rb +29 -0
- data/lib/lutaml/hash_format.rb +5 -27
- data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
- data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
- data/lib/lutaml/json/format.rb +61 -0
- data/lib/lutaml/json/generator_options.rb +62 -0
- data/lib/lutaml/json.rb +4 -59
- data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
- data/lib/lutaml/jsonl/format.rb +22 -0
- data/lib/lutaml/jsonl.rb +5 -19
- data/lib/lutaml/jsonld/adapter.rb +12 -4
- data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
- data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
- data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
- data/lib/lutaml/key_value/format.rb +26 -0
- data/lib/lutaml/key_value/transform.rb +4 -1
- data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
- data/lib/lutaml/key_value.rb +4 -21
- data/lib/lutaml/model/attribute.rb +38 -4
- data/lib/lutaml/model/collection.rb +7 -2
- data/lib/lutaml/model/compiled_rule.rb +1 -1
- data/lib/lutaml/model/config.rb +2 -2
- data/lib/lutaml/model/consolidation_map.rb +2 -2
- data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
- data/lib/lutaml/model/errors.rb +2 -2
- data/lib/lutaml/model/global_context.rb +4 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
- data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +2 -2
- data/lib/lutaml/model/schema/class_loader.rb +2 -1
- data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
- data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
- data/lib/lutaml/model/schema/file_writer.rb +5 -2
- data/lib/lutaml/model/schema/renderers/base.rb +4 -3
- data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
- data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
- data/lib/lutaml/model/schema/renderers/model.rb +18 -6
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
- data/lib/lutaml/model/schema/renderers.rb +2 -1
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
- data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
- data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
- data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
- data/lib/lutaml/model/sequence.rb +4 -4
- data/lib/lutaml/model/serialization_adapter.rb +1 -1
- data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +40 -9
- data/lib/lutaml/model/serialize/initialization.rb +9 -1
- data/lib/lutaml/model/serialize/model_import.rb +1 -0
- data/lib/lutaml/model/serialize.rb +22 -3
- data/lib/lutaml/model/transformation.rb +5 -0
- data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
- data/lib/lutaml/model/type.rb +9 -0
- data/lib/lutaml/model/type_registry.rb +2 -2
- data/lib/lutaml/model/uninitialized_class.rb +1 -1
- data/lib/lutaml/model/union.rb +5 -2
- data/lib/lutaml/model/utils.rb +2 -2
- data/lib/lutaml/model/value_policy.rb +27 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +17 -16
- data/lib/lutaml/rdf/mapping.rb +2 -2
- data/lib/lutaml/rdf/namespace_set.rb +2 -2
- data/lib/lutaml/rdf/transform.rb +2 -2
- data/lib/lutaml/toml/format.rb +48 -0
- data/lib/lutaml/toml.rb +5 -46
- data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
- data/lib/lutaml/xml/builder/base.rb +2 -2
- data/lib/lutaml/xml/format.rb +331 -0
- data/lib/lutaml/xml/mapping.rb +26 -2
- data/lib/lutaml/xml/mapping_rule.rb +62 -3
- data/lib/lutaml/xml/model_transform.rb +183 -55
- data/lib/lutaml/xml/namespace_collector.rb +3 -3
- data/lib/lutaml/xml/parse_session.rb +59 -0
- data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
- data/lib/lutaml/xml/schema/builder.rb +5 -5
- data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
- data/lib/lutaml/xml/schema/xsd.rb +0 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
- data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
- data/lib/lutaml/xml/transformation.rb +29 -0
- data/lib/lutaml/xml/xml_element.rb +2 -0
- data/lib/lutaml/xml.rb +4 -328
- data/lib/lutaml/yaml/format.rb +30 -0
- data/lib/lutaml/yaml.rb +5 -28
- data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
- data/lib/lutaml/yamls/format.rb +22 -0
- data/lib/lutaml/yamls.rb +5 -19
- data/lib/tasks/benchmark_runner.rb +2 -2
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
- data/spec/lutaml/model/attribute_spec.rb +8 -3
- data/spec/lutaml/model/collection_spec.rb +1 -1
- data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
- data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
- data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
- data/spec/lutaml/model/defaults_spec.rb +45 -0
- data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
- data/spec/lutaml/model/json_spec.rb +1 -2
- data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
- data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
- data/spec/lutaml/model/store_spec.rb +3 -1
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
- data/spec/lutaml/model/union_attribute_spec.rb +4 -2
- data/spec/lutaml/model/value_policy_spec.rb +82 -0
- data/spec/lutaml/require_load_order_spec.rb +25 -0
- data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
- data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
- data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
- data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
- data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
- metadata +26 -4
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/json/adapter/standard_adapter"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Json::Adapter::StandardAdapter do
|
|
7
|
+
let(:attributes) { { "name" => "John", "age" => 30 } }
|
|
8
|
+
|
|
9
|
+
describe ".parse" do
|
|
10
|
+
# json 3.0 removed create_additions and now raises ArgumentError on any
|
|
11
|
+
# unknown keyword, so the parser must be called without it.
|
|
12
|
+
it "parses without passing removed generator keywords to the json gem" do
|
|
13
|
+
expect(described_class.parse('{"name":"John","age":30}'))
|
|
14
|
+
.to eq(attributes)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "#to_json" do
|
|
19
|
+
subject(:document) { described_class.new(attributes) }
|
|
20
|
+
|
|
21
|
+
it "serializes with no options" do
|
|
22
|
+
expect(document.to_json).to eq('{"name":"John","age":30}')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# :register and :pretty are LutaML's own options. json 2.x ignored unknown
|
|
26
|
+
# options; json 3.0 raises ArgumentError, so they must be stripped before
|
|
27
|
+
# they reach JSON.generate.
|
|
28
|
+
it "strips LutaML options that the json generator does not accept" do
|
|
29
|
+
expect(document.to_json(register: :default_register))
|
|
30
|
+
.to eq('{"name":"John","age":30}')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "strips LutaML options when generating pretty output" do
|
|
34
|
+
expect(document.to_json(pretty: true, register: :default_register))
|
|
35
|
+
.to eq(%({\n "name": "John",\n "age": 30\n}))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# An ALLOWLIST and a denylist of LutaML's own option names behave
|
|
39
|
+
# identically for :register and :pretty. Only a key nobody enumerated
|
|
40
|
+
# tells them apart -- and json 3.0 raises on it.
|
|
41
|
+
it "strips a caller option nobody enumerated" do
|
|
42
|
+
expect(document.to_json(some_unknown_option: 1))
|
|
43
|
+
.to eq('{"name":"John","age":30}')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# json 3.0 removed :escape_slash. Dropping it would silently stop escaping
|
|
47
|
+
# slashes; it is the old alias of :script_safe, so it is translated.
|
|
48
|
+
it "honours escape_slash on every json version" do
|
|
49
|
+
expect(described_class.new({ "a" => "x/y" }).to_json(escape_slash: true))
|
|
50
|
+
.to eq('{"a":"x\\/y"}')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The filter is an allowlist, not a passthrough: genuine generator options
|
|
54
|
+
# must still reach JSON.generate.
|
|
55
|
+
it "forwards options the json generator does accept" do
|
|
56
|
+
expect(document.to_json(register: :default_register, space: " "))
|
|
57
|
+
.to eq('{"name": "John","age": 30}')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "nesting inside another JSON.generate call" do
|
|
62
|
+
subject(:document) { described_class.new(attributes) }
|
|
63
|
+
|
|
64
|
+
# Ruby's generator hands #to_json a JSON::State, not an options hash.
|
|
65
|
+
# json 3.0 removed JSON::State#[], so reading :pretty off it raises.
|
|
66
|
+
it "serializes when nested in a Hash passed to JSON.generate" do
|
|
67
|
+
expect(JSON.generate({ "doc" => document }))
|
|
68
|
+
.to eq('{"doc":{"name":"John","age":30}}')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "serializes when nested in an Array passed to JSON.generate" do
|
|
72
|
+
expect(JSON.generate([document])).to eq('[{"name":"John","age":30}]')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A DEFAULT state renders compact, which is also what a DISCARDED state
|
|
76
|
+
# produces, so the two examples above cannot tell them apart. These carry
|
|
77
|
+
# formatting, so dropping the state changes the output.
|
|
78
|
+
it "inherits the outer indent when nested in a pretty Hash" do
|
|
79
|
+
expect(JSON.pretty_generate({ "doc" => document }))
|
|
80
|
+
.to eq(%({\n "doc": {\n "name": "John",\n "age": 30\n }\n}))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "inherits the outer indent when nested in a pretty Array" do
|
|
84
|
+
expect(JSON.pretty_generate([document]))
|
|
85
|
+
.to eq(%([\n {\n "name": "John",\n "age": 30\n }\n]))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "honours a configured JSON::State handed to #to_json directly" do
|
|
89
|
+
# A DEFAULT state renders compact, which is also what a discarded state
|
|
90
|
+
# produces -- so the state has to carry formatting for this to mean
|
|
91
|
+
# anything.
|
|
92
|
+
state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
|
|
93
|
+
expect(document.to_json(state))
|
|
94
|
+
.to eq(%({\n "name": "John",\n "age": 30\n}))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe Lutaml::Json::GeneratorOptions do
|
|
99
|
+
# The derived half of the allowlist reads JSON::State, which Opal's JSON
|
|
100
|
+
# shim does not define. BASE_PERMITTED is what the filter falls back to
|
|
101
|
+
# there, so it has to stand on its own.
|
|
102
|
+
# BASE_PERMITTED is the whole allowlist on Opal, whose JSON shim has no
|
|
103
|
+
# State class for DERIVED_PERMITTED to read. So it has to stand alone
|
|
104
|
+
# against the real generator, not merely be a subset of something larger.
|
|
105
|
+
it "permits only options the running generator actually accepts" do
|
|
106
|
+
rejected = described_class::BASE_PERMITTED.reject do |key|
|
|
107
|
+
JSON.generate({ "a" => 1 }, key => nil)
|
|
108
|
+
true
|
|
109
|
+
rescue ArgumentError => e
|
|
110
|
+
# "unknown keyword" means the generator does not know this option at
|
|
111
|
+
# all. Any other complaint -- a type error about the nil value we
|
|
112
|
+
# passed -- means it knows the option and only dislikes the value.
|
|
113
|
+
!e.message.include?("unknown keyword")
|
|
114
|
+
rescue StandardError
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
expect(rejected).to eq([])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Opal's JSON shim defines no State class, so DERIVED_PERMITTED is empty
|
|
122
|
+
# there and the whole module must still load. Asserting that
|
|
123
|
+
# BASE_PERMITTED contains some symbols does not exercise the guard; only
|
|
124
|
+
# loading the file with JSON::State absent does.
|
|
125
|
+
it "loads and still permits the base set when JSON::State is absent" do
|
|
126
|
+
hide_const("JSON::State")
|
|
127
|
+
|
|
128
|
+
mod = Module.new
|
|
129
|
+
mod.module_eval(
|
|
130
|
+
File.read(File.expand_path("../../../../lib/lutaml/json/generator_options.rb", __dir__)),
|
|
131
|
+
)
|
|
132
|
+
permitted = mod.const_get(:Lutaml).const_get(:Json)
|
|
133
|
+
.const_get(:GeneratorOptions)::PERMITTED
|
|
134
|
+
|
|
135
|
+
expect(permitted).to eq(described_class::BASE_PERMITTED)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "carries the formatting options pretty output depends on" do
|
|
139
|
+
expect(described_class::BASE_PERMITTED)
|
|
140
|
+
.to include(:indent, :object_nl, :space, :space_before)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "rejects a non-Hash options argument" do
|
|
144
|
+
expect(described_class.filter(nil)).to eq({})
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "treats nil as LutaML options" do
|
|
148
|
+
expect(described_class.lutaml_options?(nil)).to be(true)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "treats a Hash as LutaML options" do
|
|
152
|
+
expect(described_class.lutaml_options?({ pretty: true })).to be(true)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "does not treat a JSON::State as LutaML options" do
|
|
156
|
+
expect(described_class.lutaml_options?(JSON::State.new)).to be(false)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -43,4 +43,23 @@ RSpec.describe Lutaml::JsonLd::Adapter do
|
|
|
43
43
|
round_tripped = JSON.parse(result)
|
|
44
44
|
expect(round_tripped).to eq(jsonld_hash.transform_keys(&:to_s))
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
describe "a JSON::State handed to #to_jsonld" do
|
|
48
|
+
subject(:document) { described_class.new({ "@id" => "urn:x", "n" => 1 }) }
|
|
49
|
+
|
|
50
|
+
# Ruby's generator only ever calls #to_json, so a state reaches #to_jsonld
|
|
51
|
+
# only from a direct caller. json 3.0 removed JSON::State#[], so the state
|
|
52
|
+
# must be forwarded to the payload rather than read as an options hash.
|
|
53
|
+
it "honours a state that carries formatting" do
|
|
54
|
+
state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
|
|
55
|
+
|
|
56
|
+
expect(document.to_jsonld(state))
|
|
57
|
+
.to eq(%({\n "@id": "urn:x",\n "n": 1\n}))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "renders compactly for a default state" do
|
|
61
|
+
expect(document.to_jsonld(JSON::State.new))
|
|
62
|
+
.to eq('{"@id":"urn:x","n":1}')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
46
65
|
end
|
|
@@ -18,7 +18,8 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
18
18
|
|
|
19
19
|
it "maps nil through value_map[:nil] => :empty to [] (collection)" do
|
|
20
20
|
expect(
|
|
21
|
-
attribute(:string, collection: true).apply_value_map(nil,
|
|
21
|
+
attribute(:string, collection: true).apply_value_map(nil,
|
|
22
|
+
{ nil: :empty }),
|
|
22
23
|
).to eq([])
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -34,13 +35,15 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
34
35
|
custom_collection_class = Class.new(Lutaml::Model::Collection) do
|
|
35
36
|
instances :items, member_class
|
|
36
37
|
end
|
|
37
|
-
attr = described_class.new("items", member_class,
|
|
38
|
+
attr = described_class.new("items", member_class,
|
|
39
|
+
collection: custom_collection_class)
|
|
38
40
|
result = attr.apply_value_map(nil, { nil: :empty })
|
|
39
41
|
expect(result).to be_a(custom_collection_class)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
it "maps nil through value_map[:nil] => :omitted to UninitializedClass" do
|
|
43
|
-
expect(attribute(:string).apply_value_map(nil,
|
|
45
|
+
expect(attribute(:string).apply_value_map(nil,
|
|
46
|
+
{ nil: :omitted })).to eq(uninit)
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -50,12 +53,14 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
it "maps '' through value_map[:empty] => :empty preserving '' for scalar" do
|
|
53
|
-
expect(attribute(:string).apply_value_map("",
|
|
56
|
+
expect(attribute(:string).apply_value_map("",
|
|
57
|
+
{ empty: :empty })).to eq("")
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
it "maps [] through value_map[:empty] => :empty preserving [] for collection" do
|
|
57
61
|
expect(
|
|
58
|
-
attribute(:string, collection: true).apply_value_map([],
|
|
62
|
+
attribute(:string, collection: true).apply_value_map([],
|
|
63
|
+
{ empty: :empty }),
|
|
59
64
|
).to eq([])
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -69,11 +74,13 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
69
74
|
|
|
70
75
|
describe "omitted handling" do
|
|
71
76
|
it "maps UninitializedClass through value_map[:omitted] => :nil" do
|
|
72
|
-
expect(attribute(:string).apply_value_map(uninit,
|
|
77
|
+
expect(attribute(:string).apply_value_map(uninit,
|
|
78
|
+
{ omitted: :nil })).to be_nil
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
it "maps UninitializedClass through value_map[:omitted] => :empty" do
|
|
76
|
-
expect(attribute(:string).apply_value_map(uninit,
|
|
82
|
+
expect(attribute(:string).apply_value_map(uninit,
|
|
83
|
+
{ omitted: :empty })).to eq("")
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
it "does NOT route UninitializedClass through the :empty branch" do
|
|
@@ -81,18 +88,21 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
81
88
|
# utils.rb:132-137. Ensure dispatch order matches the originals: uninit
|
|
82
89
|
# values go to :omitted, not :empty.
|
|
83
90
|
expect(
|
|
84
|
-
attribute(:string).apply_value_map(uninit,
|
|
91
|
+
attribute(:string).apply_value_map(uninit,
|
|
92
|
+
{ empty: :nil, omitted: :empty }),
|
|
85
93
|
).to eq("")
|
|
86
94
|
end
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
describe "Boolean bare-form (Boolean attribute type required)" do
|
|
90
98
|
it "returns false directly for Boolean attribute, value_map[:empty] = false" do
|
|
91
|
-
expect(attribute(:boolean).apply_value_map("",
|
|
99
|
+
expect(attribute(:boolean).apply_value_map("",
|
|
100
|
+
{ empty: false })).to be(false)
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
it "returns true directly for Boolean attribute, value_map[:omitted] = true" do
|
|
95
|
-
expect(attribute(:boolean).apply_value_map(uninit,
|
|
104
|
+
expect(attribute(:boolean).apply_value_map(uninit,
|
|
105
|
+
{ omitted: true })).to be(true)
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
it "does NOT apply bare-boolean form to a non-Boolean attribute (documented tightening)" do
|
|
@@ -106,15 +116,18 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
106
116
|
|
|
107
117
|
describe "Boolean nested-form (value_map[:from][:empty] / [:omitted])" do
|
|
108
118
|
it "returns false from value_map[:from][:empty] unconditionally for Boolean attribute" do
|
|
109
|
-
expect(attribute(:boolean).apply_value_map("",
|
|
119
|
+
expect(attribute(:boolean).apply_value_map("",
|
|
120
|
+
{ from: { empty: false } })).to be(false)
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
it "returns true from value_map[:from][:omitted] unconditionally for Boolean attribute" do
|
|
113
|
-
expect(attribute(:boolean).apply_value_map(uninit,
|
|
124
|
+
expect(attribute(:boolean).apply_value_map(uninit,
|
|
125
|
+
{ from: { omitted: true } })).to be(true)
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
it "returns false from nested form even for non-Boolean attribute (no type gate)" do
|
|
117
|
-
expect(attribute(:string).apply_value_map("",
|
|
129
|
+
expect(attribute(:string).apply_value_map("",
|
|
130
|
+
{ from: { empty: false } })).to be(false)
|
|
118
131
|
end
|
|
119
132
|
end
|
|
120
133
|
|
|
@@ -358,7 +358,8 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
358
358
|
it "executes the proc in the instance context (reads instance attribute)" do
|
|
359
359
|
instance = model_class.new(name: "Alice")
|
|
360
360
|
attribute = model_class.attributes[:greeting]
|
|
361
|
-
expect(attribute.default_value(register,
|
|
361
|
+
expect(attribute.default_value(register,
|
|
362
|
+
instance)).to eq("Hello, Alice")
|
|
362
363
|
end
|
|
363
364
|
end
|
|
364
365
|
end
|
|
@@ -413,7 +414,9 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
413
414
|
end
|
|
414
415
|
instance = model_class.new(prefix: "Dr")
|
|
415
416
|
attr = model_class.attributes[:name]
|
|
416
|
-
expect
|
|
417
|
+
expect do
|
|
418
|
+
attr.validate_value!(nil, register, instance_object: instance)
|
|
419
|
+
end
|
|
417
420
|
.to raise_error(Lutaml::Model::InvalidValueError) do |error|
|
|
418
421
|
# The error carries the value that was used for validation.
|
|
419
422
|
# If instance_exec ran, value is "Dr_default"; if not, error
|
|
@@ -427,7 +430,9 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
427
430
|
it "short-circuits without raising" do
|
|
428
431
|
uninit = Lutaml::Model::UninitializedClass.instance
|
|
429
432
|
attr = described_class.new("name", :string)
|
|
430
|
-
expect
|
|
433
|
+
expect do
|
|
434
|
+
attr.validate_value!(uninit, register, instance_object: model)
|
|
435
|
+
end
|
|
431
436
|
.not_to raise_error
|
|
432
437
|
end
|
|
433
438
|
end
|
|
@@ -644,7 +644,7 @@ RSpec.describe Lutaml::Model::Collection do
|
|
|
644
644
|
collection = title_collection_class.from_json(json_data)
|
|
645
645
|
|
|
646
646
|
# JSON.parse should only be called once on the main array, not on individual strings
|
|
647
|
-
expect(JSON).to have_received(:parse).once.with(json_data
|
|
647
|
+
expect(JSON).to have_received(:parse).once.with(json_data)
|
|
648
648
|
expect(collection.titles.map(&:content)).to eq(["Title One", "Title Two",
|
|
649
649
|
"Title Three"])
|
|
650
650
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# A Collection overrides #to_format and merges `collection: true` into the
|
|
6
|
+
# options BEFORE the base implementation runs. JSON::State#merge is #configure,
|
|
7
|
+
# which rejects unknown keys on json 3.0, so the state has to be wrapped first.
|
|
8
|
+
RSpec.describe "a collection nested inside JSON.generate" do
|
|
9
|
+
before do
|
|
10
|
+
stub_const("NestedItem", Class.new(Lutaml::Model::Serializable) do
|
|
11
|
+
attribute :n, :string
|
|
12
|
+
end)
|
|
13
|
+
stub_const("NestedItems", Class.new(Lutaml::Model::Collection) do
|
|
14
|
+
instances :items, NestedItem
|
|
15
|
+
end)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:collection) { NestedItems.new([NestedItem.new(n: "a")]) }
|
|
19
|
+
|
|
20
|
+
it "serializes inside a Hash" do
|
|
21
|
+
expect(JSON.generate({ "c" => collection })).to eq('{"c":[{"n":"a"}]}')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "inherits the outer indent inside a pretty Hash" do
|
|
25
|
+
expect(JSON.pretty_generate({ "c" => collection }))
|
|
26
|
+
.to eq(%({\n "c": [\n {\n "n": "a"\n }\n ]\n}))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The class-level entry point normalises separately from the instance one,
|
|
30
|
+
# and deleting that normaliser leaves every instance example green.
|
|
31
|
+
it "normalises a state passed to the class-level entry point" do
|
|
32
|
+
# array_nl is what puts the newlines in an array; without it the state
|
|
33
|
+
# is not a pretty state at all and the example proves nothing.
|
|
34
|
+
state = JSON::State.new(indent: " ", object_nl: "\n", array_nl: "\n",
|
|
35
|
+
space: " ")
|
|
36
|
+
|
|
37
|
+
expect(NestedItems.to_json(collection, state))
|
|
38
|
+
.to eq(%([\n {\n "n": "a"\n }\n]))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "still serializes directly" do
|
|
42
|
+
expect(collection.to_json).to eq('[{"n":"a"}]')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -298,9 +298,9 @@ module CustomBibtexAdapterSpec
|
|
|
298
298
|
add_mapping(name, to, field_type: :field, render_nil: render_nil)
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
-
def add_mapping(name, to, **)
|
|
301
|
+
def add_mapping(name, to, **options)
|
|
302
302
|
# validate!(name, to, {})
|
|
303
|
-
@mappings << BibtexMappingRule.new(name, to: to, **)
|
|
303
|
+
@mappings << BibtexMappingRule.new(name, to: to, **options)
|
|
304
304
|
end
|
|
305
305
|
|
|
306
306
|
def mapping_for_field(field)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#746: an empty value ("", [], {}) is a PRESENT value per
|
|
6
|
+
# docs/_guides/missing-values-handling.adoc and must reach custom `from`
|
|
7
|
+
# methods. Only nil (non-existent) and an omitted key (undefined) skip them.
|
|
8
|
+
module CustomFromEmptyValuesSpec
|
|
9
|
+
class Tagged < Lutaml::Model::Serializable
|
|
10
|
+
attribute :received, :string
|
|
11
|
+
|
|
12
|
+
key_value do
|
|
13
|
+
map "tags", to: :received, with: { to: :tags_to, from: :tags_from }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tags_to(model, doc)
|
|
17
|
+
doc["tags"] = model.received
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tags_from(model, value)
|
|
21
|
+
model.received = value.inspect
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Mapped < Lutaml::Model::Serializable
|
|
26
|
+
attribute :received, :string
|
|
27
|
+
|
|
28
|
+
key_value do
|
|
29
|
+
map "fields", to: :received, with: { to: :fields_to, from: :fields_from }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fields_to(model, doc)
|
|
33
|
+
doc["fields"] = model.received
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fields_from(model, value)
|
|
37
|
+
model.received = value.inspect
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
RSpec.describe "custom from methods and empty values" do
|
|
43
|
+
it "receives an empty collection" do
|
|
44
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags: []").received)
|
|
45
|
+
.to eq("[]")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "receives an empty string" do
|
|
49
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml('tags: ""').received)
|
|
50
|
+
.to eq("\"\"")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "receives an empty mapping" do
|
|
54
|
+
expect(CustomFromEmptyValuesSpec::Mapped.from_yaml("fields: {}").received)
|
|
55
|
+
.to eq("{}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "receives empty values from JSON too" do
|
|
59
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": []})).received)
|
|
60
|
+
.to eq("[]")
|
|
61
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": ""})).received)
|
|
62
|
+
.to eq("\"\"")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "still skips the method for a nil value" do
|
|
66
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags:").received)
|
|
67
|
+
.to be_nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "still skips the method for an omitted key" do
|
|
71
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("{}").received)
|
|
72
|
+
.to be_nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -201,8 +201,8 @@ module CustomVobjectAdapterSpec
|
|
|
201
201
|
@objects
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
def map(&)
|
|
205
|
-
@objects.map(&)
|
|
204
|
+
def map(&block)
|
|
205
|
+
@objects.map(&block)
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
private
|
|
@@ -260,16 +260,16 @@ module CustomVobjectAdapterSpec
|
|
|
260
260
|
@element_type = element_type
|
|
261
261
|
end
|
|
262
262
|
|
|
263
|
-
def map_value(to:, **)
|
|
264
|
-
add_mapping(:value, to, type: :simple, **)
|
|
263
|
+
def map_value(to:, **options)
|
|
264
|
+
add_mapping(:value, to, type: :simple, **options)
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
-
def map_property(name, to:, type: :simple, **)
|
|
268
|
-
add_mapping(name.downcase, to, type: type, **)
|
|
267
|
+
def map_property(name, to:, type: :simple, **options)
|
|
268
|
+
add_mapping(name.downcase, to, type: type, **options)
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
def map_component(name, to:, **)
|
|
272
|
-
add_mapping(name.downcase, to, type: :component, **)
|
|
271
|
+
def map_component(name, to:, **options)
|
|
272
|
+
add_mapping(name.downcase, to, type: :component, **options)
|
|
273
273
|
end
|
|
274
274
|
|
|
275
275
|
def map_field_set(count:, item_type:, item_options: {})
|
|
@@ -146,6 +146,33 @@ module DefaultsSpec
|
|
|
146
146
|
|
|
147
147
|
class InstanceAwareGrandChild < InstanceAwareChild
|
|
148
148
|
end
|
|
149
|
+
|
|
150
|
+
# Class-method defaults must keep resolving as they did before 0.8.0
|
|
151
|
+
# (lutaml-model#745): default lambdas are evaluated against the instance,
|
|
152
|
+
# with the model class as fallback for methods the instance does not have.
|
|
153
|
+
class ClassMethodDefault < Lutaml::Model::Serializable
|
|
154
|
+
attribute :os, :string, default: -> { detect_os }
|
|
155
|
+
|
|
156
|
+
def self.detect_os
|
|
157
|
+
"from-class-method"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class ClassMethodDefaultWithArgs < Lutaml::Model::Serializable
|
|
162
|
+
attribute :label, :string, default: -> { build_label("x") }
|
|
163
|
+
|
|
164
|
+
def self.build_label(prefix)
|
|
165
|
+
"#{prefix}-label"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class InstanceMethodDefault < Lutaml::Model::Serializable
|
|
170
|
+
attribute :value, :string, default: -> { instance_helper }
|
|
171
|
+
|
|
172
|
+
def instance_helper
|
|
173
|
+
"from-instance-method"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
149
176
|
end
|
|
150
177
|
|
|
151
178
|
RSpec.describe DefaultsSpec::Glaze do
|
|
@@ -346,3 +373,21 @@ RSpec.describe DefaultsSpec::Glaze do
|
|
|
346
373
|
end
|
|
347
374
|
end
|
|
348
375
|
end
|
|
376
|
+
|
|
377
|
+
RSpec.describe DefaultsSpec::ClassMethodDefault do
|
|
378
|
+
it "resolves class methods from default lambdas" do
|
|
379
|
+
expect(described_class.new.os).to eq("from-class-method")
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
RSpec.describe DefaultsSpec::ClassMethodDefaultWithArgs do
|
|
384
|
+
it "passes arguments through to class methods" do
|
|
385
|
+
expect(described_class.new.label).to eq("x-label")
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
RSpec.describe DefaultsSpec::InstanceMethodDefault do
|
|
390
|
+
it "still resolves instance methods in default lambdas" do
|
|
391
|
+
expect(described_class.new.value).to eq("from-instance-method")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "oj"
|
|
5
|
+
require "multi_json"
|
|
6
|
+
|
|
7
|
+
# What a nested model should do with the outer generator's options differs per
|
|
8
|
+
# ADAPTER, not per option. Reference values are unmodified main on json 2.21.2.
|
|
9
|
+
#
|
|
10
|
+
# stdlib honours script_safe / ascii_only / pretty -> receives the state
|
|
11
|
+
# Oj ignores them and uses its own escape_mode -> receives none of them
|
|
12
|
+
#
|
|
13
|
+
# Four earlier attempts at this got it wrong by reasoning about option names
|
|
14
|
+
# instead of measuring each adapter, so every row here is a measured cell.
|
|
15
|
+
RSpec.describe "generator options reaching a nested model" do
|
|
16
|
+
before do
|
|
17
|
+
stub_const("EscapeProbe", Class.new(Lutaml::Model::Serializable) do
|
|
18
|
+
attribute :s, :string
|
|
19
|
+
end)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:model) { EscapeProbe.new(s: "</script>é") }
|
|
23
|
+
|
|
24
|
+
describe "the stdlib adapter" do
|
|
25
|
+
it "honours script_safe" do
|
|
26
|
+
expect(JSON.generate({ "m" => model }, script_safe: true))
|
|
27
|
+
.to eq('{"m":{"s":"<\\/script>é"}}')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "honours ascii_only" do
|
|
31
|
+
expect(JSON.generate({ "m" => model }, ascii_only: true))
|
|
32
|
+
.to eq('{"m":{"s":"</script>\\u00e9"}}')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "honours the outer indent" do
|
|
36
|
+
expect(JSON.pretty_generate({ "m" => model }))
|
|
37
|
+
.to eq(%({\n "m": {\n "s": "</script>é"\n }\n}))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "the Oj adapter" do
|
|
42
|
+
around do |example|
|
|
43
|
+
previous = Oj.default_options
|
|
44
|
+
Oj.default_options = { escape_mode: :xss_safe }
|
|
45
|
+
example.run
|
|
46
|
+
Oj.default_options = previous
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Oj must keep its OWN escaping. Forwarding the generator's options -- all
|
|
50
|
+
# of them, only the caller-set ones, or just ascii_only -- each reset
|
|
51
|
+
# escape_mode and silently emitted a literal </script>.
|
|
52
|
+
it "keeps its configured escape_mode when no option is passed" do
|
|
53
|
+
result = Lutaml::Model::Config.with_adapter(json: :oj) do
|
|
54
|
+
JSON.generate({ "m" => model })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "keeps its configured escape_mode when ascii_only is passed" do
|
|
61
|
+
result = Lutaml::Model::Config.with_adapter(json: :oj) do
|
|
62
|
+
JSON.generate({ "m" => model }, ascii_only: true)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "keeps its configured escape_mode when script_safe is passed" do
|
|
69
|
+
result = Lutaml::Model::Config.with_adapter(json: :oj) do
|
|
70
|
+
JSON.generate({ "m" => model }, script_safe: true)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# MultiJson's json_gem backend IS the stdlib generator, so unlike Oj it does
|
|
78
|
+
# honour these options and must still receive them. That BACKEND cannot run
|
|
79
|
+
# under json 3.0 -- it sends create_additions, which json removed -- so these
|
|
80
|
+
# are asserted on the versions where it works. multi_json itself is fine
|
|
81
|
+
# under json 3.0 on its Oj backend; only json_gem is affected.
|
|
82
|
+
describe "the MultiJson adapter", if: Gem::Version.new(JSON::VERSION) < Gem::Version.new("3.0.0") do
|
|
83
|
+
before { MultiJson.use(:json_gem) }
|
|
84
|
+
|
|
85
|
+
it "still receives script_safe" do
|
|
86
|
+
result = Lutaml::Model::Config.with_adapter(json: :multi_json) do
|
|
87
|
+
JSON.generate({ "m" => model }, script_safe: true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
expect(result).to eq('{"m":{"s":"<\\/script>é"}}')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "still receives ascii_only" do
|
|
94
|
+
result = Lutaml::Model::Config.with_adapter(json: :multi_json) do
|
|
95
|
+
JSON.generate({ "m" => model }, ascii_only: true)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
expect(result).to eq('{"m":{"s":"</script>\\u00e9"}}')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -8,9 +8,8 @@ RSpec.describe Lutaml::Model::Json do
|
|
|
8
8
|
let(:lib_path) { File.expand_path("../../../lib", __dir__) }
|
|
9
9
|
|
|
10
10
|
it "loads require 'lutaml/json' without preloading lutaml/model" do
|
|
11
|
-
# rubocop:disable Style/CommandLiteral
|
|
11
|
+
# rubocop:disable-next Style/CommandLiteral
|
|
12
12
|
result = `#{RbConfig.ruby} -I#{lib_path} -e 'require "lutaml/json"; puts :ok' 2>&1`
|
|
13
|
-
# rubocop:enable Style/CommandLiteral
|
|
14
13
|
|
|
15
14
|
expect($?.success?).to be(true), result
|
|
16
15
|
expect(result).to include("ok")
|