philiprehberger-xml_builder 0.1.0 → 0.2.0

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: 922733a983681c827ed0fc8e94454e002abf37679785f1daba0153b9fff1dc01
4
- data.tar.gz: 6826f30ce35d4c783862d55aee71819b643a0c41e1636d30d774afaba8928e37
3
+ metadata.gz: 02c1eeed30ddee688b0bee7143b27ad18d90c649a1b1d1d98d5d7f72f437e4e8
4
+ data.tar.gz: 3b01559a4ebfdf722c7992a1f6f844ad3ae40ff809e287ca2925f7297274ddcb
5
5
  SHA512:
6
- metadata.gz: 2a1c52baeb6dd26956f6d6b0662742cc61872bc1a27cd45e3512bc99bd0db1486e30953cecf7661540dd66097b68e9cd1083b75d33d5aa90f0bbd34ea658ba19
7
- data.tar.gz: 4eb32107f4e5007bb7f6afea092cfbd25f927fc73bd4251d81384ae97d46d7931e858295ca0f7970d8c28111d84f55bc697448069ec03dba7ffe32ef1711e627
6
+ metadata.gz: e9b4ebc5605369a8f6e0ee94238ea1797d56728ee32f63d3254808240f789c8637cce12b9f386112ebd2115aa4e558b2b3601eb130a436d84e04e28bfd9e11bc
7
+ data.tar.gz: 4ae1e0ecdc135accc90eec5704ec175e5344cf98b65ee851d98937f478f9cbfcb000b62d167b03dfc1b2ac77ccd4898f9c669885122d7873dc5cca51a827e8f3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-03-28
11
+
12
+ ### Added
13
+
14
+ - XML namespace support with `namespace` and `namespace_tag` methods for prefix declarations and namespace-aware elements
15
+ - SOAP envelope builder with `soap_envelope` DSL and `build_soap` convenience method supporting SOAP 1.1 and 1.2
16
+ - XML fragment composition with `append` (merge Document objects) and `insert_fragment` (insert raw XML strings)
17
+ - Support for double-underscore to colon conversion in symbol attribute keys (e.g. `xmlns__soap:` becomes `xmlns:soap=`)
18
+
19
+ ## [0.1.1] - 2026-03-26
20
+
21
+ ### Added
22
+
23
+ - Add GitHub funding configuration
24
+
10
25
  ## [0.1.0] - 2026-03-26
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # philiprehberger-xml_builder
2
2
 
