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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
.rspec_status

Gemfile.lock

# Local-only refactor audit notes — never commit
TODO*
177 changes: 142 additions & 35 deletions lib/unitsdb.rb
Original file line number Diff line number Diff line change
@@ -1,46 +1,124 @@
# frozen_string_literal: true

require "lutaml/model"
require "unitsdb/config"
require "unitsdb/identifier"
require "unitsdb/localized_string"
require "unitsdb/symbol_presentations"
require "unitsdb/scale_properties"
require "unitsdb/unit_reference"
require "unitsdb/prefix_reference"
require "unitsdb/quantity_reference"
require "unitsdb/dimension_reference"
require "unitsdb/unit_system_reference"
require "unitsdb/scale_reference"
require "unitsdb/external_reference"
require "unitsdb/root_unit_reference"
require "unitsdb/si_derived_base"
require "unitsdb/dimension_details"
require "unitsdb/dimension"
require "unitsdb/prefix"
require "unitsdb/unit_system"
require "unitsdb/quantity"
require "unitsdb/scale"
require "unitsdb/unit"
require "unitsdb/dimensions"
require "unitsdb/prefixes"
require "unitsdb/quantities"
require "unitsdb/scales"
require "unitsdb/unit_systems"
require "unitsdb/units"
require "unitsdb/database"
require "unitsdb/qudt"
require "unitsdb/ucum"

module Unitsdb
# Core models are eagerly loaded so type registrations are complete before
# any context or database loading happens.
# All model constants are autoloaded from their respective files.
# Config.build_registry triggers `eager_load_models!` so the type
# registry is complete before iteration — otherwise autoload would
# leave unreferenced models (Scale, Qudt*, Ucum*) unregistered.

autoload :Config, "unitsdb/config"
autoload :Errors, "unitsdb/errors"
autoload :Utils, "unitsdb/utils"
autoload :Database, "unitsdb/database"

# Leaf models
autoload :Identifier, "unitsdb/identifier"
autoload :LocalizedString, "unitsdb/localized_string"
autoload :SymbolPresentations, "unitsdb/symbol_presentations"
autoload :ScaleProperties, "unitsdb/scale_properties"

# Reference models
autoload :UnitReference, "unitsdb/unit_reference"
autoload :PrefixReference, "unitsdb/prefix_reference"
autoload :QuantityReference, "unitsdb/quantity_reference"
autoload :DimensionReference, "unitsdb/dimension_reference"
autoload :UnitSystemReference, "unitsdb/unit_system_reference"
autoload :ScaleReference, "unitsdb/scale_reference"
autoload :ExternalReference, "unitsdb/external_reference"
autoload :RootUnitReference, "unitsdb/root_unit_reference"
autoload :SiDerivedBase, "unitsdb/si_derived_base"
autoload :DimensionDetails, "unitsdb/dimension_details"

# Entity models
autoload :Dimension, "unitsdb/dimension"
autoload :Prefix, "unitsdb/prefix"
autoload :UnitSystem, "unitsdb/unit_system"
autoload :Quantity, "unitsdb/quantity"
autoload :Scale, "unitsdb/scale"
autoload :Unit, "unitsdb/unit"

# Collection container models
autoload :Dimensions, "unitsdb/dimensions"
autoload :Prefixes, "unitsdb/prefixes"
autoload :Quantities, "unitsdb/quantities"
autoload :Scales, "unitsdb/scales"
autoload :UnitSystems, "unitsdb/unit_systems"
autoload :Units, "unitsdb/units"

# QUDT vocabulary models — all defined in unitsdb/qudt.rb
autoload :QudtUnit, "unitsdb/qudt"
autoload :QudtQuantityKind, "unitsdb/qudt"
autoload :QudtDimensionVector, "unitsdb/qudt"
autoload :QudtSystemOfUnits, "unitsdb/qudt"
autoload :QudtPrefix, "unitsdb/qudt"
autoload :QudtVocabularies, "unitsdb/qudt"

# UCUM XML models — all defined in unitsdb/ucum.rb
autoload :UcumBaseUnit, "unitsdb/ucum"
autoload :UcumPrefixValue, "unitsdb/ucum"
autoload :UcumPrefix, "unitsdb/ucum"
autoload :UcumUnitValueFunction, "unitsdb/ucum"
autoload :UcumUnitValue, "unitsdb/ucum"
autoload :UcumUnit, "unitsdb/ucum"
autoload :UcumNamespace, "unitsdb/ucum"
autoload :UcumFile, "unitsdb/ucum"

# CLI and Commands pull in Thor, which doesn't run under Opal.
unless RUBY_ENGINE == "opal"
autoload :Cli, "unitsdb/cli"
autoload :Commands, "unitsdb/commands"
end
autoload :Errors, "unitsdb/errors"
autoload :Utils, "unitsdb/utils"

# Constants eagerly loaded so Lutaml::Model type registrations are
# complete before Config.build_registry iterates them. Listed
# explicitly so unreferenced models (Scale, Qudt*, Ucum*) still
# register themselves via their file-bottom `Config.register_model`
# calls.
MODELS = %i[
Identifier
LocalizedString
SymbolPresentations
ScaleProperties
UnitReference
PrefixReference
QuantityReference
DimensionReference
UnitSystemReference
ScaleReference
ExternalReference
RootUnitReference
SiDerivedBase
DimensionDetails
Dimension
Prefix
UnitSystem
Quantity
Scale
Unit
Dimensions
Prefixes
Quantities
Scales
UnitSystems
Units
Database
QudtUnit
QudtQuantityKind
QudtDimensionVector
QudtSystemOfUnits
QudtPrefix
QudtVocabularies
UcumBaseUnit
UcumPrefixValue
UcumPrefix
UcumUnitValueFunction
UcumUnitValue
UcumUnit
UcumNamespace
UcumFile
].freeze

