Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/opal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: opal

on:
push:
branches: [ main ]
pull_request:

permissions:
contents: read

jobs:
opal-verification:
name: Opal boot file and payload verification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Run Opal specs
# Verifies the Opal toolchain for unitsml:
# - the boot file (lib/unitsml/opal.rb) eager-requires every
# autoloaded entry point so Opal users see no NameError,
# - the committed database_payload.rb is in sync with the
# PayloadGenerator output for the current unitsdb gem,
# - the PayloadGenerator produces evaluable Ruby that round-trips
# through Unitsml::Unitsdb::Database.from_hash,
# - Opal::Builder can compile the boot file end-to-end with
# native-only deps stubbed (the consumer provides them).
run: |
bundle exec rspec spec/unitsml/opal_boot_spec.rb \
spec/unitsml/payload_sync_spec.rb \
spec/unitsml/opal/payload_generator_spec.rb
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ AllCops:
Exclude:
- 'unitsdb/**/*'
- 'vendor/**/*'
- 'lib/unitsml/opal/database_payload.rb'
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ gemspec

gem "canon"
gem "lutaml-model", github: "lutaml/lutaml-model", branch: "main"
gem "nokogiri"
gem "oga"
gem "opal", "~> 1.8"
gem "ox"
gem "plurimath", github: "plurimath/plurimath", branch: "main"
gem "pry"
Expand Down
10 changes: 10 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

namespace :unitsml do
desc "Regenerate lib/unitsml/opal/database_payload.rb from unitsdb YAML"
task :generate_opal_payload do
require "unitsml/opal/payload_generator"
path = Unitsml::Opal::PayloadGenerator::DEFAULT_OUTPUT_PATH
Unitsml::Opal::PayloadGenerator.new.write_to(path)
puts "Wrote #{path}"
end
end

task default: :spec
17 changes: 8 additions & 9 deletions lib/unitsml/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

module Unitsml
module Model
autoload :Dimension, "#{__dir__}/model/dimension"
autoload :DimensionQuantities, "#{__dir__}/model/dimension_quantities"
autoload :Namespace, "#{__dir__}/model/namespace"
autoload :Prefix, "#{__dir__}/model/prefix"
autoload :Prefixes, "#{__dir__}/model/prefixes"
autoload :Quantities, "#{__dir__}/model/quantities"
autoload :Quantity, "#{__dir__}/model/quantity"
autoload :Unit, "#{__dir__}/model/unit"
autoload :Units, "#{__dir__}/model/units"
autoload :Dimension, "unitsml/model/dimension"
autoload :DimensionQuantities, "unitsml/model/dimension_quantities"
autoload :Prefix, "unitsml/model/prefix"
autoload :Prefixes, "unitsml/model/prefixes"
autoload :Quantities, "unitsml/model/quantities"
autoload :Quantity, "unitsml/model/quantity"
autoload :Unit, "unitsml/model/unit"
autoload :Units, "unitsml/model/units"
end
end
18 changes: 9 additions & 9 deletions lib/unitsml/model/dimension_quantities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ module Unitsml
module Model
module DimensionQuantities
autoload :AmountOfSubstance,
"#{__dir__}/dimension_quantities/amount_of_substance"
"unitsml/model/dimension_quantities/amount_of_substance"
autoload :ElectricCurrent,
"#{__dir__}/dimension_quantities/electric_current"
autoload :Length, "#{__dir__}/dimension_quantities/length"
"unitsml/model/dimension_quantities/electric_current"
autoload :Length, "unitsml/model/dimension_quantities/length"
autoload :LuminousIntensity,
"#{__dir__}/dimension_quantities/luminous_intensity"
autoload :Mass, "#{__dir__}/dimension_quantities/mass"
autoload :PlaneAngle, "#{__dir__}/dimension_quantities/plane_angle"
autoload :Quantity, "#{__dir__}/dimension_quantities/quantity"
"unitsml/model/dimension_quantities/luminous_intensity"
autoload :Mass, "unitsml/model/dimension_quantities/mass"
autoload :PlaneAngle, "unitsml/model/dimension_quantities/plane_angle"
autoload :Quantity, "unitsml/model/dimension_quantities/quantity"
autoload :ThermodynamicTemperature,
"#{__dir__}/dimension_quantities/thermodynamic_temperature"
autoload :Time, "#{__dir__}/dimension_quantities/time"
"unitsml/model/dimension_quantities/thermodynamic_temperature"
autoload :Time, "unitsml/model/dimension_quantities/time"
end
end
end
4 changes: 2 additions & 2 deletions lib/unitsml/model/prefixes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
module Unitsml
module Model
module Prefixes
autoload :Name, "#{__dir__}/prefixes/name"
autoload :Symbol, "#{__dir__}/prefixes/symbol"
autoload :Name, "unitsml/model/prefixes/name"
autoload :Symbol, "unitsml/model/prefixes/symbol"
end
end
end
2 changes: 1 addition & 1 deletion lib/unitsml/model/quantities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Unitsml
module Model
module Quantities
autoload :Name, "#{__dir__}/quantities/name"
autoload :Name, "unitsml/model/quantities/name"
end
end
end
96 changes: 96 additions & 0 deletions lib/unitsml/opal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true

