moxml 0.1.7 → 0.1.8
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/.github/workflows/dependent-repos.json +5 -0
- data/.github/workflows/dependent-tests.yml +20 -0
- data/.github/workflows/docs.yml +59 -0
- data/.github/workflows/rake.yml +10 -10
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +238 -40
- data/Gemfile +14 -9
- data/LICENSE.md +6 -2
- data/README.adoc +535 -373
- data/Rakefile +53 -0
- data/benchmarks/.gitignore +6 -0
- data/benchmarks/generate_report.rb +550 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +138 -0
- data/docs/_guides/advanced-features.adoc +87 -0
- data/docs/_guides/development-testing.adoc +165 -0
- data/docs/_guides/index.adoc +45 -0
- data/docs/_guides/modifying-xml.adoc +293 -0
- data/docs/_guides/parsing-xml.adoc +231 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +98 -0
- data/docs/_pages/adapters/libxml.adoc +286 -0
- data/docs/_pages/adapters/nokogiri.adoc +252 -0
- data/docs/_pages/adapters/oga.adoc +292 -0
- data/docs/_pages/adapters/ox.adoc +55 -0
- data/docs/_pages/adapters/rexml.adoc +293 -0
- data/docs/_pages/best-practices.adoc +430 -0
- data/docs/_pages/compatibility.adoc +468 -0
- data/docs/_pages/configuration.adoc +251 -0
- data/docs/_pages/error-handling.adoc +350 -0
- data/docs/_pages/headed-ox-limitations.adoc +558 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +141 -0
- data/docs/_pages/node-api-reference.adoc +50 -0
- data/docs/_pages/performance.adoc +36 -0
- data/docs/_pages/quick-start.adoc +244 -0
- data/docs/_pages/thread-safety.adoc +29 -0
- data/docs/_references/document-api.adoc +408 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +268 -0
- data/docs/_tutorials/builder-pattern.adoc +343 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +325 -0
- data/docs/_tutorials/xpath-queries.adoc +359 -0
- data/docs/index.adoc +122 -0
- data/examples/README.md +124 -0
- data/examples/api_client/README.md +424 -0
- data/examples/api_client/api_client.rb +394 -0
- data/examples/api_client/example_response.xml +48 -0
- data/examples/headed_ox_example/README.md +90 -0
- data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
- data/examples/rss_parser/README.md +194 -0
- data/examples/rss_parser/example_feed.xml +93 -0
- data/examples/rss_parser/rss_parser.rb +189 -0
- data/examples/sax_parsing/README.md +50 -0
- data/examples/sax_parsing/data_extractor.rb +75 -0
- data/examples/sax_parsing/example.xml +21 -0
- data/examples/sax_parsing/large_file.rb +78 -0
- data/examples/sax_parsing/simple_parser.rb +55 -0
- data/examples/web_scraper/README.md +352 -0
- data/examples/web_scraper/example_page.html +201 -0
- data/examples/web_scraper/web_scraper.rb +312 -0
- data/lib/moxml/adapter/base.rb +107 -28
- data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
- data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
- data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
- data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
- data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
- data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1548 -0
- data/lib/moxml/adapter/nokogiri.rb +121 -9
- data/lib/moxml/adapter/oga.rb +123 -12
- data/lib/moxml/adapter/ox.rb +282 -26
- data/lib/moxml/adapter/rexml.rb +127 -20
- data/lib/moxml/adapter.rb +21 -4
- data/lib/moxml/attribute.rb +6 -0
- data/lib/moxml/builder.rb +40 -4
- data/lib/moxml/config.rb +8 -3
- data/lib/moxml/context.rb +39 -1
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +39 -6
- data/lib/moxml/document_builder.rb +27 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +94 -3
- data/lib/moxml/node_set.rb +34 -0
- data/lib/moxml/sax/block_handler.rb +194 -0
- data/lib/moxml/sax/element_handler.rb +124 -0
- data/lib/moxml/sax/handler.rb +113 -0
- data/lib/moxml/sax.rb +31 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +4 -4
- data/lib/moxml/xml_utils.rb +7 -4
- data/lib/moxml/xpath/ast/node.rb +159 -0
- data/lib/moxml/xpath/cache.rb +91 -0
- data/lib/moxml/xpath/compiler.rb +1768 -0
- data/lib/moxml/xpath/context.rb +26 -0
- data/lib/moxml/xpath/conversion.rb +124 -0
- data/lib/moxml/xpath/engine.rb +52 -0
- data/lib/moxml/xpath/errors.rb +101 -0
- data/lib/moxml/xpath/lexer.rb +304 -0
- data/lib/moxml/xpath/parser.rb +485 -0
- data/lib/moxml/xpath/ruby/generator.rb +269 -0
- data/lib/moxml/xpath/ruby/node.rb +193 -0
- data/lib/moxml/xpath.rb +37 -0
- data/lib/moxml.rb +5 -2
- data/moxml.gemspec +3 -1
- data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
- data/spec/consistency/README.md +77 -0
- data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
- data/spec/examples/README.md +75 -0
- data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
- data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
- data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
- data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
- data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
- data/spec/integration/README.md +71 -0
- data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
- data/spec/integration/headed_ox_integration_spec.rb +326 -0
- data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
- data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
- data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
- data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
- data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
- data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
- data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
- data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
- data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
- data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
- data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
- data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
- data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
- data/spec/moxml/README.md +41 -0
- data/spec/moxml/adapter/.gitkeep +0 -0
- data/spec/moxml/adapter/README.md +61 -0
- data/spec/moxml/adapter/base_spec.rb +27 -0
- data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
- data/spec/moxml/adapter/libxml_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +9 -8
- data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
- data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
- data/spec/moxml/adapter_spec.rb +16 -0
- data/spec/moxml/attribute_spec.rb +30 -0
- data/spec/moxml/builder_spec.rb +33 -0
- data/spec/moxml/cdata_spec.rb +31 -0
- data/spec/moxml/comment_spec.rb +31 -0
- data/spec/moxml/config_spec.rb +3 -3
- data/spec/moxml/context_spec.rb +28 -0
- data/spec/moxml/declaration_spec.rb +36 -0
- data/spec/moxml/doctype_spec.rb +33 -0
- data/spec/moxml/document_builder_spec.rb +30 -0
- data/spec/moxml/document_spec.rb +105 -0
- data/spec/moxml/element_spec.rb +143 -0
- data/spec/moxml/error_spec.rb +266 -22
- data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
- data/spec/moxml/namespace_spec.rb +32 -0
- data/spec/moxml/node_set_spec.rb +39 -0
- data/spec/moxml/node_spec.rb +37 -0
- data/spec/moxml/processing_instruction_spec.rb +34 -0
- data/spec/moxml/sax_spec.rb +1067 -0
- data/spec/moxml/text_spec.rb +31 -0
- data/spec/moxml/version_spec.rb +14 -0
- data/spec/moxml/xml_utils/.gitkeep +0 -0
- data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
- data/spec/moxml/xml_utils_spec.rb +49 -0
- data/spec/moxml/xpath/ast/node_spec.rb +83 -0
- data/spec/moxml/xpath/axes_spec.rb +296 -0
- data/spec/moxml/xpath/cache_spec.rb +358 -0
- data/spec/moxml/xpath/compiler_spec.rb +406 -0
- data/spec/moxml/xpath/context_spec.rb +210 -0
- data/spec/moxml/xpath/conversion_spec.rb +365 -0
- data/spec/moxml/xpath/fixtures/sample.xml +25 -0
- data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
- data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
- data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
- data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
- data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
- data/spec/moxml/xpath/lexer_spec.rb +488 -0
- data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
- data/spec/moxml/xpath/parser_spec.rb +364 -0
- data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
- data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
- data/spec/moxml/xpath_capabilities_spec.rb +199 -0
- data/spec/moxml/xpath_spec.rb +77 -0
- data/spec/performance/README.md +83 -0
- data/spec/performance/benchmark_spec.rb +64 -0
- data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
- data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
- data/spec/performance/xpath_benchmark_spec.rb +259 -0
- data/spec/spec_helper.rb +58 -1
- data/spec/support/xml_matchers.rb +1 -1
- metadata +176 -34
- data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
- /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# API Client Example
|
|
5
|
+
# This example demonstrates how to use Moxml for API interactions:
|
|
6
|
+
# - Building XML API requests (SOAP)
|
|
7
|
+
# - Parsing XML API responses
|
|
8
|
+
# - Handling authentication elements
|
|
9
|
+
# - Working with namespaces
|
|
10
|
+
# - Error handling and validation
|
|
11
|
+
|
|
12
|
+
# Load moxml from the local source (use 'require "moxml"' in production)
|
|
13
|
+
require_relative "../../lib/moxml"
|
|
14
|
+
require "securerandom"
|
|
15
|
+
require "time"
|
|
16
|
+
|
|
17
|
+
# User class to represent API user data
|
|
18
|
+
class User
|
|
19
|
+
attr_reader :id, :username, :email, :full_name, :role, :status,
|
|
20
|
+
:created_at, :last_login, :permissions, :profile
|
|
21
|
+
|
|
22
|
+
def initialize(data)
|
|
23
|
+
@id = data[:id]
|
|
24
|
+
@username = data[:username]
|
|
25
|
+
@email = data[:email]
|
|
26
|
+
@full_name = data[:full_name]
|
|
27
|
+
@role = data[:role]
|
|
28
|
+
@status = data[:status]
|
|
29
|
+
@created_at = data[:created_at]
|
|
30
|
+
@last_login = data[:last_login]
|
|
31
|
+
@permissions = data[:permissions] || []
|
|
32
|
+
@profile = data[:profile] || {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
output = []
|
|
37
|
+
output << "User ID: #{@id}"
|
|
38
|
+
output << "Username: #{@username}"
|
|
39
|
+
output << "Email: #{@email}"
|
|
40
|
+
output << "Full Name: #{@full_name}"
|
|
41
|
+
output << "Role: #{@role}"
|
|
42
|
+
output << "Status: #{@status}"
|
|
43
|
+
output << "Created: #{@created_at}"
|
|
44
|
+
output << "Last Login: #{@last_login}"
|
|
45
|
+
output << "Permissions: #{@permissions.join(', ')}"
|
|
46
|
+
output << "Profile:"
|
|
47
|
+
@profile.each { |key, value| output << " #{key}: #{value}" }
|
|
48
|
+
output.join("\n")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# APIResponse class to encapsulate API response data
|
|
53
|
+
class APIResponse
|
|
54
|
+
attr_reader :status_code, :message, :data, :metadata, :session_id, :request_id
|
|
55
|
+
|
|
56
|
+
def initialize(status_code:, message:, data: nil, metadata: {},
|
|
57
|
+
session_id: nil, request_id: nil)
|
|
58
|
+
@status_code = status_code.to_i
|
|
59
|
+
@message = message
|
|
60
|
+
@data = data
|
|
61
|
+
@metadata = metadata
|
|
62
|
+
@session_id = session_id
|
|
63
|
+
@request_id = request_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def success?
|
|
67
|
+
@status_code >= 200 && @status_code < 300
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s
|
|
71
|
+
output = []
|
|
72
|
+
output << "Status: #{@status_code} - #{@message}"
|
|
73
|
+
output << "Session ID: #{@session_id}" if @session_id
|
|
74
|
+
output << "Request ID: #{@request_id}" if @request_id
|
|
75
|
+
output << "Metadata: #{@metadata.inspect}" unless @metadata.empty?
|
|
76
|
+
output.join("\n")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# SOAPClient class for making SOAP API requests
|
|
81
|
+
class SOAPClient
|
|
82
|
+
# SOAP namespaces used in requests/responses
|
|
83
|
+
NAMESPACES = {
|
|
84
|
+
"soap" => "http://schemas.xmlsoap.org/soap/envelope/",
|
|
85
|
+
"xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
|
86
|
+
"xsd" => "http://www.w3.org/2001/XMLSchema",
|
|
87
|
+
"auth" => "http://api.example.com/auth",
|
|
88
|
+
"users" => "http://api.example.com/users",
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
def initialize
|
|
92
|
+
@moxml = Moxml.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build a SOAP GetUser request
|
|
96
|
+
def build_get_user_request(user_id, session_id = nil)
|
|
97
|
+
# Generate request ID and timestamp
|
|
98
|
+
request_id = "req-#{SecureRandom.hex(6)}"
|
|
99
|
+
timestamp = Time.now.utc.iso8601
|
|
100
|
+
|
|
101
|
+
# Build the SOAP envelope using Moxml::Builder
|
|
102
|
+
Moxml::Builder.new(@moxml).build do
|
|
103
|
+
# XML declaration
|
|
104
|
+
declaration version: "1.0", encoding: "UTF-8"
|
|
105
|
+
|
|
106
|
+
# SOAP Envelope with namespaces
|
|
107
|
+
element "soap:Envelope",
|
|
108
|
+
"xmlns:soap" => NAMESPACES["soap"],
|
|
109
|
+
"xmlns:xsi" => NAMESPACES["xsi"],
|
|
110
|
+
"xmlns:xsd" => NAMESPACES["xsd"] do
|
|
111
|
+
# SOAP Header with authentication
|
|
112
|
+
element "soap:Header" do
|
|
113
|
+
element "AuthHeader", "xmlns" => NAMESPACES["auth"] do
|
|
114
|
+
element "SessionId" do
|
|
115
|
+
text(session_id || "demo-session-id")
|
|
116
|
+
end
|
|
117
|
+
element "Timestamp" do
|
|
118
|
+
text timestamp
|
|
119
|
+
end
|
|
120
|
+
element "RequestId" do
|
|
121
|
+
text request_id
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# SOAP Body with request
|
|
127
|
+
element "soap:Body" do
|
|
128
|
+
element "GetUserRequest", "xmlns" => NAMESPACES["users"] do
|
|
129
|
+
element "UserId" do
|
|
130
|
+
text user_id.to_s
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Build a SOAP CreateUser request
|
|
139
|
+
def build_create_user_request(username, email, full_name, role)
|
|
140
|
+
request_id = "req-#{SecureRandom.hex(6)}"
|
|
141
|
+
timestamp = Time.now.utc.iso8601
|
|
142
|
+
|
|
143
|
+
Moxml::Builder.new(@moxml).build do
|
|
144
|
+
declaration version: "1.0", encoding: "UTF-8"
|
|
145
|
+
|
|
146
|
+
element "soap:Envelope",
|
|
147
|
+
"xmlns:soap" => NAMESPACES["soap"],
|
|
148
|
+
"xmlns:xsi" => NAMESPACES["xsi"],
|
|
149
|
+
"xmlns:xsd" => NAMESPACES["xsd"] do
|
|
150
|
+
element "soap:Header" do
|
|
151
|
+
element "AuthHeader", "xmlns" => NAMESPACES["auth"] do
|
|
152
|
+
element "SessionId" do
|
|
153
|
+
text "demo-session-id"
|
|
154
|
+
end
|
|
155
|
+
element "Timestamp" do
|
|
156
|
+
text timestamp
|
|
157
|
+
end
|
|
158
|
+
element "RequestId" do
|
|
159
|
+
text request_id
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
element "soap:Body" do
|
|
165
|
+
element "CreateUserRequest", "xmlns" => NAMESPACES["users"] do
|
|
166
|
+
element "Username" do
|
|
167
|
+
text username
|
|
168
|
+
end
|
|
169
|
+
element "Email" do
|
|
170
|
+
text email
|
|
171
|
+
end
|
|
172
|
+
element "FullName" do
|
|
173
|
+
text full_name
|
|
174
|
+
end
|
|
175
|
+
element "Role" do
|
|
176
|
+
text role
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Parse a SOAP response
|
|
185
|
+
def parse_response(xml_string)
|
|
186
|
+
# Parse the XML response
|
|
187
|
+
doc = begin
|
|
188
|
+
@moxml.parse(xml_string)
|
|
189
|
+
rescue Moxml::ParseError => e
|
|
190
|
+
puts "Failed to parse API response: #{e.message}"
|
|
191
|
+
raise
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Extract authentication header
|
|
195
|
+
session_id = extract_text(doc, "//auth:SessionId", NAMESPACES)
|
|
196
|
+
request_id = extract_text(doc, "//auth:RequestId", NAMESPACES)
|
|
197
|
+
|
|
198
|
+
# Extract status information
|
|
199
|
+
status_code = extract_text(doc, "//users:Status/users:Code", NAMESPACES)
|
|
200
|
+
status_message = extract_text(doc, "//users:Status/users:Message",
|
|
201
|
+
NAMESPACES)
|
|
202
|
+
|
|
203
|
+
# Extract metadata
|
|
204
|
+
metadata = extract_metadata(doc)
|
|
205
|
+
|
|
206
|
+
# Check if response contains user data
|
|
207
|
+
user_element = doc.at_xpath("//users:User", NAMESPACES)
|
|
208
|
+
user_data = user_element ? parse_user(user_element) : nil
|
|
209
|
+
|
|
210
|
+
APIResponse.new(
|
|
211
|
+
status_code: status_code,
|
|
212
|
+
message: status_message,
|
|
213
|
+
data: user_data,
|
|
214
|
+
metadata: metadata,
|
|
215
|
+
session_id: session_id,
|
|
216
|
+
request_id: request_id,
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
# Parse user data from XML element
|
|
223
|
+
def parse_user(user_element)
|
|
224
|
+
# Extract basic user fields
|
|
225
|
+
id = extract_text(user_element, "./users:Id", NAMESPACES)
|
|
226
|
+
username = extract_text(user_element, "./users:Username", NAMESPACES)
|
|
227
|
+
email = extract_text(user_element, "./users:Email", NAMESPACES)
|
|
228
|
+
full_name = extract_text(user_element, "./users:FullName", NAMESPACES)
|
|
229
|
+
role = extract_text(user_element, "./users:Role", NAMESPACES)
|
|
230
|
+
status = extract_text(user_element, "./users:Status", NAMESPACES)
|
|
231
|
+
created_at = extract_text(user_element, "./users:CreatedAt", NAMESPACES)
|
|
232
|
+
last_login = extract_text(user_element, "./users:LastLogin", NAMESPACES)
|
|
233
|
+
|
|
234
|
+
# Extract permissions array
|
|
235
|
+
permission_nodes = user_element.xpath(
|
|
236
|
+
"./users:Permissions/users:Permission", NAMESPACES
|
|
237
|
+
)
|
|
238
|
+
permissions = permission_nodes.map(&:text)
|
|
239
|
+
|
|
240
|
+
# Extract profile data
|
|
241
|
+
profile_element = user_element.at_xpath("./users:Profile", NAMESPACES)
|
|
242
|
+
profile = if profile_element
|
|
243
|
+
{
|
|
244
|
+
"Department" => extract_text(profile_element, "./users:Department",
|
|
245
|
+
NAMESPACES),
|
|
246
|
+
"Title" => extract_text(profile_element, "./users:Title",
|
|
247
|
+
NAMESPACES),
|
|
248
|
+
"Location" => extract_text(profile_element, "./users:Location",
|
|
249
|
+
NAMESPACES),
|
|
250
|
+
"PhoneNumber" => extract_text(profile_element, "./users:PhoneNumber",
|
|
251
|
+
NAMESPACES),
|
|
252
|
+
}
|
|
253
|
+
else
|
|
254
|
+
{}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
User.new(
|
|
258
|
+
id: id,
|
|
259
|
+
username: username,
|
|
260
|
+
email: email,
|
|
261
|
+
full_name: full_name,
|
|
262
|
+
role: role,
|
|
263
|
+
status: status,
|
|
264
|
+
created_at: created_at,
|
|
265
|
+
last_login: last_login,
|
|
266
|
+
permissions: permissions,
|
|
267
|
+
profile: profile,
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Extract metadata from response
|
|
272
|
+
def extract_metadata(doc)
|
|
273
|
+
metadata_element = doc.at_xpath("//users:Metadata", NAMESPACES)
|
|
274
|
+
return {} unless metadata_element
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
response_time: extract_text(metadata_element, "./users:ResponseTime",
|
|
278
|
+
NAMESPACES),
|
|
279
|
+
server_version: extract_text(metadata_element, "./users:ServerVersion",
|
|
280
|
+
NAMESPACES),
|
|
281
|
+
cache_hit: extract_text(metadata_element, "./users:CacheHit", NAMESPACES),
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Helper method to safely extract text from XPath
|
|
286
|
+
def extract_text(node, xpath, namespaces = {})
|
|
287
|
+
element = node.at_xpath(xpath, namespaces)
|
|
288
|
+
element&.text&.strip || ""
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Demonstrate request building
|
|
293
|
+
def demonstrate_request_building
|
|
294
|
+
puts "=" * 80
|
|
295
|
+
puts "Building SOAP Requests"
|
|
296
|
+
puts "=" * 80
|
|
297
|
+
puts
|
|
298
|
+
|
|
299
|
+
client = SOAPClient.new
|
|
300
|
+
|
|
301
|
+
# Build GetUser request
|
|
302
|
+
puts "1. GetUser Request:"
|
|
303
|
+
puts "-" * 80
|
|
304
|
+
get_user_doc = client.build_get_user_request(1001, "session-abc-123")
|
|
305
|
+
puts get_user_doc.to_xml(indent: 2)
|
|
306
|
+
puts
|
|
307
|
+
|
|
308
|
+
# Build CreateUser request
|
|
309
|
+
puts "2. CreateUser Request:"
|
|
310
|
+
puts "-" * 80
|
|
311
|
+
create_user_doc = client.build_create_user_request(
|
|
312
|
+
"janedoe",
|
|
313
|
+
"jane.doe@example.com",
|
|
314
|
+
"Jane Doe",
|
|
315
|
+
"Developer",
|
|
316
|
+
)
|
|
317
|
+
puts create_user_doc.to_xml(indent: 2)
|
|
318
|
+
puts
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Demonstrate response parsing
|
|
322
|
+
def demonstrate_response_parsing(response_file)
|
|
323
|
+
puts "=" * 80
|
|
324
|
+
puts "Parsing SOAP Response"
|
|
325
|
+
puts "=" * 80
|
|
326
|
+
puts
|
|
327
|
+
|
|
328
|
+
# Read response XML
|
|
329
|
+
xml_content = File.read(response_file)
|
|
330
|
+
|
|
331
|
+
# Parse response
|
|
332
|
+
client = SOAPClient.new
|
|
333
|
+
response = begin
|
|
334
|
+
client.parse_response(xml_content)
|
|
335
|
+
rescue Moxml::ParseError => e
|
|
336
|
+
puts "Error parsing response: #{e.message}"
|
|
337
|
+
return
|
|
338
|
+
rescue Moxml::XPathError => e
|
|
339
|
+
puts "Error querying response: #{e.message}"
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Display response information
|
|
344
|
+
puts "Response Information:"
|
|
345
|
+
puts "-" * 80
|
|
346
|
+
puts response
|
|
347
|
+
puts
|
|
348
|
+
|
|
349
|
+
# Display user data if present
|
|
350
|
+
if response.data
|
|
351
|
+
puts "User Data:"
|
|
352
|
+
puts "-" * 80
|
|
353
|
+
puts response.data
|
|
354
|
+
puts
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Show success/failure
|
|
358
|
+
puts "Result: #{response.success? ? 'SUCCESS ✓' : 'FAILED ✗'}"
|
|
359
|
+
puts
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Main execution
|
|
363
|
+
if __FILE__ == $0
|
|
364
|
+
puts "SOAP API Client Example"
|
|
365
|
+
puts "=" * 80
|
|
366
|
+
puts
|
|
367
|
+
|
|
368
|
+
# Demonstrate building requests
|
|
369
|
+
demonstrate_request_building
|
|
370
|
+
|
|
371
|
+
# Demonstrate parsing responses
|
|
372
|
+
response_file = ARGV[0] || File.join(__dir__, "example_response.xml")
|
|
373
|
+
|
|
374
|
+
unless File.exist?(response_file)
|
|
375
|
+
puts "Error: Response file not found: #{response_file}"
|
|
376
|
+
puts "Usage: ruby api_client.rb [path/to/response.xml]"
|
|
377
|
+
exit 1
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
demonstrate_response_parsing(response_file)
|
|
381
|
+
|
|
382
|
+
# Summary
|
|
383
|
+
puts "=" * 80
|
|
384
|
+
puts "API Client Example Complete"
|
|
385
|
+
puts "=" * 80
|
|
386
|
+
puts
|
|
387
|
+
puts "Key Takeaways:"
|
|
388
|
+
puts " - Use Moxml::Builder for clean XML request construction"
|
|
389
|
+
puts " - Namespace handling is crucial for SOAP/XML APIs"
|
|
390
|
+
puts " - XPath with namespaces extracts response data efficiently"
|
|
391
|
+
puts " - Proper error handling ensures robust API interactions"
|
|
392
|
+
puts " - Structure data into Ruby objects for easy manipulation"
|
|
393
|
+
puts "=" * 80
|
|
394
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
3
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
4
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
5
|
+
<soap:Header>
|
|
6
|
+
<AuthHeader xmlns="http://api.example.com/auth">
|
|
7
|
+
<SessionId>a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6</SessionId>
|
|
8
|
+
<Timestamp>2024-10-30T10:00:00Z</Timestamp>
|
|
9
|
+
<RequestId>req-12345</RequestId>
|
|
10
|
+
</AuthHeader>
|
|
11
|
+
</soap:Header>
|
|
12
|
+
<soap:Body>
|
|
13
|
+
<GetUserResponse xmlns="http://api.example.com/users">
|
|
14
|
+
<Status>
|
|
15
|
+
<Code>200</Code>
|
|
16
|
+
<Message>Success</Message>
|
|
17
|
+
</Status>
|
|
18
|
+
<Result>
|
|
19
|
+
<User>
|
|
20
|
+
<Id>1001</Id>
|
|
21
|
+
<Username>johndoe</Username>
|
|
22
|
+
<Email>john.doe@example.com</Email>
|
|
23
|
+
<FullName>John Doe</FullName>
|
|
24
|
+
<Role>Administrator</Role>
|
|
25
|
+
<Status>Active</Status>
|
|
26
|
+
<CreatedAt>2024-01-15T08:30:00Z</CreatedAt>
|
|
27
|
+
<LastLogin>2024-10-29T14:22:00Z</LastLogin>
|
|
28
|
+
<Permissions>
|
|
29
|
+
<Permission>users.read</Permission>
|
|
30
|
+
<Permission>users.write</Permission>
|
|
31
|
+
<Permission>admin.access</Permission>
|
|
32
|
+
</Permissions>
|
|
33
|
+
<Profile>
|
|
34
|
+
<Department>Engineering</Department>
|
|
35
|
+
<Title>Senior Developer</Title>
|
|
36
|
+
<Location>San Francisco, CA</Location>
|
|
37
|
+
<PhoneNumber>+1-555-0123</PhoneNumber>
|
|
38
|
+
</Profile>
|
|
39
|
+
</User>
|
|
40
|
+
</Result>
|
|
41
|
+
<Metadata>
|
|
42
|
+
<ResponseTime>45</ResponseTime>
|
|
43
|
+
<ServerVersion>2.1.0</ServerVersion>
|
|
44
|
+
<CacheHit>false</CacheHit>
|
|
45
|
+
</Metadata>
|
|
46
|
+
</GetUserResponse>
|
|
47
|
+
</soap:Body>
|
|
48
|
+
</soap:Envelope>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# HeadedOx Demo
|
|
2
|
+
|
|
3
|
+
This example demonstrates the HeadedOx adapter, which combines:
|
|
4
|
+
- Ox's fast C-based XML parsing
|
|
5
|
+
- Moxml's comprehensive pure Ruby XPath 1.0 engine
|
|
6
|
+
|
|
7
|
+
## What is HeadedOx?
|
|
8
|
+
|
|
9
|
+
HeadedOx is a hybrid adapter that provides:
|
|
10
|
+
- **Fast parsing:** Uses Ox's C-based parser for speed
|
|
11
|
+
- **Full XPath 1.0:** All 27 XPath functions and 6 common axes
|
|
12
|
+
- **Production ready:** 99.20% test pass rate (1,992/2,008 tests)
|
|
13
|
+
- **Pure Ruby XPath:** Debuggable implementation with expression caching
|
|
14
|
+
- **Best of both:** Combines Ox speed with comprehensive XPath support
|
|
15
|
+
|
|
16
|
+
## Running the Demo
|
|
17
|
+
|
|
18
|
+
### For Development (from gem source):
|
|
19
|
+
```bash
|
|
20
|
+
# From the moxml root directory
|
|
21
|
+
bundle exec ruby examples/headed_ox_example/headed_ox_demo.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Note: The example uses `require_relative` to load Moxml from source, which requires
|
|
25
|
+
`bundle exec` to properly resolve dependencies in development.
|
|
26
|
+
|
|
27
|
+
### After Installation:
|
|
28
|
+
```bash
|
|
29
|
+
# When using the installed gem
|
|
30
|
+
ruby headed_ox_demo.rb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features Demonstrated
|
|
34
|
+
|
|
35
|
+
1. **Descendant queries** - `//book` syntax
|
|
36
|
+
2. **Attribute selection** - `@price` syntax
|
|
37
|
+
3. **Predicates** - `[@price < 20]` filtering
|
|
38
|
+
4. **XPath functions** - count(), sum(), string(), contains()
|
|
39
|
+
5. **Complex queries** - Combining multiple features
|
|
40
|
+
6. **Variable binding** - `$var` support
|
|
41
|
+
|
|
42
|
+
## Output
|
|
43
|
+
|
|
44
|
+
The demo shows HeadedOx executing various XPath queries on a sample library XML,
|
|
45
|
+
demonstrating the full range of XPath 1.0 capabilities.
|
|
46
|
+
|
|
47
|
+
## Expected Output
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
============================================================
|
|
51
|
+
HeadedOx Demo - Comprehensive XPath on Fast Ox Parsing
|
|
52
|
+
============================================================
|
|
53
|
+
|
|
54
|
+
1. Find all books:
|
|
55
|
+
Found 3 books
|
|
56
|
+
|
|
57
|
+
2. Get all prices:
|
|
58
|
+
Prices: 15.99, 25.99, 12.99
|
|
59
|
+
|
|
60
|
+
3. Find cheap books (< $20):
|
|
61
|
+
- Programming Ruby: $15.99
|
|
62
|
+
- Programming JavaScript: $12.99
|
|
63
|
+
|
|
64
|
+
4. XPath functions:
|
|
65
|
+
Total books: 3
|
|
66
|
+
Total price: $54.97
|
|
67
|
+
First title: Programming Ruby
|
|
68
|
+
|
|
69
|
+
5. Books with 'Ruby' in title:
|
|
70
|
+
- Programming Ruby
|
|
71
|
+
|
|
72
|
+
6. Using variables:
|
|
73
|
+
Books under $20: 2
|
|
74
|
+
|
|
75
|
+
============================================================
|
|
76
|
+
HeadedOx provides full XPath 1.0 support!
|
|
77
|
+
============================================================
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Why Choose HeadedOx?
|
|
81
|
+
|
|
82
|
+
Use HeadedOx when you need:
|
|
83
|
+
- Fast XML parsing (Ox's strength)
|
|
84
|
+
- Comprehensive XPath beyond basic locate()
|
|
85
|
+
- XPath 1.0 functions like count(), sum(), contains()
|
|
86
|
+
- Complex predicates and expressions
|
|
87
|
+
- Debuggable XPath implementation
|
|
88
|
+
- Production-ready stability (99.20% test coverage)
|
|
89
|
+
|
|
90
|
+
See `docs/HEADED_OX_LIMITATIONS.md` for detailed capabilities and known limitations.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative "../../lib/moxml"
|
|
2
|
+
|
|
3
|
+
# HeadedOx Adapter Demo
|
|
4
|
+
# Demonstrates Ox's fast parsing + comprehensive XPath
|
|
5
|
+
|
|
6
|
+
# Sample XML
|
|
7
|
+
xml = <<~XML
|
|
8
|
+
<library>
|
|
9
|
+
<book id="1" price="15.99" year="2020">
|
|
10
|
+
<title>Programming Ruby</title>
|
|
11
|
+
<author>Matz</author>
|
|
12
|
+
<isbn>978-1234567890</isbn>
|
|
13
|
+
</book>
|
|
14
|
+
<book id="2" price="25.99" year="2021">
|
|
15
|
+
<title>Programming Python</title>
|
|
16
|
+
<author>Guido</author>
|
|
17
|
+
<isbn>978-0987654321</isbn>
|
|
18
|
+
</book>
|
|
19
|
+
<book id="3" price="12.99" year="2022">
|
|
20
|
+
<title>Programming JavaScript</title>
|
|
21
|
+
<author>Brendan</author>
|
|
22
|
+
<isbn>978-1122334455</isbn>
|
|
23
|
+
</book>
|
|
24
|
+
</library>
|
|
25
|
+
XML
|
|
26
|
+
|
|
27
|
+
# Initialize HeadedOx
|
|
28
|
+
context = Moxml.new(:headed_ox)
|
|
29
|
+
doc = context.parse(xml)
|
|
30
|
+
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
puts "HeadedOx Demo - Comprehensive XPath on Fast Ox Parsing"
|
|
33
|
+
puts "=" * 60
|
|
34
|
+
|
|
35
|
+
# 1. Basic descendant queries
|
|
36
|
+
puts "\n1. Find all books:"
|
|
37
|
+
books = doc.xpath("//book")
|
|
38
|
+
puts "Found #{books.size} books"
|
|
39
|
+
|
|
40
|
+
# 2. Attribute selection
|
|
41
|
+
puts "\n2. Get all prices:"
|
|
42
|
+
prices = doc.xpath("//book/@price")
|
|
43
|
+
puts "Prices: #{prices.map(&:value).join(', ')}"
|
|
44
|
+
|
|
45
|
+
# 3. Predicates
|
|
46
|
+
puts "\n3. Find cheap books (< $20):"
|
|
47
|
+
cheap = doc.xpath("//book[@price < 20]")
|
|
48
|
+
cheap.each do |book|
|
|
49
|
+
puts " - #{book.xpath('title').first.text}: $#{book['price']}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 4. XPath functions
|
|
53
|
+
puts "\n4. XPath functions:"
|
|
54
|
+
puts " Total books: #{doc.xpath('count(//book)')}"
|
|
55
|
+
puts " Total price: $#{doc.xpath('sum(//book/@price)')}"
|
|
56
|
+
puts " First title: #{doc.xpath('string(//book[1]/title)')}"
|
|
57
|
+
|
|
58
|
+
# 5. Complex queries
|
|
59
|
+
puts "\n5. Books with 'Ruby' in title:"
|
|
60
|
+
ruby_books = doc.xpath('//book[contains(title, "Ruby")]')
|
|
61
|
+
ruby_books.each { |book| puts " - #{book.xpath('title').first.text}" }
|
|
62
|
+
|
|
63
|
+
# 6. Variable binding
|
|
64
|
+
puts "\n6. Using variables:"
|
|
65
|
+
max_price = 20
|
|
66
|
+
affordable = doc.xpath("//book[@price < $max]", { "max" => max_price })
|
|
67
|
+
puts "Books under $#{max_price}: #{affordable.size}"
|
|
68
|
+
|
|
69
|
+
puts "\n#{'=' * 60}"
|
|
70
|
+
puts "HeadedOx provides full XPath 1.0 support!"
|
|
71
|
+
puts "=" * 60
|