class << self
# Returns the path to the bundled data directory containing YAML files
Expand All @@ -51,13 +129,42 @@ def data_dir
# Returns a pre-loaded Database instance from the bundled data
def database(context: Config.context_id)
context_id = context.to_sym
Config.context(context_id) if context_id == Config.context_id && Config.find_context(context_id).nil?
Config.ensure_default_context! if context_id == Config.context_id
klass = Config.resolve_type(:database, context: context_id)
databases[context_id] ||= klass.from_db(data_dir, context: context_id)
end

# Drop the memoized Database instances so the next `database` call
# reloads from disk. Specs use this between examples.
def reset_database_cache!
@databases = nil
end

# Force every autoloaded model to load AND register itself with
# Config. The per-file `Config.register_model(Class, id: :foo)`
# calls at file-bottom run the first time a model loads, but
# `Config.capture_state` / `restore_state` can wipe those entries
# between tests. Re-registering here makes the registry
# self-healing across snapshot/restore cycles.
def eager_load_models!
MODELS.each do |sym|
klass = const_get(sym)
next unless klass.is_a?(Class) && klass < ::Lutaml::Model::Serializable

Config.register_model(klass, id: model_id_for(klass))
end
end

private

def model_id_for(klass)
klass.name.split("::").last
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.downcase
.to_sym
end

def databases
@databases ||= {}
end
Expand Down
46 changes: 5 additions & 41 deletions lib/unitsdb/cli.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
# frozen_string_literal: true

require "thor"
require "fileutils"

module Unitsdb
class Cli < Thor
# Enable --trace globally for all subcommands
# When enabled, Thor shows full backtraces on error
class_option :trace, type: :boolean, default: false,
desc: "Show full backtrace on error"

# Fix Thor deprecation warning
def self.exit_on_failure?
true
end

class Cli < Unitsdb::Commands::Thor
desc "ucum SUBCOMMAND", "UCUM-related commands"
subcommand "ucum", Commands::UcumCommand

Expand All @@ -41,7 +30,7 @@ def self.exit_on_failure?
desc: "Path to UnitsDB database (required)"

def search(query)
run_command(Commands::Search, :run, query)
run_command(Commands::Search, options, query, method: :run)
end

desc "get ID", "Get detailed information about an entity by ID"
Expand All @@ -52,7 +41,7 @@ def search(query)
option :database, type: :string, required: true, aliases: "-d",
desc: "Path to UnitsDB database (required)"
def get(id)
run_command(Commands::Get, :get, id)
run_command(Commands::Get, options, id, method: :get)
end

desc "check_si",
Expand All @@ -71,7 +60,7 @@ def get(id)
desc: "Path to UnitsDB database (required)"

def check_si
run_command(Commands::CheckSiCommand, :run)
run_command(Commands::CheckSiCommand, options)
end

desc "release", "Create release files (unified YAML and/or ZIP archive)"
Expand All @@ -84,32 +73,7 @@ def check_si
option :database, type: :string, required: true, aliases: "-d",
desc: "Path to UnitsDB database (required)"
def release
run_command(Commands::Release, :run)
end

private

def run_command(command_class, method, *args)
command = command_class.new(options)
command.send(method, *args)
rescue Unitsdb::Errors::CLIRuntimeError => e
handle_cli_error(e)
rescue StandardError => e
handle_error(e)
end

def handle_cli_error(error)
raise error if debugging?

warn "Error: #{error.message}"
exit 1
end

def handle_error(error)
raise error if debugging?

warn "Error: #{error.message}"
exit 1
run_command(Commands::Release, options)
end
end
end
2 changes: 2 additions & 0 deletions lib/unitsdb/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

module Unitsdb
module Commands
autoload :Thor, "unitsdb/commands/thor"
autoload :ModifyCommand, "unitsdb/commands/_modify"
autoload :Base, "unitsdb/commands/base"
autoload :CheckSi, "unitsdb/commands/check_si"
autoload :CheckSiCommand, "unitsdb/commands/check_si"
autoload :EntityPresenter, "unitsdb/commands/entity_presenter"
autoload :Get, "unitsdb/commands/get"
autoload :Normalize, "unitsdb/commands/normalize"
autoload :Qudt, "unitsdb/commands/qudt"
Expand Down
35 changes: 1 addition & 34 deletions lib/unitsdb/commands/_modify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@

module Unitsdb
module Commands
class ModifyCommand < Thor
# Inherit trace option from parent CLI
class_option :trace, type: :boolean, default: false,
desc: "Show full backtrace on error"

class ModifyCommand < Unitsdb::Commands::Thor
desc "normalize INPUT OUTPUT",
"Normalize a YAML file or all YAML files with --all"
method_option :sort, type: :string,
Expand All @@ -23,35 +19,6 @@ class ModifyCommand < Thor

def normalize(input = nil, output = nil)
run_command(Normalize, options, input, output)
rescue Unitsdb::Errors::CLIRuntimeError => e
handle_cli_error(e)
rescue StandardError => e
handle_error(e)
end

private

def run_command(command_class, options, *args)
command = command_class.new(options)
command.run(*args)
end

def handle_cli_error(error)
if options[:trace]
raise error
else
warn "Error: #{error.message}"
exit 1
end
end

def handle_error(error)
if options[:trace]
raise error
else
warn "Error: #{error.message}"
exit 1
end
end
end
end
Expand Down
Loading
Loading