moxml 0.5.78 → 0.5.80

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: fd28ba8e6db906efd5a53cf65b12b2eee71739b60ca5b318c4c2013235f5ec1d
4
- data.tar.gz: 2e36bde35317ddebb0a90c3b831f0566aa4e7b120ac51b9aead532309d586b28
3
+ metadata.gz: 656e065f07ecc3aea2624682345254907cab3bee557d14a74e7a9a1e6d3e53f2
4
+ data.tar.gz: d3606b240493ed82eb131bb8dd37e7d011bcd4d7d12d8907407c5408fcecb970
5
5
  SHA512:
6
- metadata.gz: a2042daf1ec389e25b0a2e8896389944e19414c36881deb40d82327dc9025a1962ae6475d73b4e9e412cd787257a7a19403826b5f76ffacbe6a95fb694702250
7
- data.tar.gz: 8259b9003c8085ce46567f4f75dc34502c41ebf15bff9d66b55499e127be8b69ccdcdabf3a3b1d262802684e26f8f2740b032a2dffed32929f3f121bc749090c
6
+ metadata.gz: d956c3ff27cae1eedce7a4cc9cb53f57a7d12a58999b74ddebb7bc4a3c99661e2cea010a5a2cc9682315ed5d4bfe06b4122453127d6682ecb96fcfd4401202e2
7
+ data.tar.gz: 478679f5fc817ba910aa0adc927ca0e22009ec1cc787b1388cef9ed3ff570da51c83eaa5e5a99f9b4b9c21e2d6d0d8dbed280925035bc00b580ba242776e7766
data/README.adoc CHANGED
@@ -103,9 +103,10 @@ entity references (`entity_mode: :keep`), 1.9.193.4+ native plan rows
103
103
  executor (`Moxml::StructPlan`; the floor is the varargs-mint fix),
104
104
  1.9.204+ native document-level parts (created DOCTYPEs as native
105
105
  nodes, document-PI removal, declaration clear). Current
106
- recommendation: 1.9.216+ (the plan-ABI train: bound plan rows,
107
- typed scalars, the #1242 iterparse fix; the engine's 1.9.217+
108
- borrowed-text-run wins ride in automatically).
106
+ recommendation: 1.9.228+ (the #318 text-set fix train; carries
107
+ the plan-ABI surface — bound plan rows, typed scalars, fused
108
+ Descriptor#materialize — plus the 1.9.217+ borrowed-text-run and
109
+ 1.9.221+ memory wins).
109
110
  |===
110
111
 
