hx_ruby 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ed966ca9ef6f53c6fc8eb54f403e685ccce968c18857095d48bc46c0fb8eda14
4
+ data.tar.gz: f7cc0f292ffba4285a9612fd2d8e14d7dd78290ce18998475306b20874a5af57
5
+ SHA512:
6
+ metadata.gz: 4418e948a6106c092c8d40ac27bac2b427e4835f677ac4593825f2ef8aa091cef139a71d325c977617540553b984adff1f98c67ffecc7d6672569dab1b7bd2f5
7
+ data.tar.gz: fae787afd40a83fd3f7e35342148d84ca3426807767f2059e7b2495fe913ec24b48021893f2a4bf55949e3eb9750c86e2c125773bcac6adecf307b1cf1fe39f4
@@ -0,0 +1,41 @@
1
+ # Every value is written out, and nothing here is inherited from the workspace.
2
+ #
3
+ # This is the one manifest in the repository that leaves it: `gem build` copies
4
+ # it verbatim into the gem, and it is compiled on a machine where there is no
5
+ # workspace to inherit from. `edition.workspace = true` there is not a fallback,
6
+ # it is an error - "failed to find a workspace root" - and the gem will not
7
+ # install. CI checks that nothing in this file says `workspace`.
8
+ #
9
+ # The cost is that the version is written in two places. `.github/workflows/ci.yml`
10
+ # fails if it stops matching the workspace's.
11
+ [package]
12
+ name = "hx-ruby"
13
+ description = "Ruby bindings over hx-catalog's preset inspector, so the tone browser reads .hlx files with the same code the desktop uses"
14
+ version = "0.3.0"
15
+ edition = "2021"
16
+ license = "MIT"
17
+ repository = "https://github.com/crmne/tonepush"
18
+ rust-version = "1.87"
19
+
20
+ [lib]
21
+ # The name Ruby dlopen's and the `Init_hx_ruby` entry it calls; keep it in step
22
+ # with the gem's `require "hx_ruby/hx_ruby"`.
23
+ name = "hx_ruby"
24
+ crate-type = ["cdylib"]
25
+
26
+ [dependencies]
27
+ # The whole point: parse presets with hx-catalog's inspector, never a Ruby
28
+ # reimplementation. hx-proto is the preset model those facts ultimately describe
29
+ # and is kept as a direct dependency so this binding can surface its types
30
+ # without a manifest change.
31
+ #
32
+ # Named by version and not by path, unlike every other crate here, because this
33
+ # manifest is the one that leaves the repository: `gem build` copies it verbatim
34
+ # into the gem, and it is then compiled on a machine that has no workspace and
35
+ # no sibling crates. A path would point at nothing there. The workspace root
36
+ # patches both back to the local sources, so building here still compiles what
37
+ # is checked out rather than what was last published.
38
+ hx-catalog = "0.3.0"
39
+ hx-proto = "0.3.0"
40
+ magnus = "0.8"
41
+ serde_json = "1"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ # Builds the Rust cdylib beside this file and installs it as
7
+ # lib/hx_ruby/hx_ruby.so, which lib/hx_ruby.rb then requires.
8
+ create_rust_makefile("hx_ruby/hx_ruby")
@@ -0,0 +1,129 @@
1
+ //! Ruby bindings over `hx_catalog`'s preset inspector.
2
+ //!
3
+ //! The tone browser is a Rails app, but it must not grow a second, drifting
4
+ //! copy of the `.hlx` reader in Ruby. So parsing stays here: this crate hands a
5
+ //! preset's JSON to [`hx_catalog::inspect`] - the very reader the desktop uses -
6
+ //! and returns the facts as a plain Ruby Hash. The Rails side is then a thin
7
+ //! value object over those facts, never a parser.
8
+
9
+ use hx_catalog::{Catalog, ChainContent, OutputTarget, Tone};
10
+ use magnus::{function, prelude::*, Error, ExceptionClass, RHash, Ruby};
11
+
12
+ /// `HxRuby.inspect_hlx(json)` -> a Hash of the tone facts a browser sorts by.
13
+ ///
14
+ /// Raises `HxRuby::CatalogNotInstalled` when the HX Edit resources are missing,
15
+ /// `HxRuby::Error` for any other catalog read failure, and `ArgumentError` when
16
+ /// the string is not valid preset JSON. It resolves both DSPs and every block,
17
+ /// and by construction never panics.
18
+ fn inspect_hlx(ruby: &Ruby, json: String) -> Result<RHash, Error> {
19
+ let value: serde_json::Value = serde_json::from_str(&json).map_err(|e| {
20
+ Error::new(
21
+ ruby.exception_arg_error(),
22
+ format!("not a valid .hlx preset: {e}"),
23
+ )
24
+ })?;
25
+
26
+ let catalog = Catalog::load().map_err(|e| load_error(ruby, &e))?;
27
+ let tone = hx_catalog::inspect(&value, &catalog);
28
+ tone_to_hash(ruby, &tone)
29
+ }
30
+
31
+ /// Turn a catalog load failure into the right Ruby exception. "Not installed"
32
+ /// is its own class so the Rails app can rescue the one recoverable case - the
33
+ /// resources are absent - apart from a genuine read or parse fault.
34
+ fn load_error(ruby: &Ruby, error: &hx_catalog::Error) -> Error {
35
+ let class = match error {
36
+ hx_catalog::Error::NotInstalled(_) => hx_error_class(ruby, "CatalogNotInstalled"),
37
+ _ => hx_error_class(ruby, "Error"),
38
+ };
39
+ Error::new(class, error.to_string())
40
+ }
41
+
42
+ /// Re-resolve one of the crate's exception classes, defined in [`init`]. Falls
43
+ /// back to `RuntimeError` if the module is somehow gone, so a lookup miss still
44
+ /// raises rather than panics.
45
+ fn hx_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
46
+ ruby.define_module("HxRuby")
47
+ .and_then(|module| module.const_get(name))
48
+ .unwrap_or_else(|_| ruby.exception_runtime_error())
49
+ }
50
+
51
+ fn tone_to_hash(ruby: &Ruby, tone: &Tone) -> Result<RHash, Error> {
52
+ let hash = ruby.hash_new();
53
+ hash.aset(ruby.to_symbol("name"), tone.name.as_str())?;
54
+
55
+ let blocks = ruby.ary_new();
56
+ for block in &tone.blocks {
57
+ let entry = ruby.hash_new();
58
+ entry.aset(ruby.to_symbol("path"), block.path)?;
59
+ entry.aset(ruby.to_symbol("position"), block.position)?;
60
+ entry.aset(ruby.to_symbol("model_number"), block.model_number)?;
61
+ entry.aset(ruby.to_symbol("model_name"), block.model_name.as_str())?;
62
+ // nil when the model sits in no browse category, which is rare.
63
+ entry.aset(ruby.to_symbol("category"), block.category)?;
64
+ entry.aset(ruby.to_symbol("enabled"), block.enabled)?;
65
+
66
+ let params = ruby.hash_new();
67
+ for (name, value) in &block.params {
68
+ params.aset(name.as_str(), *value)?;
69
+ }
70
+ entry.aset(ruby.to_symbol("params"), params)?;
71
+
72
+ blocks.push(entry)?;
73
+ }
74
+ hash.aset(ruby.to_symbol("blocks"), blocks)?;
75
+
76
+ let models_used = ruby.ary_new();
77
+ for number in &tone.models_used {
78
+ models_used.push(*number)?;
79
+ }
80
+ hash.aset(ruby.to_symbol("models_used"), models_used)?;
81
+
82
+ hash.aset(ruby.to_symbol("has_amp"), tone.has_amp)?;
83
+ hash.aset(ruby.to_symbol("has_cab_or_ir"), tone.has_cab_or_ir)?;
84
+ hash.aset(
85
+ ruby.to_symbol("chain_content"),
86
+ chain_content_name(tone.chain_content),
87
+ )?;
88
+ hash.aset(
89
+ ruby.to_symbol("output_target"),
90
+ output_target_name(tone.output_target_guess),
91
+ )?;
92
+
93
+ let skipped = ruby.ary_new();
94
+ for note in &tone.skipped {
95
+ skipped.push(note.as_str())?;
96
+ }
97
+ hash.aset(ruby.to_symbol("skipped"), skipped)?;
98
+
99
+ Ok(hash)
100
+ }
101
+
102
+ /// Snake-case names that line up with the Rails `Tone` enums, so the value
103
+ /// object can hand them straight to Active Record.
104
+ fn chain_content_name(content: ChainContent) -> &'static str {
105
+ match content {
106
+ ChainContent::FullRig => "full_rig",
107
+ ChainContent::AmpAndCab => "amp_and_cab",
108
+ ChainContent::AmpOnly => "amp_only",
109
+ ChainContent::EffectsOnly => "effects_only",
110
+ }
111
+ }
112
+
113
+ /// The inspector only knows whether the tone carries its own speaker, so it
114
+ /// offers two honest guesses rather than the browser's full output vocabulary.
115
+ fn output_target_name(target: OutputTarget) -> &'static str {
116
+ match target {
117
+ OutputTarget::FrfrPa => "frfr_pa",
118
+ OutputTarget::GuitarCabOrDi => "guitar_cab_or_di",
119
+ }
120
+ }
121
+
122
+ #[magnus::init]
123
+ fn init(ruby: &Ruby) -> Result<(), Error> {
124
+ let module = ruby.define_module("HxRuby")?;
125
+ let base = module.define_error("Error", ruby.exception_standard_error())?;
126
+ module.define_error("CatalogNotInstalled", base)?;
127
+ module.define_singleton_method("inspect_hlx", function!(inspect_hlx, 1))?;
128
+ Ok(())
129
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HxRuby
4
+ # Kept in step with the Rust workspace version so the gem and the crate it
5
+ # wraps move together.
6
+ VERSION = "0.3.0"
7
+ end
data/lib/hx_ruby.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hx_ruby/version"
4
+
5
+ # Load the compiled Rust extension. rake-compiler installs it under a Ruby
6
+ # version directory for gems that ship several; a plain extconf build (what
7
+ # Bundler runs for a path gem) drops it straight in lib/hx_ruby. Try the
8
+ # versioned path first, then fall back.
9
+ begin
10
+ ruby_version = RUBY_VERSION[/\d+\.\d+/]
11
+ require_relative "hx_ruby/#{ruby_version}/hx_ruby"
12
+ rescue LoadError
13
+ begin
14
+ require_relative "hx_ruby/hx_ruby"
15
+ rescue LoadError => e
16
+ # Bundler does not compile a path gem's extension on install, so a fresh
17
+ # checkout has to build it once.
18
+ raise LoadError, "#{e.message}\n\nThe hx_ruby native extension is not " \
19
+ "built yet. From the gem directory run: bundle exec rake compile"
20
+ end
21
+ end
22
+
23
+ # Ruby bindings over hx-catalog's `.hlx` inspector.
24
+ #
25
+ # The native `HxRuby.inspect_hlx(json)` is defined in Rust (see ext/hx_ruby).
26
+ # It takes a preset's JSON string and returns a Hash of tone facts, raising
27
+ # {HxRuby::CatalogNotInstalled} when the HX Edit resources are missing and
28
+ # {HxRuby::Error} for any other catalog read failure.
29
+ module HxRuby
30
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hx_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Carmine Paolino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rb_sys
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ description: Ruby bindings over hx-catalog's preset inspector, so the tone browser
28
+ reads .hlx files with the same Rust code the desktop uses instead of reimplementing
29
+ parsing in Ruby.
30
+ email:
31
+ - carmine@paolino.me
32
+ executables: []
33
+ extensions:
34
+ - ext/hx_ruby/extconf.rb
35
+ extra_rdoc_files: []
36
+ files:
37
+ - ext/hx_ruby/Cargo.toml
38
+ - ext/hx_ruby/extconf.rb
39
+ - ext/hx_ruby/src/lib.rs
40
+ - lib/hx_ruby.rb
41
+ - lib/hx_ruby/version.rb
42
+ homepage: https://docs.tonepush.rocks
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://docs.tonepush.rocks
47
+ source_code_uri: https://github.com/crmne/tonepush
48
+ changelog_uri: https://github.com/crmne/tonepush/releases
49
+ rubygems_mfa_required: 'true'
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.1.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.5.22
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Parse Line 6 .hlx presets into tone facts with the TonePush Rust catalog
69
+ test_files: []