moxml 0.5.62 → 0.5.63

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 430e3a7e8c18cfa2c083ae9b263e9fedbaed03c109811980f0ca51de36507be7
4
- data.tar.gz: 9644f7689ef2126884a6b11ecfcf9c532e0b2cae9cb44005b5b6b3434d96c7b4
3
+ metadata.gz: 60493a9fbb6b03a4db31f83e99f269c5d5cf1797f5e0e94f4b6139616905e45e
4
+ data.tar.gz: 4df319d1a67b69f2a08e99cf338131c55584b1e68cb4b46687a23c2f6329263c
5
5
  SHA512:
6
- metadata.gz: 9f89223d0bc12427916bb46220f79d957df704b878ea02dc11387c9096c6829f5309ed857a3faa4c1b9aee0697b3dfe2f7fa9f73fc5e083b6c1c0963bea11501
7
- data.tar.gz: 5120f5700dd76a688576501a90ee2e6ffc443558cb89478dd23f2ea5a424d29dac41dd4df4cb9acc764451b81f84c5a03eed3d4ab734adfa276c7a1043bf54ce
6
+ metadata.gz: 6690afe362a4af5781c54271c29daf1af651a6bc34975fd078a9c1d56ec56ac6c5af13a63746a801c206162aa25c7d09314f5bca55db48d356d83586466eda7f
7
+ data.tar.gz: a53a4a73958a40338ed54fbeb6c9098264e5f480b8125888b7471c9d6d11ec9dde455fc35e7de8847c3246fa834e607ab16f8eed5f2f47089547a45ff5b17f8b
@@ -23,6 +23,11 @@ XML = parts.join
23
23
 
24
24
  CTX = Moxml.new(:leptris)
25
25
 
26
+ STRUCT_PLAN = Moxml::StructPlan.new do
27
+ element "record", Record, attrs: { "id" => :id, "kind" => :kind }, children: :fields
28
+ element "field", Field, attrs: { "name" => :name, "unit" => :unit }, text: :value
29
+ end
30
+
26
31
  PLAN = Moxml::Plan.new do
27
32
  on("record") { |attrs, _t, fields| Record.new(attrs["id"], attrs["kind"], fields) }
28
33
  on("field") { |attrs, text, _k| Field.new(attrs["name"], attrs["unit"], text) }
@@ -35,6 +40,13 @@ def plan_run
35
40
  records
36
41
  end
37
42
 
43
+ def struct_run
44
+ records = STRUCT_PLAN.parse(XML, CTX)
45
+ raise "shape" unless records.size == 150 && records[7].fields[3].value == "value 7.3"
46
+
47
+ records
48
+ end
49
+
38
50
  def wrapper_run
39
51
  doc = CTX.parse(XML)
40
52
  catalog = Catalog.new([])
@@ -99,13 +111,23 @@ end
99
111
 
100
112
  # warm
101
113
  plan_run
114
+ struct_run
102
115
  wrapper_run
103
116
  nokogiri_run
104
117
 
118
+ struct_us = nil
119
+ struct_allocs = nil
120
+ if Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.197.1")
121
+ struct_us, struct_allocs = bench { struct_run }
122
+ end
105
123
  plan_us, plan_allocs = bench { plan_run }
106
124
  wrap_us, wrap_allocs = bench { wrapper_run }
107
125
  nk_us, nk_allocs = bench { nokogiri_run }
108
126
 
127
+ if struct_us
128
+ puts format("struct (parse+materialize) %<t>8.0f us %<a>7d allocs",
129
+ t: struct_us, a: struct_allocs)
130
+ end
109
131
  puts format("plan (parse+materialize) %<t>8.0f us %<a>7d allocs",
110
132
  t: plan_us, a: plan_allocs)
111
133
  puts format("wrapper (parse+walk) %<t>8.0f us %<a>7d allocs",
@@ -371,6 +371,16 @@ namespace_validation_mode: :strict)
371
371
  nil
372
372
  end
373
373
 
374
+ # Struct-plan executor (Moxml::StructPlan): +spec+ compiles
375
+ # the consumer's shape as {name => [Struct class, attrs
376
+ # Hash (name => slot Symbol), text slot, children slot]}.
377
+ # Returns the array of top-level minted structs, or nil when
378
+ # the adapter has no C executor (the StructPlan then runs
379
+ # its Moxml::Plan fallback over plan_rows).
380
+ def plan_structs(_native, _spec)
381
+ nil
382
+ end
383
+
374
384
  # Deterministic native-memory release for adapters backed by