# Opal entry point for unitsml.
#
# Under MRI, lib/unitsml.rb uses autoload for lazy loading. Under Opal,
# autoload does not lazy-execute, so this boot file eager-requires every
# entry point. Consumers (e.g. plurimath-js) add `-r unitsml/opal` to
# their Opal compile command.
#
# Load order rationale:
# 1. Upstream boot files first — unitsml depends on lutaml-model,
# unitsdb, mml (and transitively omml via plurimath-js consumers).
# 2. unitsml.rb next — sets up top-level autoloads and Configuration.
# 3. Errors before everything else (low-level, no deps).
# 4. The Opal payload — defines Unitsml::Unitsdb::Database::DATABASE
# so Unitsml::Unitsdb::Database.from_db can satisfy without a
# filesystem under Opal.
# 5. Unitsdb sub-files — define classes that subclass ::Unitsdb::*
# and call Configuration.register_model side-effects.
# 6. Model tree — declares the Unitsml XML model classes. Each parent
# module (Prefixes, Quantities, DimensionQuantities, Units) is
# required first to set up its own autoloads, then its leaves.
# 7. Parser/Transform/Prefix/Unit/Dimension etc. — top-level domain
# classes that reference Model and Unitsdb.

require "lutaml/model"
require "unitsdb/opal"
require "mml/opal"
require "omml/opal"

require "unitsml"

require "unitsml/version"
require "unitsml/errors"
require "unitsml/errors/base_error"
require "unitsml/errors/opal_payload_not_bundled_error"
require "unitsml/errors/plurimath_load_error"

require "unitsml/opal/database_payload"

require "unitsml/configuration"
require "unitsml/namespace"

require "unitsml/unitsdb"
require "unitsml/unitsdb/database"
require "unitsml/unitsdb/dimension_details"
require "unitsml/unitsdb/prefix_reference"
require "unitsml/unitsdb/dimension"
require "unitsml/unitsdb/dimensions"
require "unitsml/unitsdb/unit"
require "unitsml/unitsdb/units"
require "unitsml/unitsdb/prefixes"
require "unitsml/unitsdb/quantities"

require "unitsml/model"
require "unitsml/model/dimension"
require "unitsml/model/dimension_quantities"
require "unitsml/model/dimension_quantities/amount_of_substance"
require "unitsml/model/dimension_quantities/electric_current"
require "unitsml/model/dimension_quantities/length"
require "unitsml/model/dimension_quantities/luminous_intensity"
require "unitsml/model/dimension_quantities/mass"
require "unitsml/model/dimension_quantities/plane_angle"
require "unitsml/model/dimension_quantities/quantity"
require "unitsml/model/dimension_quantities/thermodynamic_temperature"
require "unitsml/model/dimension_quantities/time"
require "unitsml/model/prefix"
require "unitsml/model/prefixes"
require "unitsml/model/prefixes/name"
require "unitsml/model/prefixes/symbol"
require "unitsml/model/quantities"
require "unitsml/model/quantities/name"
require "unitsml/model/quantity"
require "unitsml/model/unit"
require "unitsml/model/units"
require "unitsml/model/units/enumerated_root_unit"
require "unitsml/model/units/name"
require "unitsml/model/units/root_units"
require "unitsml/model/units/symbol"
require "unitsml/model/units/system"

