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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +1 -1
  3. data/.github/workflows/rake.yml +1 -0
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +133 -72
  6. data/bench/bench_common.rb +8 -5
  7. data/bench/bench_niso.rb +1 -2
  8. data/bench/gate_config.rb +4 -1
  9. data/benchmark/serialization_benchmark.rb +4 -4
  10. data/docs/_guides/value-transformations.adoc +27 -0
  11. data/docs/_guides/xml-mapping.adoc +31 -0
  12. data/lib/compat/opal/generate_boot.rb +3 -1
  13. data/lib/compat/opal/lutaml_model_boot.rb +10 -7
  14. data/lib/compat/opal/moxml_boot.rb +6 -1
  15. data/lib/lutaml/hash_format/format.rb +29 -0
  16. data/lib/lutaml/hash_format.rb +5 -27
  17. data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
  18. data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
  19. data/lib/lutaml/json/format.rb +61 -0
  20. data/lib/lutaml/json/generator_options.rb +62 -0
  21. data/lib/lutaml/json.rb +4 -59
  22. data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
  23. data/lib/lutaml/jsonl/format.rb +22 -0
  24. data/lib/lutaml/jsonl.rb +5 -19
  25. data/lib/lutaml/jsonld/adapter.rb +12 -4
  26. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
  27. data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
  28. data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
  29. data/lib/lutaml/key_value/format.rb +26 -0
  30. data/lib/lutaml/key_value/transform.rb +4 -1
  31. data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
  32. data/lib/lutaml/key_value.rb +4 -21
  33. data/lib/lutaml/model/attribute.rb +38 -4
  34. data/lib/lutaml/model/collection.rb +7 -2
  35. data/lib/lutaml/model/compiled_rule.rb +1 -1
  36. data/lib/lutaml/model/config.rb +2 -2
  37. data/lib/lutaml/model/consolidation_map.rb +2 -2
  38. data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
  39. data/lib/lutaml/model/errors.rb +2 -2
  40. data/lib/lutaml/model/global_context.rb +4 -2
  41. data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
  42. data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
  43. data/lib/lutaml/model/runtime_compatibility.rb +2 -2
  44. data/lib/lutaml/model/schema/class_loader.rb +2 -1
  45. data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
  46. data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
  47. data/lib/lutaml/model/schema/file_writer.rb +5 -2
  48. data/lib/lutaml/model/schema/renderers/base.rb +4 -3
  49. data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
  50. data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
  51. data/lib/lutaml/model/schema/renderers/model.rb +18 -6
  52. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
  53. data/lib/lutaml/model/schema/renderers.rb +2 -1
  54. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
  55. data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
  56. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
  57. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
  58. data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
  59. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
  60. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
  61. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
  62. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
  63. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
  64. data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
  65. data/lib/lutaml/model/sequence.rb +4 -4
  66. data/lib/lutaml/model/serialization_adapter.rb +1 -1
  67. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
  68. data/lib/lutaml/model/serialize/format_conversion.rb +40 -9
  69. data/lib/lutaml/model/serialize/initialization.rb +9 -1
  70. data/lib/lutaml/model/serialize/model_import.rb +1 -0
  71. data/lib/lutaml/model/serialize.rb +22 -3
  72. data/lib/lutaml/model/transformation.rb +5 -0
  73. data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
  74. data/lib/lutaml/model/type.rb +9 -0
  75. data/lib/lutaml/model/type_registry.rb +2 -2
  76. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  77. data/lib/lutaml/model/union.rb +5 -2
  78. data/lib/lutaml/model/utils.rb +2 -2
  79. data/lib/lutaml/model/value_policy.rb +27 -0
  80. data/lib/lutaml/model/version.rb +1 -1
  81. data/lib/lutaml/model.rb +17 -16
  82. data/lib/lutaml/rdf/mapping.rb +2 -2
  83. data/lib/lutaml/rdf/namespace_set.rb +2 -2
  84. data/lib/lutaml/rdf/transform.rb +2 -2
  85. data/lib/lutaml/toml/format.rb +48 -0
  86. data/lib/lutaml/toml.rb +5 -46
  87. data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
  88. data/lib/lutaml/xml/builder/base.rb +2 -2
  89. data/lib/lutaml/xml/format.rb +331 -0
  90. data/lib/lutaml/xml/mapping.rb +26 -2
  91. data/lib/lutaml/xml/mapping_rule.rb +62 -3
  92. data/lib/lutaml/xml/model_transform.rb +183 -55
  93. data/lib/lutaml/xml/namespace_collector.rb +3 -3
  94. data/lib/lutaml/xml/parse_session.rb +59 -0
  95. data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
  96. data/lib/lutaml/xml/schema/builder.rb +5 -5
  97. data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
  98. data/lib/lutaml/xml/schema/xsd.rb +0 -2
  99. data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
  100. data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
  101. data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
  102. data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
  103. data/lib/lutaml/xml/transformation.rb +29 -0
  104. data/lib/lutaml/xml/xml_element.rb +2 -0
  105. data/lib/lutaml/xml.rb +4 -328
  106. data/lib/lutaml/yaml/format.rb +30 -0
  107. data/lib/lutaml/yaml.rb +5 -28
  108. data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
  109. data/lib/lutaml/yamls/format.rb +22 -0
  110. data/lib/lutaml/yamls.rb +5 -19
  111. data/lib/tasks/benchmark_runner.rb +2 -2
  112. data/lutaml-model.gemspec +1 -1
  113. data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
  114. data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
  115. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
  116. data/spec/lutaml/model/attribute_spec.rb +8 -3
  117. data/spec/lutaml/model/collection_spec.rb +1 -1
  118. data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
  119. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
  120. data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
  121. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
  122. data/spec/lutaml/model/defaults_spec.rb +45 -0
  123. data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
  124. data/spec/lutaml/model/json_spec.rb +1 -2
  125. data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
  126. data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
  127. data/spec/lutaml/model/store_spec.rb +3 -1
  128. data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
  129. data/spec/lutaml/model/union_attribute_spec.rb +4 -2
  130. data/spec/lutaml/model/value_policy_spec.rb +82 -0
  131. data/spec/lutaml/require_load_order_spec.rb +25 -0
  132. data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
  133. data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
  134. data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
  135. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
  136. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
  137. data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
  138. data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
  139. 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, { nil: :empty }),
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, collection: custom_collection_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, { nil: :omitted })).to eq(uninit)
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("", { empty: :empty })).to eq("")
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([], { empty: :empty }),
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, { omitted: :nil })).to be_nil
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, { omitted: :empty })).to eq("")
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, { empty: :nil, omitted: :empty }),
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("", { empty: false })).to be(false)
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, { omitted: true })).to be(true)
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("", { from: { empty: false } })).to be(false)
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, { from: { omitted: true } })).to be(true)
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("", { from: { empty: false } })).to be(false)
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, instance)).to eq("Hello, Alice")
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 { attr.validate_value!(nil, register, instance_object: instance) }
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 { attr.validate_value!(uninit, register, instance_object: model) }
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, anything)
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")