3
- [![Tests](https://github.com/philiprehberger/rb-xml-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-xml-builder/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/philiprehberger-xml_builder.svg)](https://rubygems.org/gems/philiprehberger-xml_builder)
5
- [![License](https://img.shields.io/github/license/philiprehberger/rb-xml-builder)](LICENSE)
6
- [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](https://github.com/sponsors/philiprehberger)
3
+ [![Tests](https://github.com/philiprehberger/rb-xml-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-xml-builder/actions/workflows/ci.yml) [![Gem Version](https://img.shields.io/gem/v/philiprehberger-xml_builder)](https://rubygems.org/gems/philiprehberger-xml_builder) [![GitHub release](https://img.shields.io/github/v/release/philiprehberger/rb-xml-builder)](https://github.com/philiprehberger/rb-xml-builder/releases) [![GitHub last commit](https://img.shields.io/github/last-commit/philiprehberger/rb-xml-builder)](https://github.com/philiprehberger/rb-xml-builder/commits/main) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Bug Reports](https://img.shields.io/badge/bug-reports-red.svg)](https://github.com/philiprehberger/rb-xml-builder/issues) [![Feature Requests](https://img.shields.io/badge/feature-requests-blue.svg)](https://github.com/philiprehberger/rb-xml-builder/issues) [![GitHub Sponsors](https://img.shields.io/badge/sponsor-philiprehberger-ea4aaa.svg?logo=github)](https://github.com/sponsors/philiprehberger)
7
4
 
8
5
  Lightweight XML builder DSL without Nokogiri dependency
9
6
 
@@ -27,6 +24,8 @@ gem install philiprehberger-xml_builder
27
24
 
28
25
  ## Usage
29
26
 
27
+ ### Basic Elements
28
+
30
29
  ```ruby
31
30
  require "philiprehberger/xml_builder"
32
31
 
@@ -97,6 +96,86 @@ xml = Philiprehberger::XmlBuilder.build do |doc|
97
96
  end
98
97
  ```
99
98
 
99
+ ### XML Namespaces
100
+
101
+ Register namespace prefixes and create namespace-aware elements:
102
+
103
+ ```ruby
104
+ xml = Philiprehberger::XmlBuilder.build do |doc|
105
+ doc.namespace(:soap, "http://schemas.xmlsoap.org/soap/envelope/")
106
+ doc.namespace(:wsse, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd")
107
+
108
+ doc.namespace_tag(:soap, :Envelope) do
109
+ doc.namespace_tag(:soap, :Header) do
110
+ doc.namespace_tag(:wsse, :Security)
111
+ end
112
+ doc.namespace_tag(:soap, :Body)
113
+ end
114
+ end
115
+ ```
116
+
117
+ You can also use string tag names directly:
118
+
119
+ ```ruby
120
+ xml = Philiprehberger::XmlBuilder.build do |doc|
121
+ doc.tag("soap:Envelope", "xmlns:soap" => "http://schemas.xmlsoap.org/soap/envelope/") do
122
+ doc.tag("soap:Body")
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### SOAP Envelope Builder
128
+
129
+ Build SOAP 1.1 or 1.2 envelopes with a convenience DSL:
130
+
131
+ ```ruby
132
+ xml = Philiprehberger::XmlBuilder.build do |doc|
133
+ doc.soap_envelope(version: "1.1") do |header, body|
134
+ header << ->(d) { d.tag("auth") { d.text("token123") } }
135
+ body << ->(d) { d.tag("GetPrice") { d.text("Widget") } }
136
+ end
137
+ end
138
+ ```
139
+
140
+ Or use the top-level shortcut:
141
+
142
+ ```ruby
143
+ xml = Philiprehberger::XmlBuilder.build_soap(soap_version: "1.2") do |header, body|
144
+ body << ->(d) { d.tag("GetStockPrice") { d.tag("Symbol") { d.text("AAPL") } } }
145
+ end
146
+ ```
147
+
148
+ ### XML Fragment Composition
149
+
150
+ Combine separately built document fragments:
151
+
152
+ ```ruby
153
+ # Build fragments independently
154
+ header = Philiprehberger::XmlBuilder::Document.new
155
+ header.tag(:title) { header.text("My Document") }
156
+
157
+ body = Philiprehberger::XmlBuilder::Document.new
158
+ body.tag(:paragraph) { body.text("Hello world") }
159
+
160
+ # Compose into a single document
161
+ xml = Philiprehberger::XmlBuilder.build do |doc|
162
+ doc.tag(:document) do
163
+ doc.tag(:header) { doc.append(header) }
164
+ doc.tag(:body) { doc.append(body) }
165
+ end
166
+ end
167
+ ```
168
+
169
+ Insert raw XML fragment strings:
170
+
171
+ ```ruby
172
+ xml = Philiprehberger::XmlBuilder.build do |doc|
173
+ doc.tag(:root) do
174
+ doc.insert_fragment('<existing>data</existing>')
175
+ end
176
+ end
177
+ ```
178
+
100
179
  ## API
101
180
 
102
181
  ### `Philiprehberger::XmlBuilder`
@@ -104,6 +183,7 @@ end
104
183
  | Method | Description |
105
184
  |--------|-------------|
106
185
  | `.build(encoding: "UTF-8", version: "1.0") { \|doc\| ... }` | Build an XML document and return the string |
186
+ | `.build_soap(soap_version: "1.1", encoding: "UTF-8", version: "1.0") { \|header, body\| ... }` | Build a SOAP envelope document |
107
187
 
108
188
  ### `Document`
109
189
 
@@ -115,6 +195,11 @@ end
115
195
  | `#comment(text)` | Add an XML comment |
116
196
  | `#processing_instruction(target, content)` | Add a processing instruction |
117
197
  | `#raw(string)` | Add raw unescaped XML |
198
+ | `#namespace(prefix, uri)` | Register an XML namespace prefix and URI |
199
+ | `#namespace_tag(prefix, name, attributes = {}) { ... }` | Add a namespace-prefixed element with auto xmlns |
200
+ | `#soap_envelope(version: "1.1") { \|header, body\| ... }` | Build a SOAP envelope with Header and Body |
201
+ | `#append(other_document)` | Append children from another Document |
202
+ | `#insert_fragment(xml_string)` | Insert a raw XML fragment string |
118
203
  | `#to_s` | Render compact XML string |
119
204
  | `#to_xml(indent: nil)` | Render XML with optional indentation |
120
205
 
@@ -132,6 +217,10 @@ bundle exec rspec
132
217
  bundle exec rubocop
133
218
  ```
134
219
 
220
+ ## Support
221
+
222
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-Philip%20Rehberger-blue?logo=linkedin)](https://linkedin.com/in/philiprehberger) [![More Packages](https://img.shields.io/badge/more-packages-blue.svg)](https://github.com/philiprehberger?tab=repositories)
223
+
135
224
  ## License
136
225
 
137
226
  [MIT](LICENSE)
@@ -6,7 +6,7 @@ module Philiprehberger
6
6
  #
7
7
  # Used as the context object inside XmlBuilder.build blocks.
8
8
  class Document
9
- attr_reader :version, :encoding
9
+ attr_reader :version, :encoding, :children
10
10
 
11
11
  # @param version [String] XML version for the declaration
12
12
  # @param encoding [String] XML encoding for the declaration
@@ -15,6 +15,7 @@ module Philiprehberger
15
15
  @encoding = encoding
16
16
  @children = []
17
17
  @node_stack = []
18
+ @namespaces = {}
18
19
  end
19
20
 
20
21
  # Add an XML element with optional attributes and nested children.
@@ -99,6 +100,96 @@ module Philiprehberger
99
100
  parts.join
100
101
  end
101
102
 
103
+ # Register an XML namespace prefix and URI.
104
+ #
105
+ # Registered namespaces are automatically added as xmlns attributes
106
+ # when using namespace_tag.
107
+ #
108
+ # @param prefix [String, Symbol] the namespace prefix
109
+ # @param uri [String] the namespace URI
110
+ # @return [void]
111
+ def namespace(prefix, uri)
112
+ @namespaces[prefix.to_s] = uri
113
+ end
114
+
115
+ # Add a namespace-prefixed element.
116
+ #
117
+ # Automatically includes the xmlns declaration for the prefix if it was
118
+ # registered via #namespace and this is the first use in the current scope.
119
+ #
120
+ # @param prefix [String, Symbol] the namespace prefix
121
+ # @param name [String, Symbol] the local element name
122
+ # @param attributes [Hash] additional element attributes
123
+ # @yield optional block for adding child elements
124
+ # @return [Node] the created node
125
+ def namespace_tag(prefix, name, attributes = {}, &)
126
+ prefixed_name = "#{prefix}:#{name}"
127
+ uri = @namespaces[prefix.to_s]
128
+ attrs = if uri
129
+ { "xmlns:#{prefix}" => uri }.merge(attributes)
130
+ else
131
+ attributes
132
+ end
133
+ tag(prefixed_name, attrs, &)
134
+ end
135
+
136
+ # Build a SOAP envelope using a block-based DSL.
137
+ #
138
+ # Supports SOAP 1.1 (default) and 1.2. Automatically sets the correct
139
+ # namespace URI and creates the Envelope, Header, and Body elements.
140
+ #
141
+ # @param version [String] SOAP version: "1.1" or "1.2"
142
+ # @yield [header, body] yields two procs for adding header and body content
143
+ # @return [void]
144
+ def soap_envelope(version: '1.1')
145
+ uri = case version
146
+ when '1.1' then 'http://schemas.xmlsoap.org/soap/envelope/'
147
+ when '1.2' then 'http://www.w3.org/2003/05/soap-envelope'
148
+ else
149
+ raise Error, "Unsupported SOAP version: #{version}. Use '1.1' or '1.2'."
150
+ end
151
+
152
+ header_children = []
153
+ body_children = []
154
+
155
+ yield(header_children, body_children) if block_given?
156
+
157
+ tag('soap:Envelope', 'xmlns:soap' => uri) do
158
+ tag('soap:Header') do
159
+ header_children.each { |child_block| child_block.call(self) }
160
+ end
161
+ tag('soap:Body') do
162
+ body_children.each { |child_block| child_block.call(self) }
163
+ end
164
+ end
165
+ end
166
+
167
+ # Append children from another Document into this document.
168
+ #
169
+ # Copies all top-level children from the source document into the current
170
+ # insertion point (either the document root or the current parent element).
171
+ #
172
+ # @param other [Document] the source document whose children to import
173
+ # @return [void]
174
+ def append(other)
175
+ raise Error, 'append expects a Document' unless other.is_a?(Document)
176
+
177
+ other.children.each do |child|
178
+ current_parent.push(child)
179
+ end
180
+ end
181
+
182
+ # Insert a raw XML fragment string into the current position.
183
+ #
184
+ # This is an alias for #raw, provided for semantic clarity when composing
185
+ # fragments.
186
+ #
187
+ # @param xml_string [String] the XML fragment to insert
188
+ # @return [void]
189
+ def insert_fragment(xml_string)
190
+ raw(xml_string)
191
+ end
192
+
102
193
  # Support method_missing for DSL-style tag creation.
103
194
  #
104
195
  # @example
@@ -50,7 +50,8 @@ module Philiprehberger
50
50
  return '' if @attributes.empty?
51
51
 
52
52
  pairs = @attributes.map do |key, value|
53
- " #{key}=\"#{Escaper.escape(value)}\""
53
+ attr_name = key.to_s.gsub('__', ':')
54
+ " #{attr_name}=\"#{Escaper.escape(value)}\""
54
55
  end
55
56
  pairs.join
56
57
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module XmlBuilder
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -20,5 +20,21 @@ module Philiprehberger
20
20
  block.call(doc)
21
21
  doc.to_s
22
22
  end
23
+
24
+ # Build a SOAP envelope document.
25
+ #
26
+ # Convenience wrapper around Document#soap_envelope that creates
27
+ # a full XML document with the proper SOAP structure.
28
+ #
29
+ # @param soap_version [String] SOAP version: "1.1" or "1.2"
30
+ # @param encoding [String] XML encoding declaration (default: "UTF-8")
31
+ # @param version [String] XML version declaration (default: "1.0")
32
+ # @yield [header, body] yields two arrays; push lambdas that accept a doc
33
+ # @return [String] the rendered SOAP XML string
34
+ def self.build_soap(soap_version: '1.1', encoding: 'UTF-8', version: '1.0', &block)
35
+ doc = Document.new(version: version, encoding: encoding)
36
+ doc.soap_envelope(version: soap_version, &block)
37
+ doc.to_s
38
+ end
23
39
  end
24
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-xml_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-27 00:00:00.000000000 Z
11
+ date: 2026-03-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Programmatic XML construction with a clean DSL, auto-escaping, CDATA,
14
14
  comments, processing instructions, and pretty printing. Zero dependencies.