chemomer 0.1.0.1

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: 142c9b75a559a70f3da42867d5fc666612991c6163592487c3dc62ff60c19c95
4
+ data.tar.gz: 7e3ee8a8f611b0090ef9bd98c893778dfae022365edeeb7d89c95f55161b9d51
5
+ SHA512:
6
+ metadata.gz: ac5e6ecbc4797c78a90ed6b7331181ce3d274f7f8b47afa70237faa421561f9055cee5fe3b17c8b814129c3a1c4c080758ddfa136797491a6b8e49dab9e6125e
7
+ data.tar.gz: d87604de945779f7260cbe29d8cfc950c4d74828e40d353c5d02669da1db8060dd8e212a272738b680bf4654f4f68fd2822be5352c09fb28c56a23098d9b926a
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ rspec:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest]
14
+ ruby: ["3.3", "3.4"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby }}
21
+ bundler-cache: true
22
+ # The gem drives the engine through its C ABI — no engine code is
23
+ # vendored, so CI builds the cdylib from the engine's main (the
24
+ # shared contract) and points the suite at it. Specs that need
25
+ # the dylib gate on CHEMOMER_LIB; the rest run everywhere.
26
+ - uses: dtolnay/rust-toolchain@stable
27
+ - name: Build the chemomer cdylib
28
+ run: |
29
+ git clone --depth 1 https://github.com/chemomer/chemomer engine
30
+ cargo build --release --manifest-path engine/Cargo.toml
31
+ - name: Point CHEMOMER_LIB at the built dylib
32
+ run: |
33
+ lib=$(ls engine/target/release/libchemomer.dylib engine/target/release/libchemomer.so 2>/dev/null | head -1)
34
+ echo "CHEMOMER_LIB=$PWD/$lib" >> "$GITHUB_ENV"
35
+ - run: bundle exec rspec
@@ -0,0 +1,50 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (must match lib/chemomer/version.rb)"
8
+ required: true
9
+ type: string
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ # Trusted publishing (OIDC): the publisher registered on RubyGems.org
15
+ # is repository chemomer/chemomer-ruby + workflow release.yml, with
16
+ # no environment — so this job must not claim one.
17
+ permissions:
18
+ contents: read
19
+ id-token: write
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ with:
23
+ ref: main
24
+ persist-credentials: false
25
+ - name: Verify version matches input
26
+ run: |
27
+ actual=$(ruby -e 'require "./lib/chemomer/version"; print Chemomer::VERSION')
28
+ if [ "$actual" != "${{ inputs.version }}" ]; then
29
+ echo "Version mismatch: input=${{ inputs.version }}, lib/chemomer/version.rb=$actual"
30
+ exit 1
31
+ fi
32
+ echo "Releasing $actual"
33
+ - uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: "3.4"
36
+ bundler-cache: true
37
+ # CI never pushes to git (read-only contents). `rake release`
38
+ # attempts `git push origin main` after publishing unless the
39
+ # version tag already exists locally — bundler then prints
40
+ # "Tag vX has already been created" and skips its git stage
41
+ # entirely. Pre-create the tag so the gem push is the only
42
+ # remote operation; tags on the remote remain the maintainer's.
43
+ - name: Pre-create the release tag (skips rake's git stage)
44
+ run: git tag "v${{ inputs.version }}"
45
+ # Builds and pushes using the GitHub OIDC identity — no API keys.
46
+ - uses: rubygems/release-gem@v1
47
+ - name: Summary
48
+ run: |
49
+ echo "Released chemomer ${{ inputs.version }} to RubyGems"
50
+ echo "https://rubygems.org/gems/chemomer"
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /pkg/
4
+ /spec/reports/
5
+ /tmp/
6
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "rspec", "~> 3.13"
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AsciiChem contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # chemomer
2
+
3
+ The **Ruby binding for the [chemomer](https://github.com/chemomer/chemomer)
4
+ engine** — molfile in, InChI identifier / InChIKey out, driven through
5
+ the engine's C ABI with [FFI](https://github.com/ffi/ffi).
6
+
7
+ ```ruby
8
+ require "chemomer"
9
+
10
+ inchi, inchikey = Chemomer.from_molfile(molfile_text)
11
+ aux = Chemomer.aux_from_molfile(molfile_text)
12
+ Chemomer.ffi_version # => 1
13
+ ```
14
+
15
+ A thin wrapper by design: the engine (the byte-exact InChI port, gated
16
+ by the shared oracle corpus
17
+ [inchi-tests](https://github.com/chemomer/inchi-tests)) lives in the
18
+ chemomer repo; this gem owns only the FFI signatures and the packaging
19
+ — no engine code is vendored. The cdylib is a runtime artifact: build
20
+ it once and point the gem at it.
21
+
22
+ ## The library
23
+
24
+ The gem resolves the cdylib in this order:
25
+
26
+ 1. `CHEMOMER_LIB` — the path to the built cdylib.
27
+ 2. The platform default sonames (`libchemomer.dylib` on macOS,
28
+ `libchemomer.so` on Linux), resolved through the loader's search
29
+ path — install the dylib on `LD_LIBRARY_PATH` /
30
+ `DYLD_LIBRARY_PATH` or into a standard lib dir for zero-config use.
31
+
32
+ Build the cdylib from an engine checkout:
33
+
34
+ ```bash
35
+ cargo build --release # in the chemomer repo
36
+ export CHEMOMER_LIB=$PWD/target/release/libchemomer.dylib
37
+ ```
38
+
39
+ Errors surface as `Chemomer::Error` — engine failures carry the C
40
+ ABI's thread-local `chemomer_last_error` message, resolution failures
41
+ carry the remedy.
42
+
43
+ ## Versioning
44
+
45
+ Runtime packages track the engine: `{engine semver}.{runtime version}`
46
+ (the same scheme as chemomer-py). This gem is `0.1.0.1` — engine
47
+ `0.1.0`, runtime `1`. An engine bump resets the counter (`0.2.0.1`);
48
+ a runtime-only fix increments it (`0.1.0.2`).
49
+
50
+ ## AuxInfo
51
+
52
+ `aux_from_molfile` needs the `chemomer_aux_from_molfile` C export,
53
+ which the engine's 0.1.0 ABI does not ship yet (the Python binding
54
+ reaches AuxInfo through the Rust API directly). The gem attaches the
55
+ symbol on first use: once an engine cdylib exports it, the method
56
+ works with no gem change; against a 0.1.0 cdylib it raises
57
+ `Chemomer::Error` saying exactly that.
58
+
59
+ ## Test
60
+
61
+ ```bash
62
+ bundle install
63
+ CHEMOMER_LIB=$PWD/../chemomer/target/release/libchemomer.dylib bundle exec rspec
64
+ ```
65
+
66
+ Specs that need the cdylib gate on `CHEMOMER_LIB` and skip cleanly
67
+ without it; the rest run everywhere. CI builds the engine's main
68
+ cdylib and runs the full suite on ubuntu + macos.
69
+
70
+ ## Publishing
71
+
72
+ `release.yml` is dispatch-only: a version input (verified against
73
+ `lib/chemomer/version.rb`), the pre-created-tag trick, then
74
+ `rubygems/release-gem@v1` — trusted publishing (OIDC), no stored
75
+ credentials, no environment.
76
+
77
+ The first publish of the gem name needs the RubyGems trusted publisher
78
+ registered first — gem `chemomer`, owner `chemomer`, repository
79
+ `chemomer/chemomer-ruby`, workflow `release.yml`, no environment — or
80
+ a local `gem push` for the very first version.
81
+
82
+ ## License
83
+
84
+ MIT, same as the engine.
data/chemomer.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/chemomer/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "chemomer"
7
+ spec.version = Chemomer::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "Ruby bindings for the chemomer InChI engine — molfile in, identifier / InChIKey out."
12
+ spec.description = "Chemomer drives the native chemomer engine — the Rust InChI port gated by the " \
13
+ "shared oracle corpus — through its C ABI via FFI. from_molfile returns the " \
14
+ "InChI identifier and the InChIKey; aux_from_molfile returns the AuxInfo line. " \
15
+ "No engine code is vendored: the cdylib is a runtime artifact."
16
+ spec.homepage = "https://github.com/chemomer/chemomer-ruby"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
19
+
20
+ spec.metadata["source_code_uri"] = "https://github.com/chemomer/chemomer-ruby"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
22
+
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ end
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "ffi"
31
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemomer
4
+ # The FFI surface of the chemomer cdylib: one anonymous module per
5
+ # library (attach_function binds the symbols once), cached per
6
+ # candidate list — the shape of asciichem-ruby's NativeEngine.
7
+ #
8
+ # The documented C ABI of chemomer 0.1.0:
9
+ #
10
+ # chemomer_from_molfile molfile → identifier string (null on error)
11
+ # chemomer_inchikey molfile → InChIKey string (null on error)
12
+ # chemomer_string_free releases a string returned by the ABI
13
+ # chemomer_last_error → the calling thread's last error, or null
14
+ # chemomer_ffi_version → the ABI version (1)
15
+ module Bindings
16
+ module_function
17
+
18
+ # The main surface: attaches eagerly (every symbol is part of the
19
+ # documented ABI) and caches per candidate list.
20
+ def for(*names)
21
+ bindings[names] ||= attach(names)
22
+ end
23
+
24
+ # AuxInfo: the 0.1.0 ABI does not export chemomer_aux_from_molfile
25
+ # (the Python binding reaches AuxInfo through the Rust API
26
+ # directly). The symbol is attached on first use, so the gem
27
+ # lights up without a change the moment an engine cdylib ships the
28
+ # export; against a dylib without it, a clear Chemomer::Error
29
+ # explains the gap.
30
+ def aux(*names)
31
+ aux_bindings[names] ||= attach_aux(names)
32
+ end
33
+
34
+ def bindings
35
+ @bindings ||= {}
36
+ end
37
+
38
+ def aux_bindings
39
+ @aux_bindings ||= {}
40
+ end
41
+
42
+ def attach(names)
43
+ guard_readable(names)
44
+ Module.new do
45
+ extend FFI::Library
46
+
47
+ ffi_lib(*names)
48
+ attach_function :chemomer_from_molfile, %i[string], :pointer
49
+ attach_function :chemomer_inchikey, %i[string], :pointer
50
+ attach_function :chemomer_string_free, %i[pointer], :void
51
+ attach_function :chemomer_last_error, [], :pointer
52
+ attach_function :chemomer_ffi_version, [], :uint32
53
+
54
+ # The message of the last error on the calling thread, or nil.
55
+ def self.last_error
56
+ pointer = chemomer_last_error
57
+ pointer.null? ? nil : pointer.read_string
58
+ end
59
+ end
60
+ rescue LoadError => e
61
+ raise Error, load_failure(e)
62
+ end
63
+
64
+ def attach_aux(names)
65
+ guard_readable(names)
66
+ Module.new do
67
+ extend FFI::Library
68
+
69
+ ffi_lib(*names)
70
+ attach_function :chemomer_aux_from_molfile, %i[string], :pointer
71
+ # The call path needs the companions too — they exist in every
72
+ # engine ABI version, so attaching them here is version-safe;
73
+ # only the aux symbol above is the version-sensitive one.
74
+ attach_function :chemomer_string_free, %i[pointer], :void
75
+ attach_function :chemomer_last_error, [], :pointer
76
+
77
+ def self.last_error
78
+ pointer = chemomer_last_error
79
+ pointer.null? ? nil : pointer.read_string
80
+ end
81
+ end
82
+ rescue FFI::NotFoundError
83
+ raise Error,
84
+ "chemomer: the cdylib does not export chemomer_aux_from_molfile — " \
85
+ "AuxInfo needs an engine that ships the export " \
86
+ "(github.com/chemomer/chemomer, src/ffi.rs)"
87
+ rescue LoadError => e
88
+ raise Error, load_failure(e)
89
+ end
90
+
91
+ # An explicit CHEMOMER_LIB path is one candidate; a missing or
92
+ # unreadable file deserves the direct message rather than the
93
+ # loader's. The default soname list is searched by the loader
94
+ # instead — nothing to check here.
95
+ def guard_readable(names)
96
+ return unless names.one?
97
+
98
+ path = names.first
99
+ return if File.readable?(path)
100
+
101
+ raise Error,
102
+ "chemomer: #{path} is not readable — set CHEMOMER_LIB to the built " \
103
+ "cdylib (cargo build --release in the chemomer checkout)"
104
+ end
105
+
106
+ def load_failure(error)
107
+ "chemomer: could not load the cdylib (#{error.message}). Set CHEMOMER_LIB " \
108
+ "to the built cdylib (cargo build --release in the chemomer checkout) or " \
109
+ "install it on the loader path."
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemomer
4
+ # The single error type of the gem: engine failures (raised from the
5
+ # C ABI's thread-local last_error) and library-resolution failures
6
+ # alike, so callers rescue one thing.
7
+ class Error < StandardError; end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chemomer
4
+ # The {engine semver}.{runtime version} scheme (shared with
5
+ # chemomer-py): the chemomer engine release this gem wraps (0.1.0)
6
+ # plus the gem's own runtime counter (1). An engine bump resets the
7
+ # counter (0.2.0.1); a runtime-only fix increments it (0.1.0.2).
8
+ VERSION = "0.1.0.1"
9
+ end
data/lib/chemomer.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "rbconfig"
5
+
6
+ # Chemomer drives the native chemomer engine — the Rust InChI port
7
+ # gated by the shared oracle corpus (chemomer/chemomer) — through its
8
+ # C ABI via FFI. A thin wrapper by design: no engine code is vendored,
9
+ # the gem owns only the FFI signatures and the packaging. The cdylib
10
+ # is a runtime artifact: build it once from an engine checkout and
11
+ # point CHEMOMER_LIB at it, or install it on the loader path for the
12
+ # default soname resolution.
13
+ #
14
+ # Chemomer.from_molfile(molfile) # => [inchi, inchikey]
15
+ # Chemomer.aux_from_molfile(molfile) # => aux line
16
+ # Chemomer.ffi_version # => 1
17
+ module Chemomer
18
+ autoload :VERSION, "chemomer/version"
19
+ autoload :Bindings, "chemomer/bindings"
20
+ autoload :Error, "chemomer/error"
21
+
22
+ # Fallback sonames tried in order when CHEMOMER_LIB is unset; the
23
+ # OS loader resolves them from its search path (LD_LIBRARY_PATH /
24
+ # DYLD_LIBRARY_PATH, /usr/local/lib, ...).
25
+ DEFAULT_LIB_NAMES = {
26
+ darwin: %w[libchemomer.dylib chemomer.dylib],
27
+ linux: %w[libchemomer.so chemomer.so],
28
+ windows: %w[chemomer.dll libchemomer.dll]
29
+ }.freeze
30
+
31
+ module_function
32
+
33
+ # The library candidates to attach: CHEMOMER_LIB when set, else
34
+ # this platform's default sonames.
35
+ def lib
36
+ env = ENV.fetch("CHEMOMER_LIB", nil)
37
+ env ? [env] : DEFAULT_LIB_NAMES.fetch(platform)
38
+ end
39
+
40
+ # molfile → [InChI identifier, InChIKey].
41
+ #
42
+ # Chemomer.from_molfile(water_molfile)
43
+ # # => ["InChI=1S/H2O/h1H2", "XLYOFNOQVPJJNP-UHFFFAOYSA-N"]
44
+ def from_molfile(molfile)
45
+ bindings = Bindings.for(*lib)
46
+ inchi = call(bindings, molfile, :chemomer_from_molfile)
47
+ inchikey = call(bindings, molfile, :chemomer_inchikey)
48
+ [inchi, inchikey]
49
+ end
50
+
51
+ # molfile → the AuxInfo line (per-component canonical numbering,
52
+ # reversibility block). Needs the chemomer_aux_from_molfile export —
53
+ # attached on first use, so it lights up the moment an engine cdylib
54
+ # ships it.
55
+ def aux_from_molfile(molfile)
56
+ bindings = Bindings.aux(*lib)
57
+ call(bindings, molfile, :chemomer_aux_from_molfile)
58
+ end
59
+
60
+ # The C ABI version of the attached cdylib (1 today).
61
+ def ffi_version
62
+ Bindings.for(*lib).chemomer_ffi_version
63
+ end
64
+
65
+ # The C ABI contract: null return on error (the message sits on the
66
+ # calling thread's last_error slot), the returned string freed right
67
+ # after reading it.
68
+ def call(bindings, molfile, function)
69
+ pointer = bindings.public_send(function, molfile)
70
+ # A null return arrives as a non-nil FFI::Pointer whose address is
71
+ # 0 — the truthiness test would sail past it, so null? is the
72
+ # guard.
73
+ if pointer.nil? || pointer.null?
74
+ raise Error, "chemomer: #{bindings.last_error || 'call failed'}"
75
+ end
76
+ value = pointer.read_string
77
+ bindings.chemomer_string_free(pointer)
78
+ value
79
+ end
80
+ private_class_method :call
81
+
82
+ def platform
83
+ case RbConfig::CONFIG["host_os"]
84
+ when /darwin/ then :darwin
85
+ when /linux/ then :linux
86
+ when /mswin|mingw|cygwin/ then :windows
87
+ else
88
+ raise Error,
89
+ "chemomer: no default soname for #{RbConfig::CONFIG['host_os']} — " \
90
+ "set CHEMOMER_LIB to the built cdylib"
91
+ end
92
+ end
93
+ private_class_method :platform
94
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chemomer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: 'Chemomer drives the native chemomer engine — the Rust InChI port gated
27
+ by the shared oracle corpus — through its C ABI via FFI. from_molfile returns the
28
+ InChI identifier and the InChIKey; aux_from_molfile returns the AuxInfo line. No
29
+ engine code is vendored: the cdylib is a runtime artifact.'
30
+ email:
31
+ - open.source@ribose.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".github/workflows/ci.yml"
37
+ - ".github/workflows/release.yml"
38
+ - ".gitignore"
39
+ - Gemfile
40
+ - LICENSE
41
+ - README.md
42
+ - chemomer.gemspec
43
+ - lib/chemomer.rb
44
+ - lib/chemomer/bindings.rb
45
+ - lib/chemomer/error.rb
46
+ - lib/chemomer/version.rb
47
+ homepage: https://github.com/chemomer/chemomer-ruby
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ source_code_uri: https://github.com/chemomer/chemomer-ruby
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.3.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.23
68
+ specification_version: 4
69
+ summary: Ruby bindings for the chemomer InChI engine — molfile in, identifier / InChIKey
70
+ out.
71
+ test_files: []