moxml 0.1.18 → 0.1.20
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/.rubocop_todo.yml +181 -11
- data/README.adoc +24 -1
- data/docs/_guides/node-api-consistency.adoc +4 -0
- data/lib/moxml/adapter/base.rb +6 -1
- data/lib/moxml/adapter/customized_ox/text.rb +15 -2
- data/lib/moxml/adapter/customized_rexml/formatter.rb +1 -0
- data/lib/moxml/adapter/libxml.rb +7 -2
- data/lib/moxml/adapter/oga.rb +6 -2
- data/lib/moxml/adapter/ox.rb +15 -8
- data/lib/moxml/builder.rb +12 -5
- data/lib/moxml/config.rb +1 -1
- data/lib/moxml/entity_registry.rb +1 -0
- data/lib/moxml/native_attachment/native.rb +69 -0
- data/lib/moxml/native_attachment/opal.rb +34 -0
- data/lib/moxml/native_attachment.rb +16 -46
- data/lib/moxml/text.rb +4 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +2 -1
- data/spec/integration/shared_examples/edge_cases.rb +4 -2
- data/spec/integration/shared_examples/entity_reference_whitespace.rb +1 -1
- data/spec/integration/shared_examples/high_level/document_builder_behavior.rb +3 -1
- data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +10 -4
- data/spec/integration/shared_examples/node_wrappers/node_set_behavior.rb +1 -1
- data/spec/moxml/adapter/headed_ox_spec.rb +1 -1
- data/spec/moxml/lazy_parse_spec.rb +1 -1
- data/spec/moxml/moxml_spec.rb +6 -40
- data/spec/moxml/native_attachment/native_spec.rb +57 -0
- data/spec/moxml/native_attachment/opal_spec.rb +29 -0
- data/spec/moxml/native_attachment/shared_examples.rb +43 -0
- data/spec/moxml/native_attachment_spec.rb +184 -0
- data/spec/moxml/text_spec.rb +23 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +3 -2
- data/spec/performance/benchmark_spec.rb +1 -1
- metadata +8 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
class NativeAttachment
|
|
5
|
+
# Opal adapter nodes are Ruby objects, so instance variables are sufficient
|
|
6
|
+
# for Moxml-owned attachments without relying on Monitor/Thread support.
|
|
7
|
+
class Opal
|
|
8
|
+
def get(native, key)
|
|
9
|
+
native.instance_variable_get(attachment_ivar_name(key))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set(native, key, value)
|
|
13
|
+
native.instance_variable_set(attachment_ivar_name(key), value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key?(native, key)
|
|
17
|
+
native.instance_variable_defined?(attachment_ivar_name(key))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delete(native, key)
|
|
21
|
+
ivar_name = attachment_ivar_name(key)
|
|
22
|
+
return unless native.instance_variable_defined?(ivar_name)
|
|
23
|
+
|
|
24
|
+
native.remove_instance_variable(ivar_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def attachment_ivar_name(key)
|
|
30
|
+
:"@moxml_attachment_#{key}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -1,65 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Moxml
|
|
4
|
-
# Stores Moxml-specific state associated with native adapter objects
|
|
5
|
-
# without polluting their internals.
|
|
6
|
-
#
|
|
7
|
-
# Uses object_id as key with GC finalizer cleanup to prevent memory leaks.
|
|
8
|
-
# Thread-safe via Monitor (reentrant-safe).
|
|
9
|
-
#
|
|
10
|
-
# Replaces the anti-pattern of using instance_variable_set/get on
|
|
11
|
-
# foreign library objects (Nokogiri, REXML, Oga, Ox, LibXML nodes).
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# attachments = NativeAttachment.new
|
|
15
|
-
# attachments.set(native_element, :entity_refs, [])
|
|
16
|
-
# refs = attachments.get(native_element, :entity_refs)
|
|
17
|
-
# attachments.key?(native_element, :doctype) #=> false
|
|
18
4
|
class NativeAttachment
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
autoload :Opal, "moxml/native_attachment/opal"
|
|
6
|
+
autoload :Native, "moxml/native_attachment/native"
|
|
7
|
+
|
|
8
|
+
def self.default_backend
|
|
9
|
+
constant = RUBY_ENGINE == "opal" ? :Opal : :Native
|
|
10
|
+
const_get(constant).new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :backend
|
|
14
|
+
|
|
15
|
+
def initialize(backend: self.class.default_backend)
|
|
16
|
+
@backend = backend
|
|
23
17
|
end
|
|
24
18
|
|
|
25
19
|
def get(native, key)
|
|
26
|
-
@
|
|
20
|
+
@backend.get(native, key)
|
|
27
21
|
end
|
|
28
22
|
|
|
29
23
|
def set(native, key, value)
|
|
30
|
-
|
|
31
|
-
@monitor.synchronize do
|
|
32
|
-
@data[id] ||= {}
|
|
33
|
-
@data[id][key] = value
|
|
34
|
-
register_finalizer(native, id) unless @finalizer_registered[id]
|
|
35
|
-
end
|
|
24
|
+
@backend.set(native, key, value)
|
|
36
25
|
end
|
|
37
26
|
|
|
38
27
|
def key?(native, key)
|
|
39
|
-
@
|
|
28
|
+
@backend.key?(native, key)
|
|
40
29
|
end
|
|
41
30
|
|
|
42
31
|
def delete(native, key)
|
|
43
|
-
@
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def register_finalizer(native, id)
|
|
49
|
-
@finalizer_registered[id] = true
|
|
50
|
-
ObjectSpace.define_finalizer(native, finalizer_for(id))
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def finalizer_for(id)
|
|
54
|
-
data = @data
|
|
55
|
-
registered = @finalizer_registered
|
|
56
|
-
# Finalizers must NOT use Mutex/Monitor (can't be called from trap context).
|
|
57
|
-
# Direct Hash operations are safe here since finalizers run sequentially
|
|
58
|
-
# and the GC'd object's id won't be accessed by any other thread.
|
|
59
|
-
proc do
|
|
60
|
-
data.delete(id)
|
|
61
|
-
registered.delete(id)
|
|
62
|
-
end
|
|
32
|
+
@backend.delete(native, key)
|
|
63
33
|
end
|
|
64
34
|
end
|
|
65
35
|
end
|
data/lib/moxml/text.rb
CHANGED
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -1758,7 +1758,8 @@ module Moxml
|
|
|
1758
1758
|
current = visit.pop
|
|
1759
1759
|
|
|
1760
1760
|
# Function name is stored in :value field, not children
|
|
1761
|
-
if
|
|
1761
|
+
if %i[call
|
|
1762
|
+
function].include?(current.type) && current.value == name
|
|
1762
1763
|
return true
|
|
1763
1764
|
end
|
|
1764
1765
|
|
|
@@ -170,7 +170,9 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
170
170
|
describe "whitespace text node preservation" do
|
|
171
171
|
# Ox/HeadedOx do not generate whitespace-only text nodes in their parser,
|
|
172
172
|
# so these tests only apply to adapters that do (Nokogiri, OGA, REXML, LibXML)
|
|
173
|
-
let(:preserves_ws)
|
|
173
|
+
let(:preserves_ws) do
|
|
174
|
+
!%i[ox headed_ox].include?(context.config.adapter_name)
|
|
175
|
+
end
|
|
174
176
|
|
|
175
177
|
it "preserves whitespace-only text nodes between sibling elements" do
|
|
176
178
|
unless preserves_ws
|
|
@@ -196,7 +198,7 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
196
198
|
expect(ws_nodes).not_to be_empty
|
|
197
199
|
|
|
198
200
|
# Element children should still be accessible
|
|
199
|
-
elements = children.
|
|
201
|
+
elements = children.grep(Moxml::Element)
|
|
200
202
|
expect(elements.map(&:name)).to eq(%w[a b c])
|
|
201
203
|
end
|
|
202
204
|
|
|
@@ -116,7 +116,7 @@ RSpec.shared_examples "Entity Reference Whitespace Preservation" do
|
|
|
116
116
|
children = doc.root.children
|
|
117
117
|
|
|
118
118
|
# Whitespace text nodes between elements are preserved
|
|
119
|
-
elements = children.
|
|
119
|
+
elements = children.grep(Moxml::Element)
|
|
120
120
|
expect(elements.length).to eq(2)
|
|
121
121
|
expect(elements.map(&:name)).to eq(%w[child1 child2])
|
|
122
122
|
end
|
|
@@ -38,7 +38,9 @@ RSpec.shared_examples "Moxml::DocumentBuilder" do
|
|
|
38
38
|
expect(non_ws_children[1]).to be_a(Moxml::Element)
|
|
39
39
|
expect(non_ws_children[1].name).to eq("child")
|
|
40
40
|
expect(non_ws_children[1]["id"]).to eq("1")
|
|
41
|
-
expect(non_ws_children[1].children.find
|
|
41
|
+
expect(non_ws_children[1].children.find do |c|
|
|
42
|
+
c.is_a?(Moxml::Cdata)
|
|
43
|
+
end).to be_a(Moxml::Cdata)
|
|
42
44
|
expect(non_ws_children[2]).to be_a(Moxml::ProcessingInstruction)
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -129,16 +129,20 @@ RSpec.shared_examples "Moxml::EntityReference" do
|
|
|
129
129
|
|
|
130
130
|
describe "entity restoration" do
|
|
131
131
|
it "restores standard XML entities when enabled" do
|
|
132
|
-
ctx_restore = Moxml.new(context.config.adapter_name)
|
|
132
|
+
ctx_restore = Moxml.new(context.config.adapter_name) do |c|
|
|
133
|
+
c.restore_entities = true
|
|
134
|
+
end
|
|
133
135
|
doc = ctx_restore.parse("<p>a&b</p>")
|
|
134
136
|
output = doc.to_xml
|
|
135
137
|
expect(output).to include("&")
|
|
136
138
|
end
|
|
137
139
|
|
|
138
140
|
it "does not create entity references when disabled" do
|
|
139
|
-
ctx_no_restore = Moxml.new(context.config.adapter_name)
|
|
141
|
+
ctx_no_restore = Moxml.new(context.config.adapter_name) do |c|
|
|
142
|
+
c.restore_entities = false
|
|
143
|
+
end
|
|
140
144
|
doc = ctx_no_restore.parse("<p>text</p>")
|
|
141
|
-
refs = doc.root.children.
|
|
145
|
+
refs = doc.root.children.grep(Moxml::EntityReference)
|
|
142
146
|
expect(refs).to be_empty
|
|
143
147
|
end
|
|
144
148
|
end
|
|
@@ -197,7 +201,9 @@ RSpec.shared_examples "Moxml::EntityReference" do
|
|
|
197
201
|
|
|
198
202
|
it "rejects invalid modes" do
|
|
199
203
|
cfg = Moxml::Config.new(context.config.adapter_name)
|
|
200
|
-
expect
|
|
204
|
+
expect do
|
|
205
|
+
cfg.entity_restoration_mode = :bogus
|
|
206
|
+
end.to raise_error(ArgumentError)
|
|
201
207
|
end
|
|
202
208
|
|
|
203
209
|
it "restores non-standard entities in lenient mode" do
|
|
@@ -42,7 +42,7 @@ RSpec.shared_examples "Moxml::NodeSet" do
|
|
|
42
42
|
|
|
43
43
|
it "compares nodes" do
|
|
44
44
|
xpath_results = doc.xpath("//child")
|
|
45
|
-
element_children = doc.root.children.
|
|
45
|
+
element_children = doc.root.children.grep(Moxml::Element)
|
|
46
46
|
expect(xpath_results.map(&:native)).to eq(element_children.map(&:native))
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -145,7 +145,7 @@ RSpec.describe Moxml::Adapter::HeadedOx do
|
|
|
145
145
|
it "returns first matching node" do
|
|
146
146
|
result = adapter.at_xpath(doc, "//book")
|
|
147
147
|
|
|
148
|
-
expect(result).to be_a(
|
|
148
|
+
expect(result).to be_a(Ox::Element)
|
|
149
149
|
expect(result.name).to eq("book")
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -86,7 +86,7 @@ RSpec.describe "Moxml lazy parse" do
|
|
|
86
86
|
|
|
87
87
|
it "round-trips through serialize" do
|
|
88
88
|
doc = ctx.parse(xml)
|
|
89
|
-
serialized = doc.to_xml
|
|
89
|
+
serialized = doc.to_xml(indent: 0)
|
|
90
90
|
doc2 = ctx.parse(serialized)
|
|
91
91
|
expect(doc2.root.name).to eq("root")
|
|
92
92
|
expect(doc2.root.children.to_a.size).to eq(2)
|
data/spec/moxml/moxml_spec.rb
CHANGED
|
@@ -2,28 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# spec/moxml_spec.rb
|
|
4
4
|
RSpec.describe Moxml do
|
|
5
|
-
around do |example|
|
|
6
|
-
original_default = Moxml::Config.instance_variable_get(:@default)
|
|
7
|
-
original_default_adapter = Moxml::Config.instance_variable_get(:@default_adapter)
|
|
8
|
-
|
|
9
|
-
Moxml::Config.remove_instance_variable(:@default) if Moxml::Config.instance_variable_defined?(:@default)
|
|
10
|
-
Moxml::Config.remove_instance_variable(:@default_adapter) if Moxml::Config.instance_variable_defined?(:@default_adapter)
|
|
11
|
-
|
|
12
|
-
example.run
|
|
13
|
-
ensure
|
|
14
|
-
if original_default.nil?
|
|
15
|
-
Moxml::Config.remove_instance_variable(:@default) if Moxml::Config.instance_variable_defined?(:@default)
|
|
16
|
-
else
|
|
17
|
-
Moxml::Config.instance_variable_set(:@default, original_default)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
if original_default_adapter.nil?
|
|
21
|
-
Moxml::Config.remove_instance_variable(:@default_adapter) if Moxml::Config.instance_variable_defined?(:@default_adapter)
|
|
22
|
-
else
|
|
23
|
-
Moxml::Config.instance_variable_set(:@default_adapter, original_default_adapter)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
5
|
it "has a version number" do
|
|
28
6
|
expect(Moxml::VERSION).not_to be_nil
|
|
29
7
|
end
|
|
@@ -45,7 +23,6 @@ RSpec.describe Moxml do
|
|
|
45
23
|
|
|
46
24
|
describe ".configure" do
|
|
47
25
|
around do |example|
|
|
48
|
-
# preserve the original config because it may be changed in examples
|
|
49
26
|
described_class.with_config { example.run }
|
|
50
27
|
end
|
|
51
28
|
|
|
@@ -56,23 +33,6 @@ RSpec.describe Moxml do
|
|
|
56
33
|
expect(context.config.adapter_name).to eq(:nokogiri)
|
|
57
34
|
end
|
|
58
35
|
|
|
59
|
-
it "defaults to oga on Opal" do
|
|
60
|
-
stub_const("RUBY_ENGINE", "opal")
|
|
61
|
-
|
|
62
|
-
context = described_class.new
|
|
63
|
-
expect(context.config.adapter_name).to eq(:oga)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "prefers ox when it is already loaded" do
|
|
67
|
-
allow(Object).to receive(:const_defined?).and_call_original
|
|
68
|
-
allow(Object).to receive(:const_defined?).with(:Nokogiri).and_return(false)
|
|
69
|
-
allow(Object).to receive(:const_defined?).with(:Ox).and_return(true)
|
|
70
|
-
allow(Object).to receive(:const_defined?).with(:Oga).and_return(false)
|
|
71
|
-
|
|
72
|
-
context = described_class.new
|
|
73
|
-
expect(context.config.adapter_name).to eq(:ox)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
36
|
it "uses configured options from the block" do
|
|
77
37
|
described_class.configure do |config|
|
|
78
38
|
config.default_adapter = :oga
|
|
@@ -86,4 +46,10 @@ RSpec.describe Moxml do
|
|
|
86
46
|
expect(context.config.default_encoding).to eq("US-ASCII")
|
|
87
47
|
end
|
|
88
48
|
end
|
|
49
|
+
|
|
50
|
+
describe "Config.runtime_default_adapter" do
|
|
51
|
+
it "returns a valid adapter for the current runtime" do
|
|
52
|
+
expect(Moxml::Config::VALID_ADAPTERS).to include(Moxml::Config.runtime_default_adapter)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
89
55
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "shared_examples"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::NativeAttachment::Native do
|
|
7
|
+
it_behaves_like "an attachment backend"
|
|
8
|
+
|
|
9
|
+
it "stores attachments outside native object instance variables" do
|
|
10
|
+
attachments = described_class.new
|
|
11
|
+
native = Object.new
|
|
12
|
+
|
|
13
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
14
|
+
|
|
15
|
+
aggregate_failures do
|
|
16
|
+
expect(attachments.get(native, :entity_refs)).to eq(["amp"])
|
|
17
|
+
expect(native.instance_variables).to be_empty
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "registers one finalizer per native object that clears sidecar storage" do
|
|
22
|
+
attachments = described_class.new
|
|
23
|
+
native = Object.new
|
|
24
|
+
other_native = Object.new
|
|
25
|
+
finalizers = {}.compare_by_identity
|
|
26
|
+
|
|
27
|
+
expect(ObjectSpace).to receive(:define_finalizer)
|
|
28
|
+
.with(native, kind_of(Proc)).once do |object, finalizer|
|
|
29
|
+
finalizers[object] = finalizer
|
|
30
|
+
end
|
|
31
|
+
expect(ObjectSpace).to receive(:define_finalizer)
|
|
32
|
+
.with(other_native, kind_of(Proc)).once do |object, finalizer|
|
|
33
|
+
finalizers[object] = finalizer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
37
|
+
attachments.set(native, :doctype, "html")
|
|
38
|
+
attachments.set(other_native, :entity_refs, ["lt"])
|
|
39
|
+
|
|
40
|
+
native_id = native.object_id
|
|
41
|
+
data = attachments.instance_variable_get(:@data)
|
|
42
|
+
registered = attachments.instance_variable_get(:@finalizer_registered)
|
|
43
|
+
|
|
44
|
+
aggregate_failures do
|
|
45
|
+
expect(data).to have_key(native_id)
|
|
46
|
+
expect(registered).to have_key(native_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
finalizers.fetch(native).call
|
|
50
|
+
|
|
51
|
+
aggregate_failures do
|
|
52
|
+
expect(data).not_to have_key(native_id)
|
|
53
|
+
expect(registered).not_to have_key(native_id)
|
|
54
|
+
expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "shared_examples"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::NativeAttachment::Opal do
|
|
7
|
+
it_behaves_like "an attachment backend"
|
|
8
|
+
|
|
9
|
+
it "stores attachments in Moxml-owned instance variables" do
|
|
10
|
+
attachments = described_class.new
|
|
11
|
+
native = Object.new
|
|
12
|
+
|
|
13
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
14
|
+
|
|
15
|
+
expect(native.instance_variable_get(:@moxml_attachment_entity_refs))
|
|
16
|
+
.to eq(["amp"])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "removes the attachment instance variable on delete" do
|
|
20
|
+
attachments = described_class.new
|
|
21
|
+
native = Object.new
|
|
22
|
+
|
|
23
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
24
|
+
attachments.delete(native, :entity_refs)
|
|
25
|
+
|
|
26
|
+
expect(native.instance_variable_defined?(:@moxml_attachment_entity_refs))
|
|
27
|
+
.to be(false)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples "an attachment backend" do
|
|
4
|
+
subject(:attachments) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:native) { Object.new }
|
|
7
|
+
let(:other_native) { Object.new }
|
|
8
|
+
|
|
9
|
+
it "stores and reads attachments by native object and key" do
|
|
10
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
11
|
+
attachments.set(native, :doctype, "html")
|
|
12
|
+
attachments.set(other_native, :entity_refs, ["lt"])
|
|
13
|
+
|
|
14
|
+
aggregate_failures do
|
|
15
|
+
expect(attachments.get(native, :entity_refs)).to eq(["amp"])
|
|
16
|
+
expect(attachments.get(native, :doctype)).to eq("html")
|
|
17
|
+
expect(attachments.get(other_native, :entity_refs)).to eq(["lt"])
|
|
18
|
+
expect(attachments.key?(native, :entity_refs)).to be(true)
|
|
19
|
+
expect(attachments.key?(native, :missing)).to be(false)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "preserves explicit nil attachments" do
|
|
24
|
+
attachments.set(native, :xml_declaration, nil)
|
|
25
|
+
|
|
26
|
+
aggregate_failures do
|
|
27
|
+
expect(attachments.get(native, :xml_declaration)).to be_nil
|
|
28
|
+
expect(attachments.key?(native, :xml_declaration)).to be(true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "deletes attachments" do
|
|
33
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
34
|
+
|
|
35
|
+
expect(attachments.delete(native, :entity_refs)).to eq(["amp"])
|
|
36
|
+
|
|
37
|
+
aggregate_failures do
|
|
38
|
+
expect(attachments.get(native, :entity_refs)).to be_nil
|
|
39
|
+
expect(attachments.key?(native, :entity_refs)).to be(false)
|
|
40
|
+
expect(attachments.delete(native, :entity_refs)).to be_nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "spec_helper"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::NativeAttachment do
|
|
8
|
+
describe ".default_backend" do
|
|
9
|
+
it "uses the Opal backend under Opal" do
|
|
10
|
+
stub_const("RUBY_ENGINE", "opal")
|
|
11
|
+
|
|
12
|
+
expect(described_class.default_backend).to be_a(described_class::Opal)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "uses the native backend under non-Opal Ruby engines" do
|
|
16
|
+
stub_const("RUBY_ENGINE", "ruby")
|
|
17
|
+
|
|
18
|
+
expect(described_class.default_backend).to be_a(described_class::Native)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "facade" do
|
|
23
|
+
subject(:attachments) { described_class.new }
|
|
24
|
+
|
|
25
|
+
let(:native) { Object.new }
|
|
26
|
+
|
|
27
|
+
it "delegates storage calls to the configured backend" do
|
|
28
|
+
backend = Class.new do
|
|
29
|
+
attr_reader :calls
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@calls = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get(native, key)
|
|
36
|
+
@calls << [:get, native, key]
|
|
37
|
+
:stored_value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def set(native, key, value)
|
|
41
|
+
@calls << [:set, native, key, value]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key?(native, key)
|
|
45
|
+
@calls << [:key?, native, key]
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete(native, key)
|
|
50
|
+
@calls << [:delete, native, key]
|
|
51
|
+
:stored_value
|
|
52
|
+
end
|
|
53
|
+
end.new
|
|
54
|
+
attachments = described_class.new(backend: backend)
|
|
55
|
+
|
|
56
|
+
expect(attachments.get(native, :entity_refs)).to eq(:stored_value)
|
|
57
|
+
attachments.set(native, :entity_refs, ["amp"])
|
|
58
|
+
expect(attachments.key?(native, :entity_refs)).to be(true)
|
|
59
|
+
expect(attachments.delete(native, :entity_refs)).to eq(:stored_value)
|
|
60
|
+
|
|
61
|
+
expect(backend.calls).to eq(
|
|
62
|
+
[
|
|
63
|
+
[:get, native, :entity_refs],
|
|
64
|
+
[:set, native, :entity_refs, ["amp"]],
|
|
65
|
+
[:key?, native, :entity_refs],
|
|
66
|
+
[:delete, native, :entity_refs],
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "uses the selected runtime backend" do
|
|
72
|
+
expected_class = if RUBY_ENGINE == "opal"
|
|
73
|
+
described_class::Opal
|
|
74
|
+
else
|
|
75
|
+
described_class::Native
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
expect(attachments.backend).to be_a(expected_class)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "loader" do
|
|
83
|
+
let(:lib_dir) { File.expand_path("../../lib", __dir__) }
|
|
84
|
+
let(:ruby) { RbConfig.ruby }
|
|
85
|
+
|
|
86
|
+
it "loads NativeAttachment through the top-level moxml entrypoint" do
|
|
87
|
+
stdout, stderr, status = Open3.capture3(
|
|
88
|
+
ruby,
|
|
89
|
+
"-I",
|
|
90
|
+
lib_dir,
|
|
91
|
+
"-e",
|
|
92
|
+
'require "moxml"; puts Moxml::NativeAttachment.new.respond_to?(:set)',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(status.success?).to be(true), stderr
|
|
96
|
+
expect(stdout).to eq("true\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "loads NativeAttachment through the internal facade file" do
|
|
100
|
+
stdout, stderr, status = Open3.capture3(
|
|
101
|
+
ruby,
|
|
102
|
+
"-I",
|
|
103
|
+
lib_dir,
|
|
104
|
+
"-e",
|
|
105
|
+
'require "moxml/native_attachment"; puts Moxml::NativeAttachment.new.respond_to?(:set)',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(status.success?).to be(true), stderr
|
|
109
|
+
expect(stdout).to eq("true\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "registers backend implementations with require-style autoload paths" do
|
|
113
|
+
stdout, stderr, status = Open3.capture3(
|
|
114
|
+
ruby,
|
|
115
|
+
"-I",
|
|
116
|
+
lib_dir,
|
|
117
|
+
"-e",
|
|
118
|
+
<<~RUBY,
|
|
119
|
+
require "moxml/native_attachment"
|
|
120
|
+
|
|
121
|
+
puts Moxml::NativeAttachment.autoload?(:Opal)
|
|
122
|
+
puts Moxml::NativeAttachment.autoload?(:Native)
|
|
123
|
+
RUBY
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(status.success?).to be(true), stderr
|
|
127
|
+
expect(stdout).to eq(
|
|
128
|
+
"moxml/native_attachment/opal\n" \
|
|
129
|
+
"moxml/native_attachment/native\n",
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "loads the native backend lazily when selected" do
|
|
134
|
+
stdout, stderr, status = Open3.capture3(
|
|
135
|
+
ruby,
|
|
136
|
+
"-I",
|
|
137
|
+
lib_dir,
|
|
138
|
+
"-e",
|
|
139
|
+
<<~'RUBY',
|
|
140
|
+
require "moxml/native_attachment"
|
|
141
|
+
|
|
142
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
143
|
+
puts Moxml::NativeAttachment.new.backend.class
|
|
144
|
+
puts !$LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
145
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/opal\.rb\z}).empty?
|
|
146
|
+
RUBY
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(status.success?).to be(true), stderr
|
|
150
|
+
expect(stdout).to eq(
|
|
151
|
+
"true\n" \
|
|
152
|
+
"Moxml::NativeAttachment::Native\n" \
|
|
153
|
+
"true\n" \
|
|
154
|
+
"true\n",
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "does not load the native backend when Opal is selected" do
|
|
159
|
+
stdout, stderr, status = Open3.capture3(
|
|
160
|
+
ruby,
|
|
161
|
+
"-I",
|
|
162
|
+
lib_dir,
|
|
163
|
+
"-e",
|
|
164
|
+
<<~'RUBY',
|
|
165
|
+
Object.send(:remove_const, :RUBY_ENGINE)
|
|
166
|
+
RUBY_ENGINE = "opal"
|
|
167
|
+
|
|
168
|
+
require "moxml/native_attachment"
|
|
169
|
+
|
|
170
|
+
puts Moxml::NativeAttachment.autoload?(:Native)
|
|
171
|
+
puts Moxml::NativeAttachment.new.backend.class
|
|
172
|
+
puts $LOADED_FEATURES.grep(%r{/native_attachment/native\.rb\z}).empty?
|
|
173
|
+
RUBY
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expect(status.success?).to be(true), stderr
|
|
177
|
+
expect(stdout).to eq(
|
|
178
|
+
"moxml/native_attachment/native\n" \
|
|
179
|
+
"Moxml::NativeAttachment::Opal\n" \
|
|
180
|
+
"true\n",
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/spec/moxml/text_spec.rb
CHANGED
|
@@ -19,6 +19,29 @@ RSpec.describe Moxml::Text do
|
|
|
19
19
|
text = doc.root.children.first
|
|
20
20
|
expect(text.to_xml).to eq("plain text")
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
it "escapes XML special characters" do
|
|
24
|
+
escaped_doc = context.parse("<root>a < b & c</root>")
|
|
25
|
+
text = escaped_doc.root.children.first
|
|
26
|
+
expect(text.to_xml).to eq("a < b & c")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "#to_s" do
|
|
31
|
+
it "returns text content" do
|
|
32
|
+
text = doc.root.children.first
|
|
33
|
+
expect(text.to_s).to eq("plain text")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "is consistent across adapters" do
|
|
37
|
+
Moxml::Adapter::AVALIABLE_ADAPTERS.each do |adapter_name|
|
|
38
|
+
ctx = Moxml.new(adapter_name)
|
|
39
|
+
d = ctx.parse("<root>hello world</root>")
|
|
40
|
+
text = d.root.children.first
|
|
41
|
+
expect(text.to_s).to eq("hello world"),
|
|
42
|
+
"Text#to_s for #{adapter_name} adapter"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
22
45
|
end
|
|
23
46
|
|
|
24
47
|
describe "creation" do
|