From b7f014a80a92543821ce28de375fa0c06773f437 Mon Sep 17 00:00:00 2001 From: suleman-uzair Date: Fri, 24 Apr 2026 22:23:40 +0500 Subject: [PATCH 1/3] fix: add Opal database payload loader --- lib/unitsml/unitsdb/database.rb | 12 ++++++++++-- spec/unitsml/unitsdb/database_spec.rb | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/unitsml/unitsdb/database.rb b/lib/unitsml/unitsdb/database.rb index 7a5be53..0aa3abc 100644 --- a/lib/unitsml/unitsdb/database.rb +++ b/lib/unitsml/unitsdb/database.rb @@ -5,15 +5,23 @@ module Unitsdb class Database < ::Unitsdb::Database DATABASE = nil + def self.load_opal_payload(database) + @opal_payload = database + end + def self.from_db(dir_path, context: Unitsml::Configuration.context.id) return super unless RUBY_ENGINE == "opal" context_id = context.to_sym - raise Unitsml::Errors::OpalPayloadNotBundledError unless DATABASE + raise Unitsml::Errors::OpalPayloadNotBundledError unless opal_payload Unitsml::Configuration.context - from_hash(DATABASE, register: context_id) + from_hash(opal_payload, register: context_id) + end + + def self.opal_payload + @opal_payload ||= const_get(:DATABASE, false) if const_defined?(:DATABASE, false) end Configuration.register_model(self, id: :database) diff --git a/spec/unitsml/unitsdb/database_spec.rb b/spec/unitsml/unitsdb/database_spec.rb index cfe495f..605a8c3 100644 --- a/spec/unitsml/unitsdb/database_spec.rb +++ b/spec/unitsml/unitsdb/database_spec.rb @@ -21,7 +21,11 @@ end context "when running on opal" do - before { stub_const("RUBY_ENGINE", "opal") } + before do + stub_const("RUBY_ENGINE", "opal") + described_class.remove_instance_variable(:@opal_payload) if + described_class.instance_variable_defined?(:@opal_payload) + end it "raises a clear error when the bundled payload is missing" do expect do @@ -29,6 +33,19 @@ end.to raise_error(Unitsml::Errors::OpalPayloadNotBundledError, /not bundled/) end + + it "loads the bundled Opal payload" do + described_class.load_opal_payload({ "units" => [] }) + allow(described_class).to receive(:from_hash).and_return(:database) + + result = described_class.from_db("/does/not/matter", context: :unitsml_ruby) + + expect(result).to eq(:database) + expect(described_class).to have_received(:from_hash).with( + { "units" => [] }, + register: :unitsml_ruby, + ) + end end end From 85e08a1c9a250cbc7f38e99b21433717771b5179 Mon Sep 17 00:00:00 2001 From: suleman-uzair Date: Mon, 27 Apr 2026 13:09:47 +0500 Subject: [PATCH 2/3] fix: clean up Opal database payload loader --- lib/unitsml/unitsdb/database.rb | 13 +++++--- spec/unitsml/unitsdb/database_spec.rb | 46 +++++++++++++++++++++------ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/lib/unitsml/unitsdb/database.rb b/lib/unitsml/unitsdb/database.rb index 0aa3abc..e0ff20b 100644 --- a/lib/unitsml/unitsdb/database.rb +++ b/lib/unitsml/unitsdb/database.rb @@ -3,10 +3,8 @@ module Unitsml module Unitsdb class Database < ::Unitsdb::Database - DATABASE = nil - - def self.load_opal_payload(database) - @opal_payload = database + def self.load_opal_payload(payload) + @opal_payload = payload end def self.from_db(dir_path, context: Unitsml::Configuration.context.id) @@ -15,14 +13,19 @@ def self.from_db(dir_path, context: Unitsml::Configuration.context.id) context_id = context.to_sym raise Unitsml::Errors::OpalPayloadNotBundledError unless opal_payload + # Ensure the UnitsML context is registered when context: is direct. Unitsml::Configuration.context from_hash(opal_payload, register: context_id) end def self.opal_payload - @opal_payload ||= const_get(:DATABASE, false) if const_defined?(:DATABASE, false) + return @opal_payload if instance_variable_defined?(:@opal_payload) + return unless const_defined?(:DATABASE, false) + + @opal_payload = const_get(:DATABASE, false) end + private_class_method :opal_payload Configuration.register_model(self, id: :database) end diff --git a/spec/unitsml/unitsdb/database_spec.rb b/spec/unitsml/unitsdb/database_spec.rb index 605a8c3..91e24a9 100644 --- a/spec/unitsml/unitsdb/database_spec.rb +++ b/spec/unitsml/unitsdb/database_spec.rb @@ -21,10 +21,28 @@ end context "when running on opal" do + def clear_opal_payload + return unless described_class.instance_variable_defined?(:@opal_payload) + + described_class.remove_instance_variable(:@opal_payload) + end + before do stub_const("RUBY_ENGINE", "opal") - described_class.remove_instance_variable(:@opal_payload) if - described_class.instance_variable_defined?(:@opal_payload) + clear_opal_payload + end + + after do + clear_opal_payload + end + + def allow_from_hash(database_class) + allow(database_class).to receive(:from_hash).and_return(:database) + end + + def expect_loaded_payload(database_class, payload) + expect(database_class).to have_received(:from_hash) + .with(payload, register: :unitsml_ruby) end it "raises a clear error when the bundled payload is missing" do @@ -35,16 +53,24 @@ end it "loads the bundled Opal payload" do - described_class.load_opal_payload({ "units" => [] }) - allow(described_class).to receive(:from_hash).and_return(:database) + payload = { "units" => [] } + described_class.load_opal_payload(payload) + allow_from_hash(described_class) - result = described_class.from_db("/does/not/matter", context: :unitsml_ruby) + expect(described_class.from_db("/does/not/matter", + context: :unitsml_ruby)).to eq(:database) + expect_loaded_payload(described_class, payload) + end - expect(result).to eq(:database) - expect(described_class).to have_received(:from_hash).with( - { "units" => [] }, - register: :unitsml_ruby, - ) + it "falls back to a legacy DATABASE constant" do + payload = { "units" => [:from_const] } + subclass = Class.new(described_class) + subclass.const_set(:DATABASE, payload) + allow_from_hash(subclass) + + expect(subclass.from_db("/does/not/matter", + context: :unitsml_ruby)).to eq(:database) + expect_loaded_payload(subclass, payload) end end end From 83ca2c6f7ffc1a129fd3647efe42dd0b5240005b Mon Sep 17 00:00:00 2001 From: suleman-uzair Date: Wed, 29 Apr 2026 21:39:42 +0500 Subject: [PATCH 3/3] test: cover Opal database payload loading --- spec/unitsml/unitsdb/database_spec.rb | 43 +++++++++++++++++++++++++++ spec/unitsml_spec.rb | 26 ++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/spec/unitsml/unitsdb/database_spec.rb b/spec/unitsml/unitsdb/database_spec.rb index 91e24a9..10b7036 100644 --- a/spec/unitsml/unitsdb/database_spec.rb +++ b/spec/unitsml/unitsdb/database_spec.rb @@ -101,6 +101,49 @@ def expect_loaded_payload(database_class, payload) end end + context "when running on opal with a loaded payload" do + let(:payload) { { "units" => [] } } + + before do + stub_const("RUBY_ENGINE", "opal") + allow(Unitsml::Configuration).to receive(:context).and_call_original + clear_unitsdb_database_cache + clear_unitsml_database_cache + Unitsml::Configuration.context(force_populate: true) + Unitsml::Unitsdb::Database.load_opal_payload(payload) + allow(Unitsml::Unitsdb::Database) + .to receive(:from_hash) + .and_return(:opal_database) + end + + after do + clear_unitsdb_database_cache + clear_unitsml_database_cache + clear_opal_payload + end + + def clear_unitsdb_database_cache + Unitsdb.instance_variable_set(:@databases, nil) + end + + def clear_unitsml_database_cache + described_class.instance_variable_set(:@database, nil) + end + + def clear_opal_payload + database_class = Unitsml::Unitsdb::Database + return unless database_class.instance_variable_defined?(:@opal_payload) + + database_class.remove_instance_variable(:@opal_payload) + end + + it "loads the UnitsML database through the real unitsdb-ruby loader" do + expect(described_class.database).to eq(:opal_database) + expect(Unitsml::Unitsdb::Database).to have_received(:from_hash) + .with(payload, register: :unitsml_ruby) + end + end + context "when unitsdb-ruby exposes a database loader" do before do stub_const("RUBY_ENGINE", "ruby") diff --git a/spec/unitsml_spec.rb b/spec/unitsml_spec.rb index 054263e..bc99558 100644 --- a/spec/unitsml_spec.rb +++ b/spec/unitsml_spec.rb @@ -1,10 +1,36 @@ # frozen_string_literal: true +require "open3" +require "rbconfig" + RSpec.describe Unitsml do it "has a version number" do expect(Unitsml::VERSION).not_to be_nil end + it "does not change an existing global XML adapter when required" do + lib_path = File.expand_path("../lib", __dir__) + script = <<~RUBY + require "lutaml/model" + Lutaml::Model::Config.xml_adapter_type = :nokogiri + require "unitsml" + + unless Lutaml::Model::Config.xml_adapter_type == :nokogiri + abort "expected :nokogiri, got \#{Lutaml::Model::Config.xml_adapter_type.inspect}" + end + RUBY + + _stdout, stderr, status = Open3.capture3( + RbConfig.ruby, + "-I", + lib_path, + "-e", + script, + ) + + expect(status).to be_success, stderr + end + it "parses a basic unit expression" do formula = described_class.parse("mm") expect(formula).to be_a(Unitsml::Formula)