111
112
  NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.174`, `nokogiri ~> 1.18`,
@@ -644,6 +645,42 @@ root.add_child(title)
644
645
  puts doc.to_xml(indent: 2)
645
646
  ----
646
647
 
648
+ === Fast paths and diagnostics
649
+
650
+ Read-only attribute listing without Attribute node objects — the
651
+ walk-hot shape when only names and values are needed (leptris
652
+ answers in one C crossing; document order, duplicates included):
653
+
654
+ [source,ruby]
655
+ ----
656
+ element.attribute_pairs.each do |name, value|
657
+ # names are shared frozen strings; dup before mutating
658
+ end
659
+ ----
660
+
661
+ Typed struct members on `Moxml::StructPlan` — the executor casts
662
+ in C with no intermediate String (unparseable input degrades to
663
+ the raw String):
664
+
665
+ [source,ruby]
666
+ ----
667
+ plan = Moxml::StructPlan.new do
668
+ element "item", Item,
669
+ attrs: { "price" => [:price, :float], "qty" => [:qty, :integer] },
670
+ text: [:label, :integer]
671
+ end
672
+ ----
673
+
674
+ Recover-class parse diagnostics (duplicate-attribute events and
675
+ siblings) on adapters with the surface; `[]` elsewhere. Read
676
+ while the document is alive:
677
+
678
+ [source,ruby]
679
+ ----
680
+ doc.parse_diagnostics
681
+ # => [{ kind: :recover, message: "Attribute a redefined" }]
682
+ ----
683
+
647
684
  == Real-world examples
648
685
 
649
686
  Practical, runnable examples demonstrating Moxml usage in common scenarios are
@@ -358,6 +358,16 @@ namespace_validation_mode: :strict)
358
358
  nil
359
359
  end
360
360
 
361
+ # Single-crossing construction contract
362
+ # (Moxml::Element#add_element): create + attach under parent
363
+ # + all attributes in one call, returning the new native
364
+ # element. This default has no bulk face — nil sends the
365
+ # caller down the portable create_element + add_child path;
366
+ # adapters with the face override (leptris — #312/#1344).
367
+ def create_element_with_attrs(_parent, _name, _attrs)
368
+ nil
369
+ end
370
+
361
371
  # Read-only attribute listing as [name, value] pairs
362
372
  # (Moxml::Element#attribute_pairs) — document order,
363
373
  # duplicates included, no Attribute node wrappers. The
@@ -353,10 +353,8 @@ module Moxml
353
353
  }.freeze
354
354
 
355
355
  def wrap_binding_node(node, context)
356
- if node.instance_variable_defined?(:@moxml_wrapper)
357
- cached = node.instance_variable_get(:@moxml_wrapper)
358
- return cached if cached
359
- end
356
+ cached = node.instance_variable_get(:@moxml_wrapper)
357
+ return cached if cached
360
358
 
361
359
  entry = BINDING_FAST_WRAP[node.class]
362
360
  return nil unless entry
@@ -1490,6 +1488,31 @@ module Moxml
1490
1488
  line.zero? ? nil : line
1491
1489
  end
1492
1490
 
1491
+ # Single-crossing construction (#312/#1344 consumer face):
1492
+ # create + attach under parent + all attributes in ONE
1493
+ # Ruby->C call (create_child is create+attach; the attr
1494
+ # loop is C->C dlsym). Requires parent binding + the face;
1495
+ # callers fall back to create_element + set_attribute +
1496
+ # add_child when absent.
1497
+ def create_element_with_attrs(parent, name, attrs)
1498
+ binding_parent = to_binding(parent)
1499
+ doc = binding_parent.document or return nil
1500
+ return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
1501
+
1502
+ flat = attrs.flatten
1503
+ addr = ::Leptris::XML::Native.create_element_with_attrs(
1504
+ doc.c_address, binding_parent.c_address, name.to_s, flat)
1505
+ return nil if addr.nil? || addr.zero?
1506
+
1507
+ # The binding's own wrap produces the proper binding
1508
+ # Element (kind class, @c_address, @document); canonical
1509
+ # convergence (#219) hands every accessor — children
1510
+ # included — the SAME native, so the wrapper identity
1511
+ # holds across later traversals.
1512
+ canonical_native(doc,
1513
+ ::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc))
1514
+ end
1515
+
1493
1516
  def attributes(element)
1494
1517
  element = to_binding(element)
1495
1518
  element.attribute_nodes
@@ -294,6 +294,18 @@ module Moxml
294
294
  element[name.to_s] = value.to_s
295
295
  end
296
296
 
297
+ # Cross-adapter parity for Document#parse_diagnostics
298
+ # (#271): Nokogiri's doc.errors already carries recover-
299
+ # mode entries; mapped into the same { kind:, message: }
300
+ # shape. Kind is :error — Nokogiri does not classify.
301
+ def parse_diagnostics(native_doc)
302
+ return [] unless native_doc.respond_to?(:errors)
303
+
304
+ native_doc.errors.map do |e|
305
+ { kind: :error, message: e.message.to_s }
306
+ end
307
+ end
308
+
297
309
  def attributes(element)
298
310
  # attribute_nodes, not attributes.values: the attributes Hash
299
311
  # is keyed by local name, so xmi:type and type collide and
data/lib/moxml/element.rb CHANGED
@@ -126,6 +126,29 @@ module Moxml
126
126
  adapter.attribute_pairs(@native)
127
127
  end
128
128
 
129
+ # Create an element with attributes and attach it under this
130
+ # element — ONE construction on adapters with the bulk face
131
+ # (leptris, #312/#1344 consumer face): create + attach + all
132
+ # attributes in a single Ruby->C crossing. Returns the child
133
+ # wrapper. Equivalent to create_element + attribute writes +
134
+ # add_child, minus two crossings and the intermediate churn.
135
+ def add_element(name, attrs = {})
136
+ native_child = adapter.create_element_with_attrs(
137
+ @native, name, attrs
138
+ )
139
+ if native_child
140
+ adapter.node_type(native_child)
141
+ Moxml::Node.wrap_with(native_child, context, adapter)
142
+ else
143
+ fallback = Moxml::Node.wrap_with(
144
+ adapter.create_element(name), context, adapter
145
+ )
146
+ attrs.each { |k, v| fallback[k] = v.to_s }
147
+ add_child(fallback)
148
+ fallback
149
+ end
150
+ end
151
+
129
152
  def attributes
130
153
  # Primed like Node.wrap: the adapter and type are known at
131
154
  # mint, so the first name/value/attribute? access skips the
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.78"
4
+ VERSION = "0.5.80"
5
5
  end
@@ -40,4 +40,16 @@ RSpec.describe Moxml::Document do
40
40
  rescue StandardError, LoadError
41
41
  skip "rexml adapter unavailable"
42
42
  end
43
+
44
+ it "maps nokogiri doc.errors into the same shape" do
45
+ ctx = Moxml.new(:nokogiri)
46
+ doc = ctx.parse(%(<r a="1" a="2"/>), recover: true) ||
47
+ ctx.parse(%(<r a="1" a="2"/>))
48
+ diags = doc.parse_diagnostics
49
+ expect(diags).not_to be_empty
50
+ expect(diags.first[:kind]).to eq(:error)
51
+ expect(diags.first[:message]).to be_a(String)
52
+ rescue StandardError, LoadError
53
+ skip "nokogiri adapter unavailable"
54
+ end
43
55
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Element#add_element adapter-contract conformance (#312): the
6
+ # child must land attached with ALL attributes in one call —
7
+ # through the bulk face (leptris) or the portable create_element +
8
+ # add_child fallback (every other adapter). Guards both halves of
9
+ # the contract: a missing base default NoMethodErrors on non-face
10
+ # adapters, a broken fallback loses attributes or the attach.
11
+ RSpec.describe Moxml::Element do
12
+ ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
13
+
14
+ def with_adapter(name)
15
+ ctx = Moxml.new(name)
16
+ yield ctx
17
+ rescue StandardError, LoadError
18
+ skip "adapter #{name} unavailable"
19
+ end
20
+
21
+ it "attaches a child with all attributes on every available adapter" do
22
+ ADAPTERS.each do |name|
23
+ with_adapter(name) do |ctx|
24
+ doc = ctx.parse("<catalog/>")
25
+ root = doc.root
26
+ child = root.add_element("record", "id" => "r1", "kind" => "k1")
27
+
28
+ expect(child.name).to eq("record"), "#{name}: name"
29
+ expect(child["id"]).to eq("r1"), "#{name}: id attr"
30
+ expect(child["kind"]).to eq("k1"), "#{name}: kind attr"
31
+ expect(child.parent.name).to eq("catalog"), "#{name}: attach"
32
+ expect(root.children.map(&:name)).to include("record"), "#{name}: visible in children"
33
+ end
34
+ end
35
+ end
36
+
37
+ it "nests under a non-root element with empty attrs" do
38
+ ADAPTERS.each do |name|
39
+ with_adapter(name) do |ctx|
40
+ doc = ctx.parse("<catalog><record/></catalog>")
41
+ record = doc.root.children.first
42
+ field = record.add_element("field")
43
+
44
+ expect(field.name).to eq("field"), "#{name}: name"
45
+ expect(field.attributes).to be_empty, "#{name}: no attrs"
46
+ expect(field.parent.name).to eq("record"), "#{name}: attach"
47
+ end
48
+ end
49
+ end
50
+ end
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.5.78
4
+ version: 0.5.80
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-09-23 00:00:00.000000000 Z
11
+ date: 2026-09-24 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
@@ -468,6 +468,7 @@ files:
468
468
  - spec/moxml/document_free_spec.rb
469
469
  - spec/moxml/document_parse_diagnostics_spec.rb
470
470
  - spec/moxml/document_spec.rb
471
+ - spec/moxml/element_add_element_spec.rb
471
472
  - spec/moxml/element_attribute_pairs_spec.rb
472
473
  - spec/moxml/element_spec.rb
473
474
  - spec/moxml/entity_preservation_spec.rb