lutaml-model 0.8.100 → 0.8.119
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/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +28 -0
- data/lib/lutaml/key_value/transform.rb +4 -0
- data/lib/lutaml/key_value/transformation.rb +10 -0
- data/lib/lutaml/model/adapter_resolver.rb +13 -5
- data/lib/lutaml/model/attribute.rb +77 -3
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +21 -3
- data/lib/lutaml/model/serialize/initialization.rb +14 -1
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +1 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -45
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +276 -58
- data/lib/lutaml/xml/leptris/plan_serializer.rb +309 -47
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +25 -84
- data/lib/lutaml/xml/model_transform.rb +5 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +15 -6
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +29 -0
- data/lutaml-model.gemspec +3 -3
- data/spec/lutaml/json/yeptris_adapter_spec.rb +3 -10
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +32 -8
- data/spec/lutaml/model/cast_element_form_spec.rb +54 -0
- data/spec/lutaml/model/collection_strict_assignment_spec.rb +55 -0
- data/spec/lutaml/model/from_json_scalar_cast_spec.rb +70 -0
- data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/yeptris_native_detection_spec.rb +83 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +34 -6
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +3 -10
- metadata +19 -10
|
@@ -78,12 +78,21 @@ module Lutaml
|
|
|
78
78
|
|
|
79
79
|
def add_xml_fragment_or_raw_content(parent, fragment_string)
|
|
80
80
|
require "moxml" unless defined?(Moxml)
|
|
81
|
-
fragment_doc =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
fragment_doc = begin
|
|
82
|
+
Moxml.new.parse(fragment_string, fragment: true)
|
|
83
|
+
rescue Moxml::ParseError
|
|
84
|
+
# Engines that reject multi-root fragments outright
|
|
85
|
+
# (lutaml/moxml#188) — fall through to the synthetic root.
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
# The fallback keys on the RESULT, not the exception: recover-mode
|
|
89
|
+
# engines return an empty document for multi-root fragments instead
|
|
90
|
+
# of raising, which used to leave the synthetic-root path dead and
|
|
91
|
+
# the fragment silently dropped.
|
|
92
|
+
if fragment_doc&.children&.any?(&:element?)
|
|
93
|
+
return add_fragment_children_to_parent(fragment_doc, parent)
|
|
94
|
+
end
|
|
95
|
+
|
|
87
96
|
wrapped = Moxml.new.parse(
|
|
88
97
|
"<lutaml-fragment>#{fragment_string}</lutaml-fragment>",
|
|
89
98
|
)
|
|
@@ -40,6 +40,35 @@ module Lutaml
|
|
|
40
40
|
end
|
|
41
41
|
private_class_method :require_engine
|
|
42
42
|
|
|
43
|
+
# True where the yeptris engine loads (libyeptris present for
|
|
44
|
+
# this platform) AND answers a probe parse. A require alone
|
|
45
|
+
# does not prove the engine: a half-initialized Yeptris
|
|
46
|
+
# constant (the poison-process state the gem documents)
|
|
47
|
+
# passes const_defined? and require while every parse fails,
|
|
48
|
+
# and the native parse error then surfaces on valid input.
|
|
49
|
+
# Idempotent; a missing or broken install reports false
|
|
50
|
+
# instead of raising, so optional-engine callers (detection)
|
|
51
|
+
# can gate on it and fall back to the standard lane.
|
|
52
|
+
def self.available?
|
|
53
|
+
require_engine
|
|
54
|
+
native_ready?
|
|
55
|
+
rescue LoadError
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.native_ready?
|
|
60
|
+
return @native_ready unless @native_ready.nil?
|
|
61
|
+
|
|
62
|
+
@native_ready = begin
|
|
63
|
+
# rubocop:disable-next Naming/VariableNumber -- upstream schema literal
|
|
64
|
+
::Yeptris::YAML.load("native_probe: 1", schema: :compat_11)
|
|
65
|
+
true
|
|
66
|
+
rescue ::Yeptris::Error, LoadError, NoMemoryError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
private_class_method :native_ready?
|
|
71
|
+
|
|
43
72
|
# The parse-error contract is the standard adapter's: invalid
|
|
44
73
|
# input raises Psych::SyntaxError regardless of the engine, so
|
|
45
74
|
# consumers' rescue ladders (and this gem's own
|
data/lutaml-model.gemspec
CHANGED
|
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
34
34
|
|
|
35
35
|
# required for liquid
|
|
36
|
-
spec.add_dependency "base64"
|
|
36
|
+
spec.add_dependency "base64", ">= 0.1.1"
|
|
37
37
|
spec.add_dependency "bigdecimal"
|
|
38
38
|
spec.add_dependency "canon"
|
|
39
39
|
spec.add_dependency "concurrent-ruby"
|
|
@@ -41,10 +41,10 @@ Gem::Specification.new do |spec|
|
|
|
41
41
|
# 0.5.84 always passes XML_PARSE_HUGE to libxml2: very large documents
|
|
42
42
|
# parse completely instead of being silently truncated at the 10 MB
|
|
43
43
|
# input-buffer cap (lutaml-model#871).
|
|
44
|
-
spec.add_dependency "moxml", "~> 0.5.
|
|
44
|
+
spec.add_dependency "moxml", "~> 0.5.139"
|
|
45
45
|
spec.add_dependency "ostruct"
|
|
46
46
|
spec.add_dependency "rubyzip", "~> 3.7"
|
|
47
|
-
spec.add_dependency "thor"
|
|
47
|
+
spec.add_dependency "thor", "~> 1.4"
|
|
48
48
|
|
|
49
49
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
50
50
|
end
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
begin
|
|
5
|
-
require "lutaml/json/adapter/yeptris_adapter"
|
|
6
|
-
rescue LoadError
|
|
7
|
-
# yeptris ships no platform gem here (e.g. Windows): every example skips.
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
YEPTRIS_AVAILABLE = defined?(Yeptris)
|
|
3
|
+
require "lutaml/json/adapter/yeptris_adapter"
|
|
11
4
|
|
|
12
|
-
RSpec.describe
|
|
13
|
-
before { skip "yeptris is not available on this platform" unless
|
|
5
|
+
RSpec.describe Lutaml::Json::Adapter::YeptrisAdapter do
|
|
6
|
+
before { skip "yeptris is not available on this platform" unless described_class.available? }
|
|
14
7
|
|
|
15
8
|
let(:attributes) { { "name" => "John", "age" => 30, "roles" => %w[admin dev] } }
|
|
16
9
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
+
require "open3"
|
|
4
5
|
|
|
5
6
|
RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for with a configured type" do
|
|
6
7
|
after do
|
|
@@ -22,17 +23,40 @@ RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for with a configured t
|
|
|
22
23
|
expect(adapter.name).to include("Json::Standard")
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
# Runs in a subprocess: whether the engine is already loaded is a
|
|
27
|
+
# process-global fact, so any in-process assertion would depend on
|
|
28
|
+
# spec file order (the yeptris adapter faces load it).
|
|
25
29
|
it "does not load the optional yeptris engine when a standard adapter is configured" do
|
|
26
|
-
|
|
30
|
+
probe = <<~RUBY
|
|
31
|
+
require "lutaml/model"
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Lutaml::Model::Config.configure do |config|
|
|
34
|
+
config.yaml_adapter_type = :standard
|
|
35
|
+
config.json_adapter_type = :standard
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
adapter_yaml = Lutaml::Model::AdapterResolver.adapter_for(:yaml)
|
|
39
|
+
adapter_json = Lutaml::Model::AdapterResolver.adapter_for(:json)
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
# Parsing through the configured adapters must likewise stay on
|
|
42
|
+
# the standard engines.
|
|
43
|
+
adapter_yaml.parse("a: 1")
|
|
44
|
+
adapter_json.parse('{"a":1}')
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
exit(Object.const_defined?(:Yeptris) ? 1 : 0)
|
|
47
|
+
RUBY
|
|
48
|
+
|
|
49
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", probe)
|
|
50
|
+
expect(status).to be_success, stderr
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "plan lane defaults" do
|
|
54
|
+
it "ships the ordered plan lane on, floored to the memo-capable engine" do
|
|
55
|
+
# leptris#1743 (exponential flatten on diamond-shared plan
|
|
56
|
+
# graphs) shipped its identity-memo flatten_plan in 1.9.366.2;
|
|
57
|
+
# the MIN_LEPTRIS_VERSION floor keeps memo-less engines on the
|
|
58
|
+
# interpretive path, so the default-on lane is safe.
|
|
59
|
+
expect(Lutaml::Model::Config.instance.xml_plan_ordered_path).to be(true)
|
|
60
|
+
end
|
|
37
61
|
end
|
|
38
62
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# The scalar-cast diversion (#978) must not capture element-form input:
|
|
6
|
+
# a type declaring its own .cast still receives parsed child elements
|
|
7
|
+
# through the mapping lane, which builds the model from its XML
|
|
8
|
+
# mapping. Diverting those into .cast raised IncorrectModelError
|
|
9
|
+
# downstream (uniword shared-schemas round-trips on 0.8.116/0.8.117).
|
|
10
|
+
RSpec.describe "cast routing for element-form input" do
|
|
11
|
+
let(:styled_class) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :value, :string
|
|
14
|
+
|
|
15
|
+
xml do
|
|
16
|
+
element "style"
|
|
17
|
+
map_attribute "value", to: :value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
key_value do
|
|
21
|
+
map "value", to: :value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.cast(value)
|
|
25
|
+
return value if value.is_a?(self)
|
|
26
|
+
|
|
27
|
+
new(value: value.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:host_class) do
|
|
33
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
34
|
+
attribute :style, "CastStyled"
|
|
35
|
+
|
|
36
|
+
xml do
|
|
37
|
+
element "host"
|
|
38
|
+
map_element "style", to: :style
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
before do
|
|
44
|
+
stub_const("CastStyled", styled_class)
|
|
45
|
+
stub_const("CastHost", host_class)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "builds element-form input through the mapping lane" do
|
|
49
|
+
model = CastHost.from_xml('<host><style value="normal"/></host>')
|
|
50
|
+
|
|
51
|
+
expect(model.style).to be_a(CastStyled)
|
|
52
|
+
expect(model.style.value).to eq("normal")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "strict collection assignment" do
|
|
6
|
+
let(:node_class) do
|
|
7
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :id, :string
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:holder_class) do
|
|
13
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
14
|
+
attribute :items, "Node", collection: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
stub_const("Node", node_class)
|
|
20
|
+
stub_const("Holder", holder_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises at the constructor boundary" do
|
|
24
|
+
expect do
|
|
25
|
+
Holder.new(items: [[Node.new(id: "a")], Node.new(id: "b")])
|
|
26
|
+
end.to raise_error(Lutaml::Model::TypeError, /item 0 is an Array/)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "raises at the writer boundary" do
|
|
30
|
+
holder = Holder.new
|
|
31
|
+
expect do
|
|
32
|
+
holder.items = [Node.new, [Node.new]]
|
|
33
|
+
end.to raise_error(Lutaml::Model::TypeError, /item 1 is an Array/)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "raises for a custom collection class" do
|
|
37
|
+
custom = Class.new(Lutaml::Model::Collection) do
|
|
38
|
+
instances :node, "Node"
|
|
39
|
+
end
|
|
40
|
+
stub_const("NodeBag", custom)
|
|
41
|
+
wrapper = Class.new(Lutaml::Model::Serializable) do
|
|
42
|
+
attribute :bag, "NodeBag"
|
|
43
|
+
end
|
|
44
|
+
stub_const("BagHolder", wrapper)
|
|
45
|
+
|
|
46
|
+
expect do
|
|
47
|
+
BagHolder.new(bag: [[Node.new]])
|
|
48
|
+
end.to raise_error(Lutaml::Model::TypeError, /nested arrays are not flattened/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "keeps flat assignment untouched" do
|
|
52
|
+
holder = Holder.new(items: [Node.new(id: "x")])
|
|
53
|
+
expect(holder.items.map(&:id)).to eq(["x"])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #978: a JSON scalar handed to a mapped-model attribute crashed with
|
|
6
|
+
# an internal NoMethodError (key? on String) wrapped as
|
|
7
|
+
# InvalidFormatError. A type that declares its own .cast owns scalar
|
|
8
|
+
# construction from that scalar; a cast-less type falls through to the
|
|
9
|
+
# legacy conversion chain, whose parse error surfaces the offending
|
|
10
|
+
# token — the remedy there is declaring .cast on the type.
|
|
11
|
+
RSpec.describe "from_json scalar into a mapped-model attribute" do
|
|
12
|
+
let(:marked_class) do
|
|
13
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
14
|
+
attribute :content, :string
|
|
15
|
+
|
|
16
|
+
key_value do
|
|
17
|
+
map "content", to: :content
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.cast(value)
|
|
21
|
+
return value if value.is_a?(self)
|
|
22
|
+
|
|
23
|
+
new(content: value.to_s)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:plain_class) do
|
|
29
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
30
|
+
attribute :content, :string
|
|
31
|
+
|
|
32
|
+
key_value do
|
|
33
|
+
map "content", to: :content
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
let(:holder_class) do
|
|
39
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
40
|
+
attribute :description, "ScalarMarked"
|
|
41
|
+
attribute :plain_desc, "ScalarPlain"
|
|
42
|
+
|
|
43
|
+
key_value do
|
|
44
|
+
map "description", to: :description
|
|
45
|
+
map "plain_desc", to: :plain_desc
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
stub_const("ScalarMarked", marked_class)
|
|
52
|
+
stub_const("ScalarPlain", plain_class)
|
|
53
|
+
stub_const("ScalarHolder", holder_class)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "constructs the mapped value through the type's .cast" do
|
|
57
|
+
model = ScalarHolder.from_json('{"description": "text"}')
|
|
58
|
+
|
|
59
|
+
expect(model.description).to be_a(ScalarMarked)
|
|
60
|
+
expect(model.description.content).to eq("text")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "falls through to the format parse error for a cast-less mapped type" do
|
|
64
|
+
expect { ScalarHolder.from_json('{"plain_desc": "text"}') }
|
|
65
|
+
.to raise_error(
|
|
66
|
+
Lutaml::Model::InvalidFormatError,
|
|
67
|
+
/unexpected token 'text'/,
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #971: a singular attribute may ask ingest to keep the leading value
|
|
6
|
+
# when the mapper collects more than one element (relaton's upstream
|
|
7
|
+
# bibitems carry multiple <formattedref> elements). Default stays
|
|
8
|
+
# strict: the over-count remains a CollectionTrueMissingError.
|
|
9
|
+
RSpec.describe "singular_tolerance attribute option" do
|
|
10
|
+
let(:item_class) do
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :content, :string
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
stub_const("ToleranceItem", item_class)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "option validation" do
|
|
21
|
+
it "accepts :first" do
|
|
22
|
+
expect do
|
|
23
|
+
Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
|
|
24
|
+
singular_tolerance: :first)
|
|
25
|
+
end.not_to raise_error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "accepts omission (strict default)" do
|
|
29
|
+
expect do
|
|
30
|
+
Lutaml::Model::Attribute.new(:ref, "ToleranceItem")
|
|
31
|
+
end.not_to raise_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "rejects unsupported tolerances" do
|
|
35
|
+
expect do
|
|
36
|
+
Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
|
|
37
|
+
singular_tolerance: :last)
|
|
38
|
+
end.to raise_error(ArgumentError, /singular_tolerance: :last/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "#tolerates_singular_overflow?" do
|
|
43
|
+
let(:tolerant) do
|
|
44
|
+
Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
|
|
45
|
+
singular_tolerance: :first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
let(:strict) { Lutaml::Model::Attribute.new(:ref, "ToleranceItem") }
|
|
49
|
+
|
|
50
|
+
let(:collection_attr) do
|
|
51
|
+
Lutaml::Model::Attribute.new(:refs, "ToleranceItem",
|
|
52
|
+
collection: true,
|
|
53
|
+
singular_tolerance: :first)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "is true for an over-ingested collection on a tolerant singular" do
|
|
57
|
+
expect(tolerant.tolerates_singular_overflow?([1, 2])).to be(true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "is false without the option" do
|
|
61
|
+
expect(strict.tolerates_singular_overflow?([1, 2])).to be(false)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "is false for a scalar on a tolerant singular" do
|
|
65
|
+
expect(tolerant.tolerates_singular_overflow?(1)).to be(false)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "is false for a collection attribute — the option does not apply" do
|
|
69
|
+
expect(collection_attr.tolerates_singular_overflow?([1, 2])).to be(false)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "strict default" do
|
|
74
|
+
it "still raises CollectionTrueMissingError on over-ingest" do
|
|
75
|
+
attr = Lutaml::Model::Attribute.new(:ref, "ToleranceItem")
|
|
76
|
+
|
|
77
|
+
expect do
|
|
78
|
+
attr.valid_collection!([item_class.new, item_class.new], String)
|
|
79
|
+
end.to raise_error(Lutaml::Model::CollectionTrueMissingError)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe "tolerant ingest" do
|
|
84
|
+
it "lets valid_collection! pass after the transform reduces to first" do
|
|
85
|
+
attr = Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
|
|
86
|
+
singular_tolerance: :first)
|
|
87
|
+
value = [item_class.new, item_class.new]
|
|
88
|
+
|
|
89
|
+
reduced = value.first if attr.tolerates_singular_overflow?(value)
|
|
90
|
+
expect(attr.valid_collection!(reduced, String)).to be(true)
|
|
91
|
+
expect(reduced).to eq(value.first)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "on the KeyValue lane" do
|
|
96
|
+
let(:marked_class) do
|
|
97
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
98
|
+
attribute :content, :string
|
|
99
|
+
|
|
100
|
+
key_value do
|
|
101
|
+
map "content", to: :content
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
let(:relation_class) do
|
|
107
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
108
|
+
attribute :rel_type, :string
|
|
109
|
+
attribute :description, "ToleranceMarked", singular_tolerance: :first
|
|
110
|
+
|
|
111
|
+
key_value do
|
|
112
|
+
map "type", to: :rel_type
|
|
113
|
+
map "description", to: :description
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
before do
|
|
119
|
+
stub_const("ToleranceMarked", marked_class)
|
|
120
|
+
stub_const("ToleranceRelation", relation_class)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "keeps the leading value on ingest instead of the whole array" do
|
|
124
|
+
model = ToleranceRelation.from_yaml(
|
|
125
|
+
"---\ntype: cites\ndescription:\n - content: First\n - content: Second\n",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(model.description).to be_a(ToleranceMarked)
|
|
129
|
+
expect(model.description.content).to eq("First")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "round-trips the collapsed singular through to_yaml" do
|
|
133
|
+
model = ToleranceRelation.from_yaml(
|
|
134
|
+
"---\ntype: cites\ndescription:\n - content: First\n - content: Second\n",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
yaml = model.to_yaml
|
|
138
|
+
expect(yaml).to include("First")
|
|
139
|
+
expect(yaml).not_to include("Second")
|
|
140
|
+
expect(ToleranceRelation.from_yaml(yaml).description.content).to eq("First")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "collapses an over-assigned collection at serialization" do
|
|
144
|
+
model = ToleranceRelation.new(
|
|
145
|
+
rel_type: "cites",
|
|
146
|
+
description: [ToleranceMarked.new(content: "One"),
|
|
147
|
+
ToleranceMarked.new(content: "Two")],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
yaml = model.to_yaml
|
|
151
|
+
expect(yaml).to include("One")
|
|
152
|
+
expect(yaml).not_to include("Two")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The KV mirror of the XML-lane cases: the YAML lane never consulted
|
|
158
|
+
# the tolerance — ingest stored the whole array and re-serialization
|
|
159
|
+
# raised the mapped-PORO type check.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Register ids and type-context ids are two namespaces that
|
|
6
|
+
# GlobalContext#resolve_type bridges implicitly: an id present only in the
|
|
7
|
+
# Register namespace (a flavor's `lutaml_default_register`) resolves to a
|
|
8
|
+
# nil type context, and every resolution shape must treat that as a
|
|
9
|
+
# legitimate "no namespace" input instead of crashing (metanorma-jis#531).
|
|
10
|
+
RSpec.describe "type resolution through a register id with no registered type context" do
|
|
11
|
+
let(:collection_class) do
|
|
12
|
+
Class.new(Lutaml::Model::Collection) do
|
|
13
|
+
instances :entries, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "resolves collection instance types under a nil context" do
|
|
18
|
+
collection = collection_class.new(["a"], lutaml_register: :jis_document)
|
|
19
|
+
|
|
20
|
+
expect(collection.entries).to eq(["a"])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "resolves built-in symbol names under a nil context" do
|
|
24
|
+
resolved = Lutaml::Model::GlobalContext.resolve_type(:string, :jis_document)
|
|
25
|
+
|
|
26
|
+
expect(resolved).to eq(Lutaml::Model::Type::String)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "raises UnknownTypeError for unknown names under a nil context" do
|
|
30
|
+
expect do
|
|
31
|
+
Lutaml::Model::GlobalContext.resolve_type(:no_such_type, :jis_document)
|
|
32
|
+
end.to raise_error(Lutaml::Model::UnknownTypeError)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "lutaml/json/adapter/yeptris_adapter"
|
|
6
|
+
|
|
7
|
+
# Surface 1 of #978: the yeptris lane was selected from a bare
|
|
8
|
+
# constant check, so a process holding a half-initialized Yeptris
|
|
9
|
+
# module (the poison-process state the gem documents) committed to
|
|
10
|
+
# the native lane and failed pre-mapping on valid JSON — while other
|
|
11
|
+
# processes fell back to the standard lane, flipping the adapter
|
|
12
|
+
# choice across runs. Detection now probes the native engine; a
|
|
13
|
+
# broken native deterministically rides the standard lane.
|
|
14
|
+
RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for yeptris native detection" do
|
|
15
|
+
let(:parse_probe) do
|
|
16
|
+
<<~RUBY
|
|
17
|
+
require "lutaml/model"
|
|
18
|
+
|
|
19
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
20
|
+
attribute :v, :string
|
|
21
|
+
key_value { map "v", to: :v }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
model = klass.from_json('{"v": "scalar"}')
|
|
25
|
+
adapter = Lutaml::Model::AdapterResolver.adapter_for(:json)
|
|
26
|
+
puts "ADAPTER=\#{adapter.name.split('::').last} PARSED=\#{model.v}"
|
|
27
|
+
RUBY
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subprocess_yeptris_capable?
|
|
31
|
+
probe = <<~RUBY
|
|
32
|
+
begin
|
|
33
|
+
require "yeptris"
|
|
34
|
+
exit(0)
|
|
35
|
+
rescue LoadError
|
|
36
|
+
exit(1)
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
_out, _err, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", probe)
|
|
40
|
+
status.success?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "resolves the yeptris lane when the native engine answers a probe" do
|
|
44
|
+
skip "optional yeptris engine not resolvable outside the bundle" unless subprocess_yeptris_capable?
|
|
45
|
+
|
|
46
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", parse_probe)
|
|
47
|
+
expect(status).to be_success, stderr
|
|
48
|
+
expect(stdout).to include("ADAPTER=YeptrisAdapter")
|
|
49
|
+
expect(stdout).to include("PARSED=scalar")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "falls back to the standard lane when the native engine cannot parse" do
|
|
53
|
+
poison = <<~RUBY
|
|
54
|
+
require "lutaml/model"
|
|
55
|
+
require "yeptris"
|
|
56
|
+
require "lutaml/json/adapter/yeptris_adapter"
|
|
57
|
+
|
|
58
|
+
# The poison state: the module is defined and require succeeds,
|
|
59
|
+
# but every native parse fails. Whatever the cause (a stale
|
|
60
|
+
# libyeptris, a half-initialized load), the lane must not be
|
|
61
|
+
# selected.
|
|
62
|
+
Lutaml::Json::Adapter::YeptrisAdapter
|
|
63
|
+
.define_singleton_method(:native_ready?) { false }
|
|
64
|
+
|
|
65
|
+
#{parse_probe}
|
|
66
|
+
RUBY
|
|
67
|
+
|
|
68
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", poison)
|
|
69
|
+
expect(status).to be_success, stderr
|
|
70
|
+
expect(stdout).to include("ADAPTER=StandardAdapter")
|
|
71
|
+
expect(stdout).to include("PARSED=scalar")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "reports the adapter unavailable when the native probe fails" do
|
|
75
|
+
allow(Lutaml::Json::Adapter::YeptrisAdapter)
|
|
76
|
+
.to receive(:native_ready?).and_return(false)
|
|
77
|
+
|
|
78
|
+
expect(Lutaml::Json::Adapter::YeptrisAdapter.available?).to be(false)
|
|
79
|
+
ensure
|
|
80
|
+
allow(Lutaml::Json::Adapter::YeptrisAdapter)
|
|
81
|
+
.to receive(:native_ready?).and_call_original
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/xml/adapter/leptris_adapter"
|
|
5
|
+
require "lutaml/xml/adapter/nokogiri_adapter"
|
|
6
|
+
|
|
7
|
+
# A failed parse reaches the empty-document guard as a document with no
|
|
8
|
+
# root, indistinguishable from genuinely empty input. The engine's parse
|
|
9
|
+
# diagnostics ride the document (moxml attachments :parse_errors) and
|
|
10
|
+
# must surface in the raised message — otherwise the error names the
|
|
11
|
+
# input's emptiness while the input is neither empty nor the problem
|
|
12
|
+
# (lutaml-model#956).
|
|
13
|
+
#
|
|
14
|
+
# moxml#369 changed the recoverable-malformation contract: the leptris
|
|
15
|
+
# adapter's non-strict parse now RECOVERS (the engine's recover mode,
|
|
16
|
+
# leptris#1660) and returns the best-effort tree instead of failing —
|
|
17
|
+
# a mismatched closer yields a document, not a raise. Only inputs with
|
|
18
|
+
# nothing to recover into (whitespace-only) still reach the guard,
|
|
19
|
+
# carrying the recovery diagnostic.
|
|
20
|
+
RSpec.describe "empty-document errors surface engine parse diagnostics" do
|
|
21
|
+
describe Lutaml::Xml::Adapter::LeptrisAdapter do
|
|
22
|
+
it "returns a recovered tree for recoverable malformations (moxml#369)" do
|
|
23
|
+
version = Gem.loaded_specs.fetch("moxml").version
|
|
24
|
+
recovered = version >= Gem::Version.new("0.5.131")
|
|
25
|
+
unless recovered
|
|
26
|
+
skip "moxml predates the recover-mode wiring (0.5.131)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
doc = described_class.parse("<root><a>x</b></root>")
|
|
30
|
+
expect(doc.root.name).to eq("root")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "reports the engine diagnostic for whitespace-only input" do
|
|
34
|
+
expect do
|
|
35
|
+
described_class.parse(" ")
|
|
36
|
+
end.to raise_error(Lutaml::Model::InvalidFormatError, /the XML engine reported:/)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe Lutaml::Xml::Adapter::NokogiriAdapter do
|
|
41
|
+
it "keeps the plain empty-document message when the engine reported nothing" do
|
|
42
|
+
expect do
|
|
43
|
+
described_class.parse("")
|
|
44
|
+
end.to raise_error(Lutaml::Model::InvalidFormatError) do |error|
|
|
45
|
+
expect(error.message).to include("The XML may be empty")
|
|
46
|
+
expect(error.message).not_to include("the XML engine reported:")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|