moxml 0.1.18 → 0.1.19
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/README.adoc +24 -1
- data/lib/moxml/config.rb +1 -1
- 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/version.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/xpath/functions/node_functions_spec.rb +3 -2
- data/spec/performance/benchmark_spec.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76508cc0d2699469ade87f48e38ea73289abc4f95cb7a313f6d71548477248f3
|
|
4
|
+
data.tar.gz: 1752cb433953a869cedd1d88fc565c53f77bd08201c5977247150e1e7b75a386
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4219577f2e00a9e4f00f1ce1bbaf4f03d6c841dfe8adb9983162a6389b1e276725984dc010f451cac55fbd8049a81451d641fc073fc1cf8417f90ee6e50cdf5
|
|
7
|
+
data.tar.gz: 44f3e08b174e2689fed3a60ee3ef0b805fdc14cbeeb905ebde15e5241be3e3f168441dc95afe99dd244fc1047c581fce912ec3e061299e4e101f63329f00def9
|
data/README.adoc
CHANGED
|
@@ -33,7 +33,7 @@ Moxml supports the following XML libraries:
|
|
|
33
33
|
REXML:: https://github.com/ruby/rexml[REXML], a pure Ruby XML parser
|
|
34
34
|
distributed with standard Ruby. Not the fastest, but always available.
|
|
35
35
|
|
|
36
|
-
Nokogiri::
|
|
36
|
+
Nokogiri:: https://github.com/sparklemotion/nokogiri[Nokogiri], a
|
|
37
37
|
widely used implementation which wraps around the performant
|
|
38
38
|
https://github.com/GNOME/libxml2[libxml2] C library.
|
|
39
39
|
|
|
@@ -47,6 +47,29 @@ LibXML:: https://github.com/xml4r/libxml-ruby[libxml-ruby], Ruby bindings
|
|
|
47
47
|
for the performant https://github.com/GNOME/libxml2[libxml2] C library.
|
|
48
48
|
Alternative to Nokogiri with similar performance characteristics.
|
|
49
49
|
|
|
50
|
+
==== Default adapter selection
|
|
51
|
+
|
|
52
|
+
When no adapter is explicitly specified, Moxml selects one automatically based on
|
|
53
|
+
the runtime environment:
|
|
54
|
+
|
|
55
|
+
. If running on Opal (`RUBY_ENGINE == "opal"`), Oga is used (pure Ruby, no C extensions)
|
|
56
|
+
. Otherwise, Moxml detects already-loaded XML libraries in this order:
|
|
57
|
+
.. Nokogiri
|
|
58
|
+
.. Ox
|
|
59
|
+
.. Oga
|
|
60
|
+
. If none of the above are loaded, Nokogiri is the fallback default
|
|
61
|
+
|
|
62
|
+
[source,ruby]
|
|
63
|
+
----
|
|
64
|
+
# Automatic detection — picks the best available adapter
|
|
65
|
+
ctx = Moxml.new
|
|
66
|
+
ctx.config.adapter_name # => :nokogiri (or :oga on Opal)
|
|
67
|
+
|
|
68
|
+
# Explicit override — always takes precedence
|
|
69
|
+
ctx = Moxml.new(:ox)
|
|
70
|
+
ctx.config.adapter_name # => :ox
|
|
71
|
+
----
|
|
72
|
+
|
|
50
73
|
=== Feature table
|
|
51
74
|
|
|
52
75
|
Moxml exercises its best effort to provide a consistent interface across basic
|
data/lib/moxml/config.rb
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
class NativeAttachment
|
|
7
|
+
# Stores Moxml-specific state associated with native adapter objects
|
|
8
|
+
# without polluting their internals.
|
|
9
|
+
#
|
|
10
|
+
# Uses object_id as key with GC finalizer cleanup to prevent memory leaks.
|
|
11
|
+
# Thread-safe via Monitor (reentrant-safe).
|
|
12
|
+
#
|
|
13
|
+
# Replaces the anti-pattern of using instance_variable_set/get on
|
|
14
|
+
# foreign library objects (Nokogiri, REXML, Oga, Ox, LibXML nodes).
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# attachments = NativeAttachment.new
|
|
18
|
+
# attachments.set(native_element, :entity_refs, [])
|
|
19
|
+
# refs = attachments.get(native_element, :entity_refs)
|
|
20
|
+
# attachments.key?(native_element, :doctype) #=> false
|
|
21
|
+
class Native
|
|
22
|
+
def initialize
|
|
23
|
+
@data = {}
|
|
24
|
+
@finalizer_registered = {}
|
|
25
|
+
@monitor = Monitor.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get(native, key)
|
|
29
|
+
@monitor.synchronize { @data[native.object_id]&.[](key) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set(native, key, value)
|
|
33
|
+
id = native.object_id
|
|
34
|
+
@monitor.synchronize do
|
|
35
|
+
@data[id] ||= {}
|
|
36
|
+
@data[id][key] = value
|
|
37
|
+
register_finalizer(native, id) unless @finalizer_registered[id]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def key?(native, key)
|
|
42
|
+
@monitor.synchronize { @data[native.object_id]&.key?(key) || false }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete(native, key)
|
|
46
|
+
@monitor.synchronize { @data[native.object_id]&.delete(key) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def register_finalizer(native, id)
|
|
52
|
+
@finalizer_registered[id] = true
|
|
53
|
+
ObjectSpace.define_finalizer(native, finalizer_for(id))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def finalizer_for(id)
|
|
57
|
+
data = @data
|
|
58
|
+
registered = @finalizer_registered
|
|
59
|
+
# Finalizers must NOT use Mutex/Monitor (can't be called from trap context).
|
|
60
|
+
# Direct Hash operations are safe here since finalizers run sequentially
|
|
61
|
+
# and the GC'd object's id won't be accessed by any other thread.
|
|
62
|
+
proc do
|
|
63
|
+
data.delete(id)
|
|
64
|
+
registered.delete(id)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -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/version.rb
CHANGED
|
@@ -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
|
|
@@ -127,7 +127,7 @@ RSpec.describe "XPath Node Functions" do
|
|
|
127
127
|
it "inherits language from parent element" do
|
|
128
128
|
ast = Moxml::XPath::Parser.parse('lang("en")')
|
|
129
129
|
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
130
|
-
child = doc_with_lang.root.children.first
|
|
130
|
+
child = doc_with_lang.root.children.select { |c| c.is_a?(Moxml::Element) }.first
|
|
131
131
|
result = proc.call(child)
|
|
132
132
|
|
|
133
133
|
expect(result).to be true
|
|
@@ -136,7 +136,8 @@ RSpec.describe "XPath Node Functions" do
|
|
|
136
136
|
it "uses closest xml:lang attribute" do
|
|
137
137
|
ast = Moxml::XPath::Parser.parse('lang("fr")')
|
|
138
138
|
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
139
|
-
|
|
139
|
+
elements = doc_with_lang.root.children.select { |c| c.is_a?(Moxml::Element) }
|
|
140
|
+
other = elements[1]
|
|
140
141
|
result = proc.call(other)
|
|
141
142
|
|
|
142
143
|
expect(result).to be true
|
|
@@ -30,7 +30,7 @@ RSpec.shared_examples "Performance Examples" do
|
|
|
30
30
|
rexml: { parser: 0, serializer: 5 },
|
|
31
31
|
ox: { parser: 2, serializer: 1000 },
|
|
32
32
|
headed_ox: { parser: 2, serializer: 1000 },
|
|
33
|
-
libxml: { parser:
|
|
33
|
+
libxml: { parser: 2, serializer: 3 },
|
|
34
34
|
}
|
|
35
35
|
end
|
|
36
36
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -141,6 +141,8 @@ files:
|
|
|
141
141
|
- lib/moxml/error.rb
|
|
142
142
|
- lib/moxml/namespace.rb
|
|
143
143
|
- lib/moxml/native_attachment.rb
|
|
144
|
+
- lib/moxml/native_attachment/native.rb
|
|
145
|
+
- lib/moxml/native_attachment/opal.rb
|
|
144
146
|
- lib/moxml/node.rb
|
|
145
147
|
- lib/moxml/node_set.rb
|
|
146
148
|
- lib/moxml/processing_instruction.rb
|
|
@@ -325,6 +327,10 @@ files:
|
|
|
325
327
|
- spec/moxml/moxml_spec.rb
|
|
326
328
|
- spec/moxml/namespace_spec.rb
|
|
327
329
|
- spec/moxml/namespace_uri_validation_spec.rb
|
|
330
|
+
- spec/moxml/native_attachment/native_spec.rb
|
|
331
|
+
- spec/moxml/native_attachment/opal_spec.rb
|
|
332
|
+
- spec/moxml/native_attachment/shared_examples.rb
|
|
333
|
+
- spec/moxml/native_attachment_spec.rb
|
|
328
334
|
- spec/moxml/node_cache_spec.rb
|
|
329
335
|
- spec/moxml/node_set_cache_spec.rb
|
|
330
336
|
- spec/moxml/node_set_spec.rb
|