375
385
  # C trees (issue #134). GC-managed engines no-op; released
376
386
  # documents raise the engine's use-after-free error on
@@ -537,6 +537,32 @@ module Moxml
537
537
  NATIVE_READ_LAYER &&
538
538
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.4")
539
539
 
540
+ # leptris-ruby#266 (binding 1.9.197.1): Native.plan_structs —
541
+ # the C struct executor behind Moxml::StructPlan.
542
+ NATIVE_PLAN_STRUCTS =
543
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.197.1")
544
+
545
+ def self.plan_structs(native, spec)
546
+ return nil unless NATIVE_PLAN_STRUCTS
547
+
548
+ doc = if native.is_a?(::Leptris::XML::Document)
549
+ native
550
+ else
551
+ doc_for(native)
552
+ end
553
+ return nil if doc.nil? || attachments.get(doc, :entity_markers)
554
+
555
+ root_binding = if native.is_a?(::Leptris::XML::Document)
556
+ doc.root
557
+ else
558
+ to_binding(native)
559
+ end
560
+ return nil unless root_binding
561
+
562
+ ::Leptris::XML::Native.plan_structs(doc, root_binding.c_address,
563
+ spec)
564
+ end
565
+
540
566
  def self.plan_rows(native)
541
567
  return nil unless NATIVE_READ_LAYER
542
568
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Struct-compiled materialization — the fully compiled grammar:
5
+ # declare the document shape once as STRUCTS; adapters with a C
6
+ # executor (leptris 1.9.197.1+) mint the typed objects directly
7
+ # with zero Ruby frames per element; everything else falls back to
8
+ # an equivalent Moxml::Plan over the row stream (spec-pinned
9
+ # equal).
10
+ #
11
+ # plan = Moxml::StructPlan.new do
12
+ # element "record", Record, attrs: { "id" => :id, "kind" => :kind },
13
+ # children: :fields
14
+ # element "field", Field, attrs: { "name" => :name, "unit" => :unit },
15
+ # text: :value
16
+ # end
17
+ # records = plan.parse(xml, ctx) # Array of Record
18
+ #
19
+ # Slot symbols must be the Struct's member names. Semantics equal
20
+ # Moxml::Plan: unmatched elements are barriers — their matched
21
+ # descendants surface at top level.
22
+ class StructPlan
23
+ Element = Struct.new(:klass, :attrs, :text, :children)
24
+
25
+ def initialize(&block)
26
+ @elements = {}
27
+ instance_eval(&block) if block
28
+ end
29
+
30
+ # Registers a struct element. +attrs+ maps attribute names to
31
+ # member slots; +text+ and +children+ name the member slots
32
+ # receiving the first text child and the matched children array.
33
+ # Returns self so registrations chain.
34
+ def element(name, klass, attrs: {}, text: nil, children: nil)
35
+ @elements[name] = Element.new(klass, attrs, text, children)
36
+ self
37
+ end
38
+
39
+ # The compiled spec the C executor consumes:
40
+ # {name => [klass, attrs, text slot, children slot]}.
41
+ def compile
42
+ @elements.each_with_object({}) do |(name, el), spec|
43
+ spec[name] = [el.klass, el.attrs, el.text, el.children]
44
+ end
45
+ end
46
+
47
+ def parse(xml, context)
48
+ materialize(context.parse(xml))
49
+ end
50
+
51
+ def materialize(node)
52
+ adapter = node.context.config.adapter
53
+ roots = adapter.plan_structs(node.native, compile)
54
+ return roots if roots
55
+
56
+ plan.materialize(node)
57
+ end
58
+
59
+ private
60
+
61
+ # The fallback: the same element table as block handlers over
62
+ # the plan row stream. instance_eval'd DSL — capture the table
63
+ # in a local (the Plan's self has no @elements).
64
+ def plan
65
+ @plan ||= begin
66
+ elements = @elements
67
+ Moxml::Plan.new do
68
+ elements.each do |name, el|
69
+ on(name) do |attrs, text, children|
70
+ obj = el.klass.new
71
+ el.attrs.each { |aname, slot| obj[slot] = attrs[aname] }
72
+ obj[el.text] = text if el.text
73
+ obj[el.children] = children if el.children
74
+ obj
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.62"
4
+ VERSION = "0.5.63"
5
5
  end