require "unitsml/dimension"
require "unitsml/extender"
require "unitsml/fenced"
require "unitsml/fenced_numeric"
require "unitsml/formula"
require "unitsml/intermediate_exp_rules"
require "unitsml/mathml_helper"
require "unitsml/number"
require "unitsml/parse"
require "unitsml/parser"
require "unitsml/prefix"
require "unitsml/sqrt"
require "unitsml/transform"
require "unitsml/unit"
require "unitsml/utility"
7 changes: 7 additions & 0 deletions lib/unitsml/opal/database_payload.rb

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions lib/unitsml/opal/payload_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require "unitsdb"

module Unitsml
module Opal
class PayloadGenerator
DEFAULT_OUTPUT_PATH =
File.expand_path("database_payload.rb", __dir__).freeze

def initialize(unitsdb_data_dir: default_data_dir)
@unitsdb_data_dir = unitsdb_data_dir
end

def call
"#{header}#{body}"
end

def write_to(path = DEFAULT_OUTPUT_PATH)
File.write(path, call)
path
end

private

attr_reader :unitsdb_data_dir

def header
<<~RUBY
# frozen_string_literal: true

# AUTO-GENERATED by `bundle exec rake unitsml:generate_opal_payload`.
# Do not edit by hand. Edit the YAML in unitsdb-ruby/data/ and
# regenerate. This file is loaded by lib/unitsml/opal.rb under Opal
# to satisfy Unitsml::Unitsdb::Database.from_db without a filesystem.
RUBY
end

def body
"Unitsml::Unitsdb::Database.const_set(:DATABASE, " \
"#{serialize(database_hash)}.freeze)\n"
end

def database_hash
::Unitsdb::Database.from_db(unitsdb_data_dir).to_hash
end

# Custom serializer rather than Hash#inspect so the output is
# byte-identical across Ruby versions (3.3 vs 3.4 changed Hash#inspect
# to add spaces around "=>"). String#inspect is itself stable for
# UTF-8 clean data.
def serialize(value)
case value
when Hash
"{#{value.map { |k, v| "#{serialize(k)}=>#{serialize(v)}" }.join(",")}}"
when Array
"[#{value.map { |v| serialize(v) }.join(",")}]"
when String then value.inspect
when Symbol then ":#{value}"
when Integer, Float then value.to_s
when true then "true"
when false then "false"
when nil then "nil"
else
raise "Unsupported payload type: #{value.class}"
end
end

def default_data_dir
File.join(unitsdb_gem_path, "data")
end

def unitsdb_gem_path
Gem.loaded_specs.fetch("unitsdb").full_gem_path
end
end
end
end
19 changes: 10 additions & 9 deletions lib/unitsml/unitsdb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

require "unitsdb"

require_relative "unitsdb/database"
require_relative "unitsdb/dimension_details"
require_relative "unitsdb/prefix_reference"
require_relative "unitsdb/dimension"
require_relative "unitsdb/dimensions"
require_relative "unitsdb/unit"
require_relative "unitsdb/units"
require_relative "unitsdb/prefixes"
require_relative "unitsdb/quantities"
module Unitsml
module Unitsdb
autoload :Database, "unitsml/unitsdb/database"
autoload :DimensionDetails, "unitsml/unitsdb/dimension_details"
autoload :PrefixReference, "unitsml/unitsdb/prefix_reference"
autoload :Dimension, "unitsml/unitsdb/dimension"
autoload :Dimensions, "unitsml/unitsdb/dimensions"
autoload :Unit, "unitsml/unitsdb/unit"
autoload :Units, "unitsml/unitsdb/units"
autoload :Prefixes, "unitsml/unitsdb/prefixes"
autoload :Quantities, "unitsml/unitsdb/quantities"

class << self
REQUIRED_DATABASE_FILES = %w[
prefixes.yaml
Expand Down
24 changes: 20 additions & 4 deletions lib/unitsml/unitsdb/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,34 @@
module Unitsml
module Unitsdb
class Database < ::Unitsdb::Database
DATABASE = nil

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.load_opal_payload(payload)
@opal_payload = payload
end

def self.reset_opal_payload
return unless instance_variable_defined?(:@opal_payload)

remove_instance_variable(:@opal_payload)
end

def self.opal_payload
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
Expand Down
Loading
Loading