serialbench 0.1.0 → 0.1.2

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +181 -30
  3. data/.github/workflows/ci.yml +3 -3
  4. data/.github/workflows/docker.yml +272 -0
  5. data/.github/workflows/rake.yml +15 -0
  6. data/.github/workflows/release.yml +25 -0
  7. data/Gemfile +6 -30
  8. data/README.adoc +381 -415
  9. data/Rakefile +0 -55
  10. data/config/benchmarks/full.yml +29 -0
  11. data/config/benchmarks/short.yml +26 -0
  12. data/config/environments/asdf-ruby-3.2.yml +8 -0
  13. data/config/environments/asdf-ruby-3.3.yml +8 -0
  14. data/config/environments/docker-ruby-3.0.yml +9 -0
  15. data/config/environments/docker-ruby-3.1.yml +9 -0
  16. data/config/environments/docker-ruby-3.2.yml +9 -0
  17. data/config/environments/docker-ruby-3.3.yml +9 -0
  18. data/config/environments/docker-ruby-3.4.yml +9 -0
  19. data/docker/Dockerfile.alpine +33 -0
  20. data/docker/Dockerfile.ubuntu +32 -0
  21. data/docker/README.md +214 -0
  22. data/exe/serialbench +1 -1
  23. data/lib/serialbench/benchmark_runner.rb +270 -350
  24. data/lib/serialbench/cli/base_cli.rb +51 -0
  25. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  26. data/lib/serialbench/cli/environment_cli.rb +181 -0
  27. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  28. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  29. data/lib/serialbench/cli.rb +59 -410
  30. data/lib/serialbench/config_manager.rb +140 -0
  31. data/lib/serialbench/models/benchmark_config.rb +63 -0
  32. data/lib/serialbench/models/benchmark_result.rb +45 -0
  33. data/lib/serialbench/models/environment_config.rb +71 -0
  34. data/lib/serialbench/models/platform.rb +59 -0
  35. data/lib/serialbench/models/result.rb +53 -0
  36. data/lib/serialbench/models/result_set.rb +71 -0
  37. data/lib/serialbench/models/result_store.rb +108 -0
  38. data/lib/serialbench/models.rb +54 -0
  39. data/lib/serialbench/ruby_build_manager.rb +153 -0
  40. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  41. data/lib/serialbench/runners/base.rb +32 -0
  42. data/lib/serialbench/runners/docker_runner.rb +142 -0
  43. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  44. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  45. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  47. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +50 -0
  48. data/lib/serialbench/serializers/json/yajl_serializer.rb +6 -4
  49. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  50. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  51. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  53. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  54. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +21 -5
  56. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  57. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  58. data/lib/serialbench/serializers/xml/rexml_serializer.rb +32 -4
  59. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +59 -0
  60. data/lib/serialbench/serializers/yaml/psych_serializer.rb +54 -0
  61. data/lib/serialbench/serializers/yaml/syck_serializer.rb +102 -0
  62. data/lib/serialbench/serializers.rb +34 -6
  63. data/lib/serialbench/site_generator.rb +105 -0
  64. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  65. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  66. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  67. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  68. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  69. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  70. data/lib/serialbench/templates/base.liquid +49 -0
  71. data/lib/serialbench/templates/format_based.liquid +279 -0
  72. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  73. data/lib/serialbench/version.rb +1 -1
  74. data/lib/serialbench.rb +2 -31
  75. data/serialbench.gemspec +28 -17
  76. metadata +192 -55
  77. data/lib/serialbench/chart_generator.rb +0 -821
  78. data/lib/serialbench/result_formatter.rb +0 -182
  79. data/lib/serialbench/result_merger.rb +0 -1201
  80. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  81. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  82. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  83. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  84. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  85. data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -0,0 +1,63 @@
