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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +294 -229
  3. data/.github/workflows/rake.yml +11 -0
  4. data/.github/workflows/windows-debug.yml +171 -0
  5. data/.gitignore +32 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +274 -0
  8. data/Gemfile +15 -1
  9. data/README.adoc +41 -0
  10. data/data/schemas/result.yml +29 -0
  11. data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
  12. data/docs/SYCK_YAML_FIX.md +91 -0
  13. data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
  14. data/docs/WINDOWS_LIBXML_FIX.md +136 -0
  15. data/docs/WINDOWS_SETUP.md +122 -0
  16. data/lib/serialbench/benchmark_runner.rb +3 -3
  17. data/lib/serialbench/cli/benchmark_cli.rb +74 -1
  18. data/lib/serialbench/cli/environment_cli.rb +3 -3
  19. data/lib/serialbench/cli/resultset_cli.rb +112 -26
  20. data/lib/serialbench/cli/ruby_build_cli.rb +75 -88
  21. data/lib/serialbench/cli/validate_cli.rb +88 -0
  22. data/lib/serialbench/cli.rb +6 -2
  23. data/lib/serialbench/config_manager.rb +15 -26
  24. data/lib/serialbench/models/benchmark_config.rb +12 -0
  25. data/lib/serialbench/models/benchmark_result.rb +39 -3
  26. data/lib/serialbench/models/environment_config.rb +3 -2
  27. data/lib/serialbench/models/platform.rb +61 -6
  28. data/lib/serialbench/models/result.rb +28 -1
  29. data/lib/serialbench/models/result_set.rb +8 -0
  30. data/lib/serialbench/ruby_build_manager.rb +19 -23
  31. data/lib/serialbench/runners/asdf_runner.rb +1 -1
  32. data/lib/serialbench/runners/docker_runner.rb +2 -4
  33. data/lib/serialbench/runners/local_runner.rb +71 -0
  34. data/lib/serialbench/serializers/base_serializer.rb +1 -1
  35. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -5
  36. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +0 -2
  37. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -1
  38. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -1
  39. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +6 -1
  40. data/lib/serialbench/serializers/xml/leptris_serializer.rb +167 -0
  41. data/lib/serialbench/serializers/xml/libxml_serializer.rb +13 -8
  42. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +11 -2
  43. data/lib/serialbench/serializers/xml/oga_serializer.rb +8 -8
  44. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
  45. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -3
  46. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +1 -3
  47. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -5
  48. data/lib/serialbench/serializers/yaml/syck_serializer.rb +1 -1
  49. data/lib/serialbench/serializers.rb +5 -3
  50. data/lib/serialbench/site_generator.rb +234 -2
  51. data/lib/serialbench/templates/assets/css/format_based.css +1 -53
  52. data/lib/serialbench/templates/assets/css/themes.css +5 -4
  53. data/lib/serialbench/templates/assets/js/chart_helpers.js +44 -14
  54. data/lib/serialbench/templates/assets/js/dashboard.js +14 -15
  55. data/lib/serialbench/templates/format_based.liquid +480 -252
  56. data/lib/serialbench/version.rb +1 -1
  57. data/lib/serialbench/yaml_validator.rb +36 -0
  58. data/serialbench.gemspec +11 -2
  59. data/site/.gitignore +5 -0
  60. data/site/README.md +32 -0
  61. data/site/astro.config.mjs +12 -0
  62. data/site/package-lock.json +6160 -0
  63. data/site/package.json +22 -0
  64. data/site/public/favicon.svg +4 -0
  65. data/site/scripts/gen-sample-data.mjs +154 -0
  66. data/site/src/components/AvailabilityMatrix.astro +61 -0
  67. data/site/src/components/CalibratedLeaderboard.vue +134 -0
  68. data/site/src/components/CitationBox.vue +50 -0
  69. data/site/src/components/DashboardConsole.vue +193 -0
  70. data/site/src/components/FeaturesTable.astro +97 -0
  71. data/site/src/components/MemoryPanel.vue +44 -0
  72. data/site/src/components/OpsChart.vue +135 -0
  73. data/site/src/config.ts +18 -0
  74. data/site/src/layouts/Layout.astro +85 -0
  75. data/site/src/lib/channels.ts +24 -0
  76. data/site/src/lib/dashboard.ts +269 -0
  77. data/site/src/pages/index.astro +44 -0
  78. data/site/src/pages/library/[slug].astro +128 -0
  79. data/site/src/pages/methodology.astro +55 -0
  80. data/site/src/styles/global.css +103 -0
  81. data/site/tsconfig.json +5 -0
  82. metadata +58 -23
  83. data/.github/workflows/ci.yml +0 -74
  84. 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
- from_yaml(IO.read(data_file))
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 'net/http'
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('.') + '.0'
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
- uri = URI(GITHUB_API_URL)
105
-
106
- http = Net::HTTP.new(uri.host, uri.port)
107
- http.use_ssl = true
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
- raise "GitHub API request failed: #{response.code} #{response.message}" unless response.code == '200'
106
+ client = Octokit::Client.new(client_options)
116
107
 
117
- data = JSON.parse(response.body)
108
+ contents = client.contents('rbenv/ruby-build', path: 'share/ruby-build')
118
109
 
119
110
  # Extract definition names from the file list
120
- definitions = data
121
- .select { |item| item['type'] == 'file' }
122
- .map { |item| item['name'] }
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
- ensure_cache_exists!
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
- raise "Failed to load Ruby-Build definitions from cache: #{e.message}"
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, benchmark_config_path, result_dir)
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(benchmark_config, benchmark_config_path, result_dir)
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, &block)
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, options = {})
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)
@@ -59,8 +59,6 @@ module Serialbench
59
59
  raise NotImplementedError, 'Subclasses must implement #library_require_name'
60
60
  end
61
61
 
62
- public
63
-
64
62
  # Check if the TOML library is available
65
63
  def available?
66
64
  return @available if defined?(@available)
@@ -27,7 +27,7 @@ module Serialbench
27
27
  TomlRB.parse(toml_string)
28
28
  end
29
29
 
30
- def generate(object, options = {})
30
+ def generate(object, _options = {})
31
31
  require 'toml-rb'
32
32
  TomlRB.dump(object)
33
33
  end
@@ -24,7 +24,7 @@ module Serialbench
24
24
  # TOML doesn't typically support streaming parsing
25
25
  # Parse the entire document and yield it
26
26
  result = parse(toml_string)
27
- block&.call(result) if block
27
+ block&.call(result)
28
28
  1 # Return 1 document processed
29
29
  end
30
30
 
@@ -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(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) if @block
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(name)
121
+ def end_element(_name)
113
122
  element = @element_stack.pop
114
123
  if @element_stack.empty?
115
- @block&.call(element) if @block
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(namespace, name)
111
+ def after_element(_namespace, _name)
112
112
  element = @element_stack.pop
113
113
  if @element_stack.empty?
114
- @block&.call(element) if @block
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(name)
64
+ def end_element(_name)
65
65
  element = @element_stack.pop
66
66
  if @element_stack.empty?
67
- @block&.call(element) if @block
67
+ @block&.call(element)
68
68
  else
69
69
  @element_stack.last[:children] << element
70
70
  end