serialbench 0.1.2 → 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 +4 -4
- data/.github/workflows/benchmark.yml +294 -229
- data/.github/workflows/rake.yml +11 -0
- data/.github/workflows/windows-debug.yml +171 -0
- data/.gitignore +32 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +274 -0
- data/Gemfile +15 -1
- data/README.adoc +41 -0
- data/data/schemas/result.yml +29 -0
- data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
- data/docs/SYCK_YAML_FIX.md +91 -0
- data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
- data/docs/WINDOWS_LIBXML_FIX.md +136 -0
- data/docs/WINDOWS_SETUP.md +122 -0
- data/lib/serialbench/benchmark_runner.rb +3 -3
- data/lib/serialbench/cli/benchmark_cli.rb +74 -1
- data/lib/serialbench/cli/environment_cli.rb +3 -3
- data/lib/serialbench/cli/resultset_cli.rb +112 -26
- data/lib/serialbench/cli/ruby_build_cli.rb +75 -88
- data/lib/serialbench/cli/validate_cli.rb +88 -0
- data/lib/serialbench/cli.rb +6 -2
- data/lib/serialbench/config_manager.rb +15 -26
- data/lib/serialbench/models/benchmark_config.rb +12 -0
- data/lib/serialbench/models/benchmark_result.rb +39 -3
- data/lib/serialbench/models/environment_config.rb +3 -2
- data/lib/serialbench/models/platform.rb +61 -6
- data/lib/serialbench/models/result.rb +28 -1
- data/lib/serialbench/models/result_set.rb +8 -0
- data/lib/serialbench/ruby_build_manager.rb +19 -23
- data/lib/serialbench/runners/asdf_runner.rb +1 -1
- data/lib/serialbench/runners/docker_runner.rb +2 -4
- data/lib/serialbench/runners/local_runner.rb +71 -0
- data/lib/serialbench/serializers/base_serializer.rb +1 -1
- data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -5
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -1
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -1
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +6 -1
- data/lib/serialbench/serializers/xml/leptris_serializer.rb +167 -0
- data/lib/serialbench/serializers/xml/libxml_serializer.rb +13 -8
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +11 -2
- data/lib/serialbench/serializers/xml/oga_serializer.rb +8 -8
- data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -3
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +1 -3
- data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -5
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +1 -1
- data/lib/serialbench/serializers.rb +5 -3
- data/lib/serialbench/site_generator.rb +234 -2
- data/lib/serialbench/templates/assets/css/format_based.css +1 -53
- data/lib/serialbench/templates/assets/css/themes.css +5 -4
- data/lib/serialbench/templates/assets/js/chart_helpers.js +44 -14
- data/lib/serialbench/templates/assets/js/dashboard.js +14 -15
- data/lib/serialbench/templates/format_based.liquid +480 -252
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench/yaml_validator.rb +36 -0
- data/serialbench.gemspec +11 -2
- data/site/.gitignore +5 -0
- data/site/README.md +32 -0
- data/site/astro.config.mjs +12 -0
- data/site/package-lock.json +6160 -0
- data/site/package.json +22 -0
- data/site/public/favicon.svg +4 -0
- data/site/scripts/gen-sample-data.mjs +154 -0
- data/site/src/components/AvailabilityMatrix.astro +61 -0
- data/site/src/components/CalibratedLeaderboard.vue +134 -0
- data/site/src/components/CitationBox.vue +50 -0
- data/site/src/components/DashboardConsole.vue +193 -0
- data/site/src/components/FeaturesTable.astro +97 -0
- data/site/src/components/MemoryPanel.vue +44 -0
- data/site/src/components/OpsChart.vue +135 -0
- data/site/src/config.ts +18 -0
- data/site/src/layouts/Layout.astro +85 -0
- data/site/src/lib/channels.ts +24 -0
- data/site/src/lib/dashboard.ts +269 -0
- data/site/src/pages/index.astro +44 -0
- data/site/src/pages/library/[slug].astro +128 -0
- data/site/src/pages/methodology.astro +55 -0
- data/site/src/styles/global.css +103 -0
- data/site/tsconfig.json +5 -0
- metadata +58 -23
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docker.yml +0 -272
|
@@ -14,6 +14,13 @@ module Serialbench
|
|
|
14
14
|
attribute :benchmark_config_path, :string
|
|
15
15
|
attribute :environment_config_path, :string
|
|
16
16
|
attribute :tags, :string, collection: true
|
|
17
|
+
|
|
18
|
+
key_value do
|
|
19
|
+
map 'created_at', to: :created_at
|
|
20
|
+
map 'benchmark_config_path', to: :benchmark_config_path
|
|
21
|
+
map 'environment_config_path', to: :environment_config_path
|
|
22
|
+
map 'tags', to: :tags
|
|
23
|
+
end
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
class Result < Lutaml::Model::Serializable
|
|
@@ -23,6 +30,14 @@ module Serialbench
|
|
|
23
30
|
attribute :benchmark_config, BenchmarkConfig
|
|
24
31
|
attribute :benchmark_result, BenchmarkResult
|
|
25
32
|
|
|
33
|
+
key_value do
|
|
34
|
+
map 'platform', to: :platform
|
|
35
|
+
map 'metadata', to: :metadata
|
|
36
|
+
map 'environment_config', to: :environment_config
|
|
37
|
+
map 'benchmark_config', to: :benchmark_config
|
|
38
|
+
map 'benchmark_result', to: :benchmark_result
|
|
39
|
+
end
|
|
40
|
+
|
|
26
41
|
def self.load(path)
|
|
27
42
|
raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
|
|
28
43
|
|
|
@@ -31,7 +46,19 @@ module Serialbench
|
|
|
31
46
|
|
|
32
47
|
raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
yaml_content = IO.read(data_file)
|
|
50
|
+
|
|
51
|
+
# Debug: Check if yaml_content is empty or too small
|
|
52
|
+
if yaml_content.nil? || yaml_content.strip.empty?
|
|
53
|
+
raise ArgumentError, "Results file at #{data_file} is empty"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if yaml_content.bytesize < 200
|
|
57
|
+
warn "WARNING: Results file at #{data_file} is suspiciously small (#{yaml_content.bytesize} bytes)"
|
|
58
|
+
warn "Content preview: #{yaml_content[0..100]}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
from_yaml(yaml_content)
|
|
35
62
|
end
|
|
36
63
|
|
|
37
64
|
def self.find_all(base_path = 'results/runs')
|
|
@@ -47,12 +47,20 @@ module Serialbench
|
|
|
47
47
|
|
|
48
48
|
result = Result.load(result_path)
|
|
49
49
|
|
|
50
|
+
# Validate that the result has required fields
|
|
51
|
+
raise ArgumentError, "Result from #{result_path} is missing platform information" if result.platform.nil?
|
|
52
|
+
raise ArgumentError, "Result from #{result_path} is missing environment_config" if result.environment_config.nil?
|
|
53
|
+
raise ArgumentError, "Result from #{result_path} is missing benchmark_config" if result.benchmark_config.nil?
|
|
54
|
+
|
|
50
55
|
# Check if result already exists:
|
|
51
56
|
# If environment_config.created_at is identical;
|
|
52
57
|
# If platform.platform_string is identical;
|
|
53
58
|
# If benchmark_config.benchmark_name is identical;
|
|
54
59
|
|
|
55
60
|
duplicates = results.select do |r|
|
|
61
|
+
# Skip results with nil platform (defensive check)
|
|
62
|
+
next if r.platform.nil? || result.platform.nil?
|
|
63
|
+
|
|
56
64
|
r.platform.platform_string == result.platform.platform_string &&
|
|
57
65
|
r.environment_config.created_at == result.environment_config.created_at &&
|
|
58
66
|
r.benchmark_config.benchmark_name == result.benchmark_config.benchmark_name
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require 'json'
|
|
3
|
+
require 'octokit'
|
|
5
4
|
require 'yaml'
|
|
6
5
|
require 'fileutils'
|
|
7
6
|
|
|
@@ -34,9 +33,7 @@ module Serialbench
|
|
|
34
33
|
def show_definition(tag)
|
|
35
34
|
definitions = load_definitions_from_cache
|
|
36
35
|
|
|
37
|
-
unless definitions.include?(tag)
|
|
38
|
-
raise "Ruby-Build definition '#{tag}' not found. Available definitions: #{definitions.length}"
|
|
39
|
-
end
|
|
36
|
+
raise "Ruby-Build definition '#{tag}' not found. Available definitions: #{definitions.length}" unless definitions.include?(tag)
|
|
40
37
|
|
|
41
38
|
{
|
|
42
39
|
tag: tag,
|
|
@@ -66,7 +63,7 @@ module Serialbench
|
|
|
66
63
|
variations = [
|
|
67
64
|
ruby_version,
|
|
68
65
|
"#{ruby_version}.0",
|
|
69
|
-
ruby_version.split('.')[0..1].join('.')
|
|
66
|
+
"#{ruby_version.split('.')[0..1].join('.')}.0"
|
|
70
67
|
]
|
|
71
68
|
|
|
72
69
|
variations.each do |variation|
|
|
@@ -101,30 +98,28 @@ module Serialbench
|
|
|
101
98
|
private
|
|
102
99
|
|
|
103
100
|
def fetch_definitions_from_github
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
request = Net::HTTP::Get.new(uri)
|
|
110
|
-
request['Accept'] = 'application/vnd.github.v3+json'
|
|
111
|
-
request['User-Agent'] = 'Serialbench Ruby-Build Manager'
|
|
112
|
-
|
|
113
|
-
response = http.request(request)
|
|
101
|
+
# Initialize client with optional authentication
|
|
102
|
+
# Falls back to unauthenticated if GITHUB_TOKEN not set
|
|
103
|
+
client_options = {}
|
|
104
|
+
client_options[:access_token] = ENV['GITHUB_TOKEN'] if ENV['GITHUB_TOKEN']
|
|
114
105
|
|
|
115
|
-
|
|
106
|
+
client = Octokit::Client.new(client_options)
|
|
116
107
|
|
|
117
|
-
|
|
108
|
+
contents = client.contents('rbenv/ruby-build', path: 'share/ruby-build')
|
|
118
109
|
|
|
119
110
|
# Extract definition names from the file list
|
|
120
|
-
definitions =
|
|
121
|
-
.select { |item| item[
|
|
122
|
-
.map { |item| item[
|
|
111
|
+
definitions = contents
|
|
112
|
+
.select { |item| item[:type] == 'file' && item[:name] != 'Makefile' }
|
|
113
|
+
.map { |item| item[:name] }
|
|
123
114
|
.sort
|
|
124
115
|
|
|
125
116
|
raise 'No Ruby-Build definitions found in GitHub response' if definitions.empty?
|
|
126
117
|
|
|
127
118
|
definitions
|
|
119
|
+
rescue Octokit::Error => e
|
|
120
|
+
warn "Warning: Error fetching Ruby-Build definitions from GitHub: #{e.message}"
|
|
121
|
+
warn "Note: Set GITHUB_TOKEN environment variable to avoid rate limits"
|
|
122
|
+
[]
|
|
128
123
|
end
|
|
129
124
|
|
|
130
125
|
def save_definitions_to_cache(definitions)
|
|
@@ -141,12 +136,13 @@ module Serialbench
|
|
|
141
136
|
end
|
|
142
137
|
|
|
143
138
|
def load_definitions_from_cache
|
|
144
|
-
|
|
139
|
+
return [] unless cache_exists?
|
|
145
140
|
|
|
146
141
|
cache_data = YAML.load_file(CACHE_FILE)
|
|
147
142
|
cache_data['definitions'] || []
|
|
148
143
|
rescue StandardError => e
|
|
149
|
-
|
|
144
|
+
warn "Warning: Failed to load Ruby-Build definitions from cache: #{e.message}"
|
|
145
|
+
[]
|
|
150
146
|
end
|
|
151
147
|
end
|
|
152
148
|
end
|
|
@@ -134,7 +134,7 @@ module Serialbench
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Run benchmark for specific Ruby version
|
|
137
|
-
def run_benchmark(benchmark_config,
|
|
137
|
+
def run_benchmark(benchmark_config, _benchmark_config_path, result_dir)
|
|
138
138
|
puts "🚀 Running benchmark for #{@name}..."
|
|
139
139
|
|
|
140
140
|
FileUtils.mkdir_p(result_dir)
|
|
@@ -78,9 +78,7 @@ module Serialbench
|
|
|
78
78
|
|
|
79
79
|
# Validate Docker is available
|
|
80
80
|
def validate_docker_available
|
|
81
|
-
unless system('docker --version > /dev/null 2>&1')
|
|
82
|
-
raise DockerError, 'Docker is not installed or not available in PATH'
|
|
83
|
-
end
|
|
81
|
+
raise DockerError, 'Docker is not installed or not available in PATH' unless system('docker --version > /dev/null 2>&1')
|
|
84
82
|
|
|
85
83
|
return if system('docker info > /dev/null 2>&1')
|
|
86
84
|
|
|
@@ -94,7 +92,7 @@ module Serialbench
|
|
|
94
92
|
end
|
|
95
93
|
|
|
96
94
|
# Run benchmark in container
|
|
97
|
-
def run_benchmark_in_container(
|
|
95
|
+
def run_benchmark_in_container(_benchmark_config, benchmark_config_path, result_dir)
|
|
98
96
|
puts '🏃 Running benchmark in Docker container...'
|
|
99
97
|
puts " 📁 Results will be saved to: #{result_dir}"
|
|
100
98
|
puts " 🐳 Using Docker image: #{docker_image_string}"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative '../benchmark_runner'
|
|
6
|
+
require_relative '../models/result'
|
|
7
|
+
require_relative '../models/platform'
|
|
8
|
+
|
|
9
|
+
module Serialbench
|
|
10
|
+
module Runners
|
|
11
|
+
# Handles local benchmark execution using the current Ruby environment
|
|
12
|
+
class LocalRunner < Base
|
|
13
|
+
def prepare
|
|
14
|
+
# For local environments, no preparation is needed
|
|
15
|
+
# The current Ruby environment is already set up
|
|
16
|
+
puts "✅ Local environment ready (using current Ruby: #{RUBY_VERSION})"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run_benchmark(benchmark_config, benchmark_config_path, result_dir)
|
|
20
|
+
puts "🏠 Running benchmark locally with Ruby #{RUBY_VERSION}..."
|
|
21
|
+
puts " 📁 Results will be saved to: #{result_dir}"
|
|
22
|
+
|
|
23
|
+
FileUtils.mkdir_p(result_dir)
|
|
24
|
+
|
|
25
|
+
# Run the benchmark
|
|
26
|
+
runner = Serialbench::BenchmarkRunner.new(
|
|
27
|
+
environment_config: @environment_config,
|
|
28
|
+
benchmark_config: benchmark_config
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
results = runner.run_all_benchmarks
|
|
32
|
+
|
|
33
|
+
# Get platform information with correct Ruby version from environment config
|
|
34
|
+
platform = Serialbench::Models::Platform.current_local(
|
|
35
|
+
ruby_version: @environment_config.ruby_build_tag
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Create metadata
|
|
39
|
+
metadata = Models::RunMetadata.new(
|
|
40
|
+
benchmark_config_path: benchmark_config_path,
|
|
41
|
+
environment_config_path: @environment_config_path,
|
|
42
|
+
tags: [
|
|
43
|
+
'local',
|
|
44
|
+
platform.os,
|
|
45
|
+
platform.arch,
|
|
46
|
+
"ruby-#{@environment_config.ruby_build_tag}"
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Save results
|
|
51
|
+
results_model = Models::Result.new(
|
|
52
|
+
platform: platform,
|
|
53
|
+
metadata: metadata,
|
|
54
|
+
environment_config: @environment_config,
|
|
55
|
+
benchmark_config: benchmark_config,
|
|
56
|
+
benchmark_result: results
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Restore YAML to use Psych for output, otherwise lutaml-model's to_yaml
|
|
60
|
+
# will have no output (Syck gem overrides YAML constant)
|
|
61
|
+
Object.const_set(:YAML, Psych)
|
|
62
|
+
|
|
63
|
+
results_file = File.join(result_dir, 'results.yaml')
|
|
64
|
+
results_model.to_file(results_file)
|
|
65
|
+
|
|
66
|
+
puts "✅ Benchmark completed successfully"
|
|
67
|
+
puts " 📊 Results saved to: #{results_file}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -25,7 +25,7 @@ module Serialbench
|
|
|
25
25
|
raise NotImplementedError, 'Subclasses must implement #generate'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def stream_parse(data
|
|
28
|
+
def stream_parse(data)
|
|
29
29
|
# Default implementation falls back to regular parsing
|
|
30
30
|
# Override in subclasses that support streaming
|
|
31
31
|
result = parse(data)
|
|
@@ -27,15 +27,11 @@ module Serialbench
|
|
|
27
27
|
RapidJSON.parse(json_string)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def generate(object,
|
|
30
|
+
def generate(object, _options = {})
|
|
31
31
|
require 'rapidjson'
|
|
32
32
|
RapidJSON.dump(object)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def features
|
|
36
|
-
%w[parsing generation high-performance c-extension]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
35
|
private
|
|
40
36
|
|
|
41
37
|
def require_library(library_name)
|
|
@@ -29,10 +29,15 @@ module Serialbench
|
|
|
29
29
|
xpath: supports_xpath?,
|
|
30
30
|
namespaces: supports_namespaces?,
|
|
31
31
|
validation: supports_validation?,
|
|
32
|
-
streaming: supports_streaming
|
|
32
|
+
streaming: supports_streaming?,
|
|
33
|
+
stax: supports_stax?
|
|
33
34
|
}
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
def supports_stax?
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
36
41
|
def supports_generation?
|
|
37
42
|
true
|
|
38
43
|
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_xml_serializer'
|
|
4
|
+
|
|
5
|
+
module Serialbench
|
|
6
|
+
module Serializers
|
|
7
|
+
module Xml
|
|
8
|
+
class LeptrisSerializer < BaseXmlSerializer
|
|
9
|
+
def name
|
|
10
|
+
'leptris'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse(xml_string)
|
|
14
|
+
require 'leptris'
|
|
15
|
+
Leptris::XML.parse(xml_string)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate(data)
|
|
19
|
+
require 'leptris'
|
|
20
|
+
if data.is_a?(Leptris::XML::Document) || data.is_a?(Leptris::XML::Node)
|
|
21
|
+
data.to_xml(indent: 2)
|
|
22
|
+
else
|
|
23
|
+
build_document_from_data(data).to_xml(indent: 2)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_streaming(xml_string, &block)
|
|
28
|
+
require 'leptris'
|
|
29
|
+
|
|
30
|
+
handler = StreamingHandler.new(&block)
|
|
31
|
+
parser = Leptris::XML::SAX::Parser.new(handler)
|
|
32
|
+
parser.parse_memory(xml_string)
|
|
33
|
+
handler.elements_processed
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supports_streaming?
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def supports_xpath?
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# leptris-ruby >= 1.6 exposes Leptris::XML::Pull, a StAX-style
|
|
45
|
+
# cursor (one event per #next). Declared by gem version so the
|
|
46
|
+
# check never triggers the FFI library load.
|
|
47
|
+
def supports_stax?
|
|
48
|
+
spec = Gem.loaded_specs['leptris'] || Gem::Specification.find_by_name('leptris')
|
|
49
|
+
!spec.nil? && spec.version >= Gem::Version.new('1.6.0')
|
|
50
|
+
rescue Gem::LoadError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def version
|
|
55
|
+
return 'unknown' unless available?
|
|
56
|
+
|
|
57
|
+
require 'leptris'
|
|
58
|
+
Leptris::VERSION
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def library_require_name
|
|
62
|
+
'leptris'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The gem's require succeeds even when the libtaurus shared library is
|
|
66
|
+
# missing -- the FFI load happens lazily at first use. Probe with a
|
|
67
|
+
# real parse so "available" means "actually usable".
|
|
68
|
+
def available?
|
|
69
|
+
return @available if defined?(@available)
|
|
70
|
+
|
|
71
|
+
@available = begin
|
|
72
|
+
require 'leptris'
|
|
73
|
+
Leptris::XML.parse('<probe/>')
|
|
74
|
+
true
|
|
75
|
+
rescue StandardError, LoadError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def build_document_from_data(data, root_name = 'root')
|
|
83
|
+
document = Leptris::XML.parse('<root/>')
|
|
84
|
+
document.root.name = sanitize_element_name(root_name)
|
|
85
|
+
append_data(document.root, data)
|
|
86
|
+
document
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def append_data(element, data)
|
|
90
|
+
case data
|
|
91
|
+
when Hash
|
|
92
|
+
data.each do |key, value|
|
|
93
|
+
child = element.document.create_element(sanitize_element_name(key.to_s))
|
|
94
|
+
element.add_child(child)
|
|
95
|
+
append_data(child, value)
|
|
96
|
+
end
|
|
97
|
+
when Array
|
|
98
|
+
data.each do |item|
|
|
99
|
+
child = element.document.create_element('item')
|
|
100
|
+
element.add_child(child)
|
|
101
|
+
append_data(child, item)
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
element.content = data.to_s
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sanitize_element_name(name)
|
|
109
|
+
sanitized = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
|
|
110
|
+
sanitized = "element_#{sanitized}" if sanitized.empty? || sanitized =~ /\A\d/
|
|
111
|
+
sanitized
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# SAX handler counting elements, mirroring the Nokogiri adapter's
|
|
115
|
+
# StreamingHandler shape (Leptris SAX delivers attrs as [name, value]
|
|
116
|
+
# pairs, same as Nokogiri::XML::SAX). Plain duck-typed class --
|
|
117
|
+
# Leptris::XML::SAX::Parser needs no base class, and subclassing it
|
|
118
|
+
# would trigger the FFI library load at file-load time.
|
|
119
|
+
class StreamingHandler
|
|
120
|
+
attr_reader :elements_processed
|
|
121
|
+
|
|
122
|
+
def initialize(&block)
|
|
123
|
+
@block = block
|
|
124
|
+
@elements_processed = 0
|
|
125
|
+
@element_stack = []
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# No-op callbacks the parser invokes unconditionally (the base
|
|
129
|
+
# Leptris::XML::SAX::Document normally provides these).
|
|
130
|
+
def start_document; end
|
|
131
|
+
def end_document; end
|
|
132
|
+
def xmldecl(_version, _encoding, _standalone); end
|
|
133
|
+
def comment(_string); end
|
|
134
|
+
def processing_instruction(_name, _content); end
|
|
135
|
+
def start_prefix_mapping(_prefix, _uri); end
|
|
136
|
+
def end_prefix_mapping(_prefix); end
|
|
137
|
+
def warning(_string); end
|
|
138
|
+
def error(_message, _line = 0, _column = 0); end
|
|
139
|
+
|
|
140
|
+
def start_element(name, attrs = [])
|
|
141
|
+
@elements_processed += 1
|
|
142
|
+
@element_stack.push({ name: name, attributes: Hash[attrs], children: [], text: '' })
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def end_element(_name)
|
|
146
|
+
element = @element_stack.pop
|
|
147
|
+
if @element_stack.empty?
|
|
148
|
+
@block&.call(element)
|
|
149
|
+
else
|
|
150
|
+
@element_stack.last[:children] << element
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def characters(string)
|
|
155
|
+
return if string.strip.empty?
|
|
156
|
+
|
|
157
|
+
@element_stack.last[:text] += string if @element_stack.any?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def cdata_block(string)
|
|
161
|
+
@element_stack.last[:text] += string if @element_stack.any?
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -38,6 +38,15 @@ module Serialbench
|
|
|
38
38
|
handler.elements_processed
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def supports_xpath?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The binding exposes a pull-parser cursor (XML::Reader)
|
|
46
|
+
def supports_stax?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def supports_streaming?
|
|
42
51
|
true
|
|
43
52
|
end
|
|
@@ -58,26 +67,22 @@ module Serialbench
|
|
|
58
67
|
def build_xml_from_data(data, name = 'root')
|
|
59
68
|
require 'libxml'
|
|
60
69
|
|
|
70
|
+
element = LibXML::XML::Node.new(name.to_s)
|
|
61
71
|
case data
|
|
62
72
|
when Hash
|
|
63
|
-
element = LibXML::XML::Node.new(name.to_s)
|
|
64
73
|
data.each do |key, value|
|
|
65
74
|
child = build_xml_from_data(value, key.to_s)
|
|
66
75
|
element << child
|
|
67
76
|
end
|
|
68
|
-
element
|
|
69
77
|
when Array
|
|
70
|
-
element = LibXML::XML::Node.new(name.to_s)
|
|
71
78
|
data.each_with_index do |item, index|
|
|
72
79
|
child = build_xml_from_data(item, "item_#{index}")
|
|
73
80
|
element << child
|
|
74
81
|
end
|
|
75
|
-
element
|
|
76
82
|
else
|
|
77
|
-
element = LibXML::XML::Node.new(name.to_s)
|
|
78
83
|
element.content = data.to_s
|
|
79
|
-
element
|
|
80
84
|
end
|
|
85
|
+
element
|
|
81
86
|
end
|
|
82
87
|
|
|
83
88
|
# SAX handler for streaming
|
|
@@ -100,10 +105,10 @@ module Serialbench
|
|
|
100
105
|
@element_stack.push(@current_element)
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
def on_end_element(
|
|
108
|
+
def on_end_element(_element)
|
|
104
109
|
element_data = @element_stack.pop
|
|
105
110
|
if @element_stack.empty?
|
|
106
|
-
@block&.call(element_data)
|
|
111
|
+
@block&.call(element_data)
|
|
107
112
|
else
|
|
108
113
|
@element_stack.last[:children] << element_data
|
|
109
114
|
end
|
|
@@ -38,6 +38,15 @@ module Serialbench
|
|
|
38
38
|
handler.elements_processed
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def supports_xpath?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The binding exposes a pull-parser cursor (XML::Reader)
|
|
46
|
+
def supports_stax?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def supports_streaming?
|
|
42
51
|
true
|
|
43
52
|
end
|
|
@@ -109,10 +118,10 @@ module Serialbench
|
|
|
109
118
|
@element_stack.push(@current_element)
|
|
110
119
|
end
|
|
111
120
|
|
|
112
|
-
def end_element(
|
|
121
|
+
def end_element(_name)
|
|
113
122
|
element = @element_stack.pop
|
|
114
123
|
if @element_stack.empty?
|
|
115
|
-
@block&.call(element)
|
|
124
|
+
@block&.call(element)
|
|
116
125
|
else
|
|
117
126
|
@element_stack.last[:children] << element
|
|
118
127
|
end
|
|
@@ -38,6 +38,10 @@ module Serialbench
|
|
|
38
38
|
handler.elements_processed
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def supports_xpath?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
def supports_streaming?
|
|
42
46
|
true
|
|
43
47
|
end
|
|
@@ -58,27 +62,23 @@ module Serialbench
|
|
|
58
62
|
def build_xml_from_data(data, name = 'root')
|
|
59
63
|
require 'oga'
|
|
60
64
|
|
|
65
|
+
element = Oga::XML::Element.new(name: name)
|
|
61
66
|
case data
|
|
62
67
|
when Hash
|
|
63
|
-
element = Oga::XML::Element.new(name: name)
|
|
64
68
|
data.each do |key, value|
|
|
65
69
|
child = build_xml_from_data(value, key.to_s)
|
|
66
70
|
element.children << child
|
|
67
71
|
end
|
|
68
|
-
element
|
|
69
72
|
when Array
|
|
70
|
-
element = Oga::XML::Element.new(name: name)
|
|
71
73
|
data.each_with_index do |item, index|
|
|
72
74
|
child = build_xml_from_data(item, "item_#{index}")
|
|
73
75
|
element.children << child
|
|
74
76
|
end
|
|
75
|
-
element
|
|
76
77
|
else
|
|
77
|
-
element = Oga::XML::Element.new(name: name)
|
|
78
78
|
text_node = Oga::XML::Text.new(text: data.to_s)
|
|
79
79
|
element.children << text_node
|
|
80
|
-
element
|
|
81
80
|
end
|
|
81
|
+
element
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# SAX handler for streaming
|
|
@@ -108,10 +108,10 @@ module Serialbench
|
|
|
108
108
|
@element_stack.last[:text] += text if @element_stack.any?
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
def after_element(
|
|
111
|
+
def after_element(_namespace, _name)
|
|
112
112
|
element = @element_stack.pop
|
|
113
113
|
if @element_stack.empty?
|
|
114
|
-
@block&.call(element)
|
|
114
|
+
@block&.call(element)
|
|
115
115
|
else
|
|
116
116
|
@element_stack.last[:children] << element
|
|
117
117
|
end
|
|
@@ -61,10 +61,10 @@ module Serialbench
|
|
|
61
61
|
@element_stack.push(@current_element)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def end_element(
|
|
64
|
+
def end_element(_name)
|
|
65
65
|
element = @element_stack.pop
|
|
66
66
|
if @element_stack.empty?
|
|
67
|
-
@block&.call(element)
|
|
67
|
+
@block&.call(element)
|
|
68
68
|
else
|
|
69
69
|
@element_stack.last[:children] << element
|
|
70
70
|
end
|