1
+ require 'lutaml/model'
2
+
3
+ module Serialbench
4
+ module Models
5
+ # Configuration for comprehensive benchmarks - Full testing with all data sizes
6
+ # Used by Docker script for complete performance analysis
7
+
8
+ # data_sizes:
9
+ # - small
10
+ # - medium
11
+ # - large
12
+
13
+ # formats:
14
+ # - xml
15
+ # - json
16
+ # - yaml
17
+ # - toml
18
+
19
+ # iterations:
20
+ # small: 20
21
+ # medium: 5
22
+ # large: 2
23
+
24
+ # # Enable memory profiling for comprehensive analysis
25
+ # memory_profiling: true
26
+
27
+ # # Standard warmup iterations
28
+ # warmup_iterations: 3
29
+
30
+ # # Enable streaming benchmarks where supported
31
+ # streaming_benchmarks: true
32
+
33
+ class BenchmarkIteration < Lutaml::Model::Serializable
34
+ attribute :small, :integer, default: -> { 20 }
35
+ attribute :medium, :integer, default: -> { 5 }
36
+ attribute :large, :integer, default: -> { 2 }
37
+
38
+ key_value do
39
+ map 'small', to: :small
40
+ map 'medium', to: :medium
41
+ map 'large', to: :large
42
+ end
43
+ end
44
+
45
+ class BenchmarkConfig < Lutaml::Model::Serializable
46
+ attribute :name, :string
47
+ attribute :description, :string
48
+ attribute :data_sizes, :string, collection: true, values: %w[small medium large]
49
+ attribute :formats, :string, collection: true, values: %w[xml json yaml toml]
50
+ attribute :iterations, BenchmarkIteration
51
+ attribute :warmup, :integer, default: -> { 1 }
52
+ attribute :operations, :string, collection: true, values: %w[parse generate memory streaming]
53
+
54
+ def to_file(file_path)
55
+ File.write(file_path, to_yaml)
56
+ end
57
+
58
+ def self.from_file(file_path)
59
+ from_yaml(IO.read(file_path))
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Serialbench
6
+ module Models
7
+ class SerializerInformation < Lutaml::Model::Serializable
8
+ attribute :format, :string, values: %w[xml json yaml toml]
9
+ attribute :name, :string
10
+ attribute :version, :string
11
+ end
12
+
13
+ class SerializerInformationCollection < Lutaml::Model::Collection
14
+ instances :items, SerializerInformation
15
+ end
16
+
17
+ class AdapterPerformance < Lutaml::Model::Serializable
18
+ attribute :adapter, :string
19
+ attribute :format, :string, values: %w[xml json yaml toml]
20
+ attribute :data_size, :string, values: %w[small medium large]
21
+ end
22
+
23
+ class IterationPerformance < AdapterPerformance
24
+ attribute :time_per_iterations, :float
25
+ attribute :time_per_iteration, :float
26
+ attribute :iterations_per_second, :float
27
+ attribute :iterations_count, :integer
28
+ end
29
+
30
+ class MemoryPerformance < AdapterPerformance
31
+ attribute :total_allocated, :integer
32
+ attribute :total_retained, :integer
33
+ attribute :allocated_memory, :integer
34
+ attribute :retained_memory, :integer
35
+ end
36
+
37
+ class BenchmarkResult < Lutaml::Model::Serializable
38
+ attribute :serializers, SerializerInformation, collection: true
39
+ attribute :parsing, IterationPerformance, collection: true
40
+ attribute :generation, IterationPerformance, collection: true
41
+ attribute :memory, MemoryPerformance, collection: true
42
+ attribute :streaming, IterationPerformance, collection: true
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ require 'lutaml/model'
2
+ require_relative '../ruby_build_manager'
3
+
4
+ module Serialbench
5
+ module Models
6
+ # ---
7
+ # name: docker-ruby-3.2
8
+ # kind: docker
9
+ # created_at: '2025-06-13T15:18:43+08:00'
10
+ # ruby_build_tag: "3.2.4"
11
+ # description: Docker environment for Ruby 3.2 benchmarks
12
+ # docker:
13
+ # image: 'ruby:3.2-slim'
14
+ # dockerfile: '../../docker/Dockerfile.ubuntu'
15
+
16
+ class DockerEnvConfig < Lutaml::Model::Serializable
17
+ attribute :image, :string
18
+ attribute :dockerfile, :string
19
+
20
+ key_value do
21
+ map 'image', to: :image
22
+ map 'dockerfile', to: :dockerfile
23
+ end
24
+ end
25
+
26
+ # ---
27
+ # name: ruby-324-asdf
28
+ # kind: asdf
29
+ # created_at: '2025-06-12T22:54:43+08:00'
30
+ # ruby_build_tag: 3.2.4
31
+ # description: ASDF environment
32
+ # asdf:
33
+ # auto_install: true
34
+ class AsdfEnvConfig < Lutaml::Model::Serializable
35
+ attribute :auto_install, :boolean, default: -> { true }
36
+
37
+ key_value do
38
+ map 'auto_install', to: :auto_install
39
+ end
40
+ end
41
+
42
+ class EnvironmentConfig < Lutaml::Model::Serializable
43
+ attribute :name, :string
44
+ attribute :kind, :string
45
+ attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
46
+ attribute :ruby_build_tag, :string, values:
47
+ RubyBuildManager.list_definitions
48
+ attribute :description, :string
49
+ attribute :docker, DockerEnvConfig
50
+ attribute :asdf, AsdfEnvConfig
51
+
52
+ key_value do
53
+ map 'name', to: :name
54
+ map 'description', to: :description
55
+ map 'kind', to: :kind
56
+ map 'created_at', to: :created_at
57
+ map 'ruby_build_tag', to: :ruby_build_tag
58
+ map 'docker', to: :docker
59
+ map 'asdf', to: :asdf
60
+ end
61
+
62
+ def to_file(file_path)
63
+ File.write(file_path, to_yaml)
64
+ end
65
+
66
+ def self.from_file(file_path)
67
+ from_yaml(IO.read(file_path))
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative '../ruby_build_manager'
5
+
6
+ module Serialbench
7
+ module Models
8
+ # platform:
9
+ # platform_string: docker-ruby-3.0
10
+ # kind: docker
11
+ # os: linux
12
+ # arch: arm64
13
+ # ruby_build_tag: 3.0.7
14
+
15
+ class Platform < Lutaml::Model::Serializable
16
+ attribute :platform_string, :string
17
+ attribute :kind, :string, default: -> { 'local' }
18
+ attribute :os, :string, default: -> { detect_os }
19
+ attribute :arch, :string, default: -> { detect_arch }
20
+ attribute :ruby_version, :string, default: -> { RUBY_VERSION }
21
+ attribute :ruby_platform, :string, default: -> { RUBY_PLATFORM }
22
+ attribute :ruby_build_tag, :string, values: RubyBuildManager.list_definitions
23
+
24
+ def self.current_local
25
+ new(
26
+ platform_string: "local-#{RUBY_VERSION}",
27
+ kind: 'local',
28
+ ruby_build_tag: RUBY_VERSION
29
+ )
30
+ end
31
+
32
+ def self.detect_os
33
+ case RbConfig::CONFIG['host_os']
34
+ when /darwin/i
35
+ 'macos'
36
+ when /linux/i
37
+ 'linux'
38
+ when /mswin|mingw|cygwin/i
39
+ 'windows'
40
+ else
41
+ 'unknown'
42
+ end
43
+ end
44
+
45
+ def self.detect_arch
46
+ case RbConfig::CONFIG['host_cpu']
47
+ when /x86_64|amd64/i
48
+ 'x86_64'
49
+ when /aarch64|arm64/i
50
+ 'arm64'
51
+ when /arm/i
52
+ 'arm'
53
+ else
54
+ 'unknown'
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'fileutils'
5
+ require_relative 'platform'
6
+ require_relative 'benchmark_result'
7
+ require_relative 'benchmark_config'
8
+ require_relative 'environment_config'
9
+
10
+ module Serialbench
11
+ module Models
12
+ class RunMetadata < Lutaml::Model::Serializable
13
+ attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
14
+ attribute :benchmark_config_path, :string
15
+ attribute :environment_config_path, :string
16
+ attribute :tags, :string, collection: true
17
+ end
18
+
19
+ class Result < Lutaml::Model::Serializable
20
+ attribute :platform, Platform
21
+ attribute :metadata, RunMetadata
22
+ attribute :environment_config, EnvironmentConfig
23
+ attribute :benchmark_config, BenchmarkConfig
24
+ attribute :benchmark_result, BenchmarkResult
25
+
26
+ def self.load(path)
27
+ raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
28
+
29
+ # Load benchmark data
30
+ data_file = File.join(path, 'results.yaml')
31
+
32
+ raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
33
+
34
+ from_yaml(IO.read(data_file))
35
+ end
36
+
37
+ def self.find_all(base_path = 'results/runs')
38
+ return [] unless Dir.exist?(base_path)
39
+
40
+ Dir.glob(File.join(base_path, '*')).select { |path| Dir.exist?(path) }.map do |path|
41
+ load(path)
42
+ rescue StandardError => e
43
+ warn "Failed to load run result from #{path}: #{e.message}"
44
+ nil
45
+ end.compact
46
+ end
47
+
48
+ def to_file(path)
49
+ File.write(path, to_yaml)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'time'
5
+ require 'fileutils'
6
+
7
+ module Serialbench
8
+ module Models
9
+ # ResultSet model for managing collections of benchmark results
10
+ class ResultSet < Lutaml::Model::Serializable
11
+ attribute :name, :string
12
+ attribute :description, :string
13
+ attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
14
+ attribute :updated_at, :string, default: -> { Time.now.utc.iso8601 }
15
+ attribute :results, Result, collection: true, initialize_empty: true
16
+
17
+ def self.load(path)
18
+ raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
19
+
20
+ # Load benchmark data
21
+ data_file = File.join(path, 'resultset.yaml')
22
+
23
+ raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
24
+
25
+ from_yaml(IO.read(data_file))
26
+ end
27
+
28
+ def to_file(path)
29
+ File.write(path, to_yaml)
30
+ end
31
+
32
+ def save(dir)
33
+ FileUtils.mkdir_p(dir)
34
+ to_file(File.join(dir, 'resultset.yaml'))
35
+ end
36
+
37
+ def add_result(result_path)
38
+ # Assume result_path is the directory containing benchmark results and is named
39
+ # accordingly
40
+ result_name = File.basename(result_path)
41
+ raise ArgumentError, 'Result name cannot be empty' if result_name.empty?
42
+ # Validate that the result path is a directory
43
+ raise ArgumentError, 'Result path must be a directory' unless File.directory?(result_path)
44
+
45
+ result_file_path = File.join(result_path, 'results.yaml')
46
+ raise ArgumentError, "No results data found in #{result_path}" unless File.exist?(result_file_path)
47
+
48
+ result = Result.load(result_path)
49
+
50
+ # Check if result already exists:
51
+ # If environment_config.created_at is identical;
52
+ # If platform.platform_string is identical;
53
+ # If benchmark_config.benchmark_name is identical;
54
+
55
+ duplicates = results.select do |r|
56
+ r.platform.platform_string == result.platform.platform_string &&
57
+ r.environment_config.created_at == result.environment_config.created_at &&
58
+ r.benchmark_config.benchmark_name == result.benchmark_config.benchmark_name
59
+ end
60
+
61
+ raise ArgumentError, 'Result is already present in this resultset' if duplicates.any?
62
+
63
+ # Add the result to the resultset
64
+ results << result
65
+ self.updated_at = Time.now.utc.iso8601
66
+
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'result'
5
+
6
+ module Serialbench
7
+ module Models
8
+ class ResultStore
9
+ DEFAULT_BASE_PATH = 'results'
10
+ RUNS_PATH = 'runs'
11
+ SETS_PATH = 'sets'
12
+
13
+ attr_reader :base_path
14
+
15
+ def initialize(base_path = DEFAULT_BASE_PATH)
16
+ @base_path = base_path
17
+ ensure_results_directory
18
+ end
19
+
20
+ def self.default
21
+ @default ||= new
22
+ end
23
+
24
+ # Run management
25
+ def runs_path
26
+ File.join(@base_path, RUNS_PATH)
27
+ end
28
+
29
+ def find_runs(tags: nil, limit: nil)
30
+ runs = Result.find_all(runs_path)
31
+
32
+ runs = runs.select { |run| (Array(tags) - run.tags).empty? } if tags
33
+
34
+ limit ? runs.first(limit) : runs
35
+ end
36
+
37
+ # Run set management
38
+ def sets_path
39
+ File.join(@base_path, SETS_PATH)
40
+ end
41
+
42
+ def find_resultsets(tags: nil, limit: nil)
43
+ resultsets = ResultSet.find_all(sets_path)
44
+
45
+ resultsets = resultsets.select { |resultset| (Array(tags) - resultset.tags).empty? } if tags
46
+
47
+ limit ? resultsets.first(limit) : resultsets
48
+ end
49
+
50
+ # Convenience methods
51
+ def create_resultset(name, run_platform_strings, metadata: {})
52
+ run_paths = run_platform_strings.map { |ps| File.join(runs_path, ps) }
53
+ resultset = ResultSet.create(name, run_paths, metadata: metadata)
54
+ save_resultset(resultset)
55
+ resultset
56
+ end
57
+
58
+ # Validation
59
+ def validate_structure
60
+ errors = []
61
+
62
+ # Check base structure
63
+ errors << "Base path does not exist: #{@base_path}" unless Dir.exist?(@base_path)
64
+ errors << "Runs directory does not exist: #{runs_path}" unless Dir.exist?(runs_path)
65
+ errors << "Sets directory does not exist: #{sets_path}" unless Dir.exist?(sets_path)
66
+
67
+ # Validate individual runs
68
+ if Dir.exist?(runs_path)
69
+ Dir.glob(File.join(runs_path, '*')).each do |run_path|
70
+ next unless Dir.exist?(run_path)
71
+
72
+ begin
73
+ run = Result.load(run_path)
74
+ run.validate!
75
+ rescue StandardError => e
76
+ errors << "Invalid result at #{run_path}: #{e.message}"
77
+ end
78
+ end
79
+ end
80
+
81
+ # Validate result sets
82
+ if Dir.exist?(sets_path)
83
+ Dir.glob(File.join(sets_path, '*')).each do |set_path|
84
+ next unless Dir.exist?(set_path)
85
+
86
+ begin
87
+ resultset = ResultSet.load(set_path)
88
+ resultset.validate!
89
+ rescue StandardError => e
90
+ errors << "Invalid result set at #{set_path}: #{e.message}"
91
+ end
92
+ end
93
+ end
94
+
95
+ errors
96
+ end
97
+
98
+ def valid?
99
+ validate_structure.empty?
100
+ end
101
+
102
+ def ensure_results_directory
103
+ FileUtils.mkdir_p(runs_path) unless Dir.exist?(runs_path)
104
+ FileUtils.mkdir_p(sets_path) unless Dir.exist?(sets_path)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'models/benchmark_result'
4
+ require_relative 'models/platform'
5
+ require_relative 'models/result'
6
+ require_relative 'models/result_store'
7
+ require_relative 'models/benchmark_config'
8
+ require_relative 'models/environment_config'
9
+ require_relative 'models/result_set'
10
+
11
+ module Serialbench
12
+ module Models
13
+ # Factory method to create appropriate model based on data structure
14
+ def self.from_file(file_path)
15
+ case File.extname(file_path).downcase
16
+ when '.yaml', '.yml'
17
+ data = YAML.load_file(file_path)
18
+ when '.json'
19
+ data = JSON.parse(File.read(file_path))
20
+ else
21
+ raise ArgumentError, "Unsupported file format: #{File.extname(file_path)}"
22
+ end
23
+
24
+ from_data(data)
25
+ end
26
+
27
+ def self.from_data(data)
28
+ BenchmarkResult.new(data)
29
+ end
30
+
31
+ # Convert any benchmark result to YAML format
32
+ def self.to_yaml_file(result, file_path)
33
+ result.to_yaml_file(file_path)
34
+ end
35
+
36
+ # Convert any benchmark result to JSON format (for HTML templates)
37
+ def self.to_json_file(result, file_path)
38
+ result.to_json_file(file_path)
39
+ end
40
+
41
+ # Convenience methods for the new OO architecture
42
+ def self.result_store
43
+ ResultStore.default
44
+ end
45
+
46
+ def self.create_run(platform_string, benchmark_data, metadata: {})
47
+ Result.create(platform_string, benchmark_data, metadata: metadata)
48
+ end
49
+
50
+ def self.create_resultset(name, run_paths_or_objects, metadata: {})
51
+ ResultSet.create(name, run_paths_or_objects, metadata: metadata)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+
8
+ module Serialbench
9
+ # Manages Ruby-Build definitions from the official ruby-build repository
10
+ class RubyBuildManager
11
+ GITHUB_API_URL = 'https://api.github.com/repos/rbenv/ruby-build/contents/share/ruby-build'
12
+ CACHE_DIR = File.expand_path('~/.serialbench')
13
+ CACHE_FILE = File.join(CACHE_DIR, 'ruby-build-definitions.yaml')
14
+
15
+ class << self
16
+ def update_definitions
17
+ puts '🔄 Fetching Ruby-Build definitions from GitHub...'
18
+
19
+ definitions = fetch_definitions_from_github
20
+ save_definitions_to_cache(definitions)
21
+
22
+ puts "✅ Updated #{definitions.length} Ruby-Build definitions"
23
+ puts "📁 Cache location: #{CACHE_FILE}"
24
+
25
+ definitions
26
+ rescue StandardError => e
27
+ raise "Failed to update Ruby-Build definitions: #{e.message}"
28
+ end
29
+
30
+ def list_definitions
31
+ load_definitions_from_cache
32
+ end
33
+
34
+ def show_definition(tag)
35
+ definitions = load_definitions_from_cache
36
+
37
+ unless definitions.include?(tag)
38
+ raise "Ruby-Build definition '#{tag}' not found. Available definitions: #{definitions.length}"
39
+ end
40
+
41
+ {
42
+ tag: tag,
43
+ available: true,
44
+ source: 'ruby-build',
45
+ cache_file: CACHE_FILE
46
+ }
47
+ end
48
+
49
+ def validate_tag(tag)
50
+ puts "🔍 Validating Ruby-Build tag: #{tag} against #{CACHE_FILE}"
51
+ return false if tag.nil? || tag.strip.empty?
52
+
53
+ definitions = load_definitions_from_cache
54
+ definitions.include?(tag)
55
+ rescue StandardError
56
+ false
57
+ end
58
+
59
+ def suggest_current_ruby_tag
60
+ ruby_version = RUBY_VERSION
61
+
62
+ # Try exact match first
63
+ return ruby_version if validate_tag(ruby_version)
64
+
65
+ # Try common variations
66
+ variations = [
67
+ ruby_version,
68
+ "#{ruby_version}.0",
69
+ ruby_version.split('.')[0..1].join('.') + '.0'
70
+ ]
71
+
72
+ variations.each do |variation|
73
+ return variation if validate_tag(variation)
74
+ end
75
+
76
+ # Return the Ruby version even if not found, user can adjust
77
+ ruby_version
78
+ end
79
+
80
+ def cache_exists?
81
+ File.exist?(CACHE_FILE)
82
+ end
83
+
84
+ def cache_age
85
+ return nil unless cache_exists?
86
+
87
+ Time.now - File.mtime(CACHE_FILE)
88
+ end
89
+
90
+ def ensure_cache_exists!
91
+ return if cache_exists?
92
+
93
+ raise <<~ERROR
94
+ Ruby-Build definitions cache not found.
95
+
96
+ Update the cache first:
97
+ serialbench ruby-build update
98
+ ERROR
99
+ end
100
+
101
+ private
102
+
103
+ 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)
114
+
115
+ raise "GitHub API request failed: #{response.code} #{response.message}" unless response.code == '200'
116
+
117
+ data = JSON.parse(response.body)
118
+
119
+ # Extract definition names from the file list
120
+ definitions = data
121
+ .select { |item| item['type'] == 'file' }
122
+ .map { |item| item['name'] }
123
+ .sort
124
+
125
+ raise 'No Ruby-Build definitions found in GitHub response' if definitions.empty?
126
+
127
+ definitions
128
+ end
129
+
130
+ def save_definitions_to_cache(definitions)
131
+ FileUtils.mkdir_p(CACHE_DIR)
132
+
133
+ cache_data = {
134
+ 'updated_at' => Time.now.utc.iso8601,
135
+ 'source' => GITHUB_API_URL,
136
+ 'count' => definitions.length,
137
+ 'definitions' => definitions
138
+ }
139
+
140
+ File.write(CACHE_FILE, cache_data.to_yaml)
141
+ end
142
+
143
+ def load_definitions_from_cache
144
+ ensure_cache_exists!
145
+
146
+ cache_data = YAML.load_file(CACHE_FILE)
147
+ cache_data['definitions'] || []
148
+ rescue StandardError => e
149
+ raise "Failed to load Ruby-Build definitions from cache: #{e.message}"
150
+ end
151
+ end
152
+ end
153
+ end