data/lib/moxml.rb CHANGED
@@ -94,6 +94,7 @@ module Moxml
94
94
  autoload :XmlEmitter, "moxml/xml_emitter"
95
95
  autoload :Materializer, "moxml/materializer"
96
96
  autoload :Plan, "moxml/plan"
97
+ autoload :StructPlan, "moxml/struct_plan"
97
98
  autoload :Adapter, "moxml/adapter"
98
99
  autoload :XPath, "moxml/xpath"
99
100
  autoload :SAX, "moxml/sax"
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Moxml::StructPlan: the fully compiled grammar — struct slots
6
+ # declared once, minted in C on leptris (1.9.197.1+) and via the
7
+ # plan fallback everywhere else. Results must be identical to
8
+ # Moxml::Plan.
9
+ SRecord = Struct.new(:id, :kind, :fields)
10
+ SField = Struct.new(:name, :unit, :value)
11
+
12
+ RSpec.describe Moxml::StructPlan do
13
+ let(:xml) do
14
+ '<catalog><record id="r0" kind="k0"><field name="f0" unit="u0">v 0.0</field><field name="f1" unit="u1">v 0.1</field></record><record id="r1" kind="k1"><field name="f0" unit="u0">v 1.0</field></record></catalog>'
15
+ end
16
+ let(:plan) do
17
+ described_class.new do
18
+ element "record", SRecord,
19
+ attrs: { "id" => :id, "kind" => :kind }, children: :fields
20
+ element "field", SField,
21
+ attrs: { "name" => :name, "unit" => :unit }, text: :value
22
+ end
23
+ end
24
+ let(:block_plan) do
25
+ Moxml::Plan.new do
26
+ on("record") { |attrs, _t, fields| SRecord.new(attrs["id"], attrs["kind"], fields) }
27
+ on("field") { |attrs, text, _k| SField.new(attrs["name"], attrs["unit"], text) }
28
+ end
29
+ end
30
+
31
+ ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
32
+
33
+ def with_adapter(name)
34
+ yield Moxml.new(name)
35
+ rescue StandardError
36
+ skip "#{name} unavailable"
37
+ end
38
+
39
+ ADAPTERS.each do |adapter_name|
40
+ describe "on #{adapter_name}" do
41
+ it "materializes the same structs as Moxml::Plan" do
42
+ with_adapter(adapter_name) do |ctx|
43
+ expect(plan.parse(xml, ctx))
44
+ .to eq(block_plan.parse(xml, ctx))
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ it "treats unmatched elements as barriers like Moxml::Plan" do
51
+ barrier_xml = "<catalog><wrap><record id=\"r9\"/></wrap><record id=\"r8\"/></catalog>"
52
+ with_adapter(:leptris) do |ctx|
53
+ rows = plan.parse(barrier_xml, ctx)
54
+ expect(rows.map(&:id)).to eq(%w[r9 r8])
55
+ end
56
+ end
57
+
58
+ it "runs the C executor on leptris (not the fallback)" do
59
+ with_adapter(:leptris) do |ctx|
60
+ doc = ctx.parse(xml)
61
+ roots = Moxml::Adapter::Leptris.plan_structs(
62
+ doc.native,
63
+ "record" => [SRecord, { "id" => :id, "kind" => :kind }, nil, :fields],
64
+ "field" => [SField, { "name" => :name, "unit" => :unit }, :value, nil],
65
+ )
66
+ expect(roots).to eq(plan.parse(xml, ctx))
67
+ end
68
+ end
69
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.62
4
+ version: 0.5.63
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -271,6 +271,7 @@ files:
271
271
  - lib/moxml/signature/transform_pipeline.rb
272
272
  - lib/moxml/signature/verification_result.rb
273
273
  - lib/moxml/signature/verifier.rb
274
+ - lib/moxml/struct_plan.rb
274
275
  - lib/moxml/text.rb
275
276
  - lib/moxml/version.rb
276
277
  - lib/moxml/wrappers.rb
@@ -510,6 +511,7 @@ files:
510
511
  - spec/moxml/signature/key_extractor_spec.rb
511
512
  - spec/moxml/signature/model_spec.rb
512
513
  - spec/moxml/signature/round_trip_spec.rb
514
+ - spec/moxml/struct_plan_spec.rb
513
515
  - spec/moxml/text_spec.rb
514
516
  - spec/moxml/version_spec.rb
515
517
  - spec/moxml/xml_emitter_spec.rb