Skip to content
Closed
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ require (
github.com/go-ruby-pundit/pundit v0.0.0-20260706120141-d3cc8a101bc3
github.com/go-ruby-racc/racc v0.0.0-20260630123809-0d492278523f
github.com/go-ruby-rack/rack v0.0.0-20260705200150-888027c33329
github.com/go-ruby-rails/rails v0.0.0-20260706183557-5ddb406695b0
github.com/go-ruby-railties/railties v0.0.0-20260706120201-300b4fe0f2df
github.com/go-ruby-rdoc/rdoc v0.0.0-20260702162339-c866323cc54e
github.com/go-ruby-redis/redis v0.0.0-20260701125752-5de216f6ad92
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ github.com/go-ruby-racc/racc v0.0.0-20260630123809-0d492278523f h1:+PEruR5OxctQr
github.com/go-ruby-racc/racc v0.0.0-20260630123809-0d492278523f/go.mod h1:HeFcYYwifNZvQzF9jdaSFViHYT0mUNLwWEFPQj0j6V4=
github.com/go-ruby-rack/rack v0.0.0-20260705200150-888027c33329 h1:4sXRBW9MXaEuosiyzzk1Fh+C8R2+PiPdua5sutdvV+E=
github.com/go-ruby-rack/rack v0.0.0-20260705200150-888027c33329/go.mod h1:rKZ3vD81gJq61NvZ3XX9cr6AxtIV3+lFZKG7lPKV3PM=
github.com/go-ruby-rails/rails v0.0.0-20260706183557-5ddb406695b0 h1:igwKtg0QdPTtWX6U7MTy8KKRbrAkqfoR6JHb2uPh/qs=
github.com/go-ruby-rails/rails v0.0.0-20260706183557-5ddb406695b0/go.mod h1:0RYYAMUG0AGHYVzGF0V+0CBLsPTEFUgXe60YW9ik1+8=
github.com/go-ruby-railties/railties v0.0.0-20260706120201-300b4fe0f2df h1:GQCDL/SGGtHqmDB2IfgCFe7AsgAA2LuhHg0kbbpYoFA=
github.com/go-ruby-railties/railties v0.0.0-20260706120201-300b4fe0f2df/go.mod h1:AKDyiSCjqbfeZaNm8IsIUXao8ATsl2LQ9kdT5GaqYx4=
github.com/go-ruby-rdoc/rdoc v0.0.0-20260702162339-c866323cc54e h1:Orev3pHC09GghkceOci3+dlV4Untoly525O7MyFdXOY=
Expand Down
1 change: 1 addition & 0 deletions internal/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ func (vm *VM) bootstrap() {
vm.registerActiveStorage() // ActiveStorage::Blob/Service/Attachment + Attached::One/Many (require "active_storage"), backed by go-ruby-activestorage; ModelStore/Service/Signer/Random/Clock seams wired to a deterministic in-process config (MemStore + DiskService temp dir); needs StandardError for ActiveStorage::Error
vm.registerActionCable() // ActionCable::Server/Connection::Base/Channel::Base + ActionCable.server (require "action_cable"), backed by go-ruby-actioncable; the transport/channel-action/factory seams eval Ruby bodies + the async adapter default keep the byte-exact actioncable-v1-json wire protocol in the library; needs StandardError (ActionCable::Error)
vm.registerRailties() // Rails::Railtie/Engine/Application boot framework (require "rails"), backed by go-ruby-railties; the initializer/hook/routes blocks are the rbgo seams (run inline on initialize!); a later rails meta-gem layers the top-level Rails.* accessors
vm.registerRails() // the `rails` meta-gem (require "rails/all"), backed by go-ruby-rails: extends railties' Rails module with the top-level singleton accessors (Rails.application/env/root/…), Rails::VERSION, Rails::EnvironmentInquirer and the component catalog; MUST run after registerRailties (extends the Rails module it created)
vm.registerDevise() // Devise::Config/Resource/Encryptor/TokenGenerator + Strategies::DatabaseAuthenticatable (require "devise"), backed by go-ruby-devise over go-ruby-bcrypt + go-ruby-warden; the model #[]/#[]=/#save + finder callable are the rbgo seams, and the strategy plugs into the bound Warden registry; needs StandardError (Devise::Error). Registered after registerWarden + registerBCrypt (dependency order)
vm.registerHanami() // Hanami::Router (hanami-router) + Hanami::Action (hanami-controller) (require "hanami" / "hanami/router" / "hanami/action"), backed by go-ruby-hanami over go-ruby-rack; the endpoint Resolver, the action #handle body (ActionCall), before/after callbacks, handle_exception, params validation and session loading are the rbgo seams; needs Rack, so registered after registerRack
vm.registerActiveRecord() // ActiveRecord::Model/Relation/Record + Base.establish_connection (require "active_record"), backed by go-ruby-activerecord; adapter seam wired to go-ruby-sqlite3 so queries run; needs StandardError for ActiveRecordError
Expand Down
5 changes: 5 additions & 0 deletions internal/vm/digest_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ func hasCustomEq(_ *VM, v object.Value) bool {
// Rails::StringInquirer#== compares the wrapped string by value (env ==
// "production"), not object identity, so it must dispatch its own ==.
return true
case *RailsEnvVal:
// Rails::EnvironmentInquirer#== compares the wrapped environment name by
// value (Rails.env == "production"), not object identity, so it must
// dispatch its own ==.
return true
}
return false
}
Expand Down
2 changes: 2 additions & 0 deletions internal/vm/object_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,8 @@ func (vm *VM) classOf(v object.Value) *RClass {
return x.cls
case *StringInquirerVal:
return x.cls
case *RailsEnvVal:
return x.cls
case *DeviseConfig:
return x.cls
case *DeviseResource:
Expand Down
237 changes: 237 additions & 0 deletions internal/vm/rails_meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
rails "github.com/go-ruby-rails/rails"
railties "github.com/go-ruby-railties/railties"

"github.com/go-embedded-ruby/ruby/internal/object"
)

// registerRails layers the `rails` meta-gem onto the boot framework the railties
// binding already installed (registerRailties, which must run first). In Ruby the
// `rails` gem ships almost no code of its own: it is the meta-gem that ties the
// framework's components together and vends the top-level Rails module singleton
// accessors (Rails.application / env / root / logger / cache / configuration /
// autoloaders / groups / public_path / backtrace_cleaner / error), the
// Rails::VERSION constant, and Rails::EnvironmentInquirer (the Rails.env type).
//
// This binding does NOT re-create the Rails module or its Railtie/Engine/
// Application/StringInquirer classes — railties already registered those and the
// "rails" provided feature. It extends the existing Rails module in place with
// the singleton accessors, adds Rails::VERSION and Rails::EnvironmentInquirer, and
// wires `require "rails/all"` to pull in every shipped framework component.
//
// The application-state accessors (root / public_path / configuration /
// autoloaders / cache) delegate to the meta-gem, which resolves them against the
// railties Application registered through rails.SetApplication — the App seam
// bridged here by railsAppAdapter. Rails.application itself keeps returning the
// Ruby Application wrapper (a *RailsAppVal) that Ruby set.
func (vm *VM) registerRails() {
// registerRailties (which runs first) already created the Rails module and its
// Railtie/Engine/Application/StringInquirer classes; this binding extends that
// module in place rather than recreating it.
mod := vm.consts["Rails"].(*RClass)

def := func(name string, fn func(*VM, object.Value, []object.Value, *Proc) object.Value) {
mod.smethods[name] = &Method{name: name, owner: mod, native: fn}
}

// Rails.application / Rails.application= — the getter returns the Ruby
// Application wrapper Ruby registered; the setter records it and threads the
// underlying railties Application into the meta-gem's App seam so the
// delegating accessors resolve against it. Assigning nil clears the app.
def("application", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.railsApp == nil {
return object.NilV
}
return vm.railsApp
})
def("application=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
if object.IsNil(args[0]) {
vm.railsApp = nil
rails.SetApplication(nil)
return object.NilV
}
av, ok := args[0].(*RailsAppVal)
if !ok {
raise("TypeError", "wrong argument type %s (expected a Rails::Application)", vm.classOf(args[0]).name)
}
vm.railsApp = av
rails.SetApplication(railsAppAdapter{app: av.app})
return args[0]
})

// Rails.env / Rails.env= — the current environment as an EnvironmentInquirer
// (resolved from RAILS_ENV, then RACK_ENV, then "development", by the meta-gem).
def("env", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return &RailsEnvVal{e: rails.Env(), cls: vm.consts["Rails::EnvironmentInquirer"].(*RClass)}
})
def("env=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
e := rails.SetEnv(railsGroupStr(rackArg(args)))
return &RailsEnvVal{e: e, cls: vm.consts["Rails::EnvironmentInquirer"].(*RClass)}
})

// Rails.root / Rails.public_path — filesystem paths delegated to the bound
// application (nil before an application is registered, as in Ruby).
def("root", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.railsApp == nil {
return object.NilV
}
return railsStr(rails.Root())
})
def("public_path", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.railsApp == nil {
return object.NilV
}
return railsStr(rails.PublicPath())
})

// Rails.configuration — the application's configuration object, resolved
// through the meta-gem's App seam and re-wrapped as the Ruby configuration; nil
// before an application is registered (as in Ruby).
def("configuration", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.railsApp == nil {
return object.NilV
}
appCfg := rails.Configuration().(*railties.ApplicationConfiguration)
return &RailsConfigVal{
app: appCfg,
eng: appCfg.EngineConfiguration,
cfg: appCfg.EngineConfiguration.Configuration,
cls: vm.consts["Rails::Railtie::Configuration"].(*RClass),
}
})

// Rails.autoloaders / Rails.logger / Rails.cache — opaque any-typed slots the
// meta-gem stores; a Ruby value set through the setter round-trips, otherwise
// nil. Cache falls back to the application's store (railties vends none, so
// nil) when no explicit store was set.
def("autoloaders", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return railsAny(rails.Autoloaders())
})
def("logger", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return railsAny(rails.Logger())
})
def("logger=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
rails.SetLogger(rackArg(args))
return rackArg(args)
})
def("cache", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return railsAny(rails.Cache())
})
def("cache=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
rails.SetCache(rackArg(args))
return rackArg(args)
})

// Rails.groups(*extra) — the ordered, de-duplicated group list for the current
// environment (default, env, extras, RAILS_GROUPS), as Ruby Strings.
def("groups", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
extra := make([]string, len(args))
for i, a := range args {
extra[i] = railsGroupStr(a)
}
return thorStrArray(rails.Groups(extra...))
})

// Rails.version / Rails.gem_version — the targeted Rails release as a String.
def("version", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return railsStr(rails.Version())
})
def("gem_version", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return railsStr(rails.GemVersion())
})

// Rails.backtrace_cleaner / Rails.error — the shared ActiveSupport singletons
// (BacktraceCleaner / ErrorReporter). Those concrete objects are owned by
// go-ruby-activesupport and not yet surfaced as Ruby classes, so the accessors
// return nil until that binding lands.
def("backtrace_cleaner", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NilV
})
def("error", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NilV
})

vm.registerRailsVersion(mod)
vm.registerRailsEnvironmentInquirer(mod)

// require "rails/all" pulls in every shipped framework component. The
// featureHooks map is already created by registerPrime (which runs first).
vm.featureHooks["rails/all"] = vm.installRailsAll
}

// registerRailsVersion installs Rails::VERSION, the module mirroring Ruby's
// Rails::VERSION with its STRING / MAJOR / MINOR / TINY / PRE constants.
func (vm *VM) registerRailsVersion(mod *RClass) {
ver := newClass("Rails::VERSION", nil)
ver.isModule = true
ver.consts["STRING"] = object.NewString(rails.VERSION.STRING())
ver.consts["MAJOR"] = object.IntValue(int64(rails.VERSION.Major))
ver.consts["MINOR"] = object.IntValue(int64(rails.VERSION.Minor))
ver.consts["TINY"] = object.IntValue(int64(rails.VERSION.Tiny))
ver.consts["PRE"] = railsPre(rails.VERSION.Pre)
mod.consts["VERSION"] = ver
vm.consts["Rails::VERSION"] = ver
}

// registerRailsEnvironmentInquirer installs Rails::EnvironmentInquirer, the
// Rails::StringInquirer subclass Rails.env returns: the generic `name?` predicate
// (via method_missing) plus the environment-specific `local?` (development/test),
// and the String coercions (to_str / ==).
func (vm *VM) registerRailsEnvironmentInquirer(mod *RClass) {
super := vm.consts["Rails::StringInquirer"].(*RClass)
c := newClass("Rails::EnvironmentInquirer", super)
mod.consts["EnvironmentInquirer"] = c
vm.consts["Rails::EnvironmentInquirer"] = c

envOf := func(self object.Value) *RailsEnvVal { return self.(*RailsEnvVal) }

c.define("to_str", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return railsStr(envOf(self).e.String())
})
c.define("==", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
return object.Bool(envOf(self).e.String() == railsGroupStr(rackArg(args)))
})
c.define("local?", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(envOf(self).e.Local())
})
c.define("method_missing", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
name := railsMissingName(args)
if n := len(name); n > 0 && name[n-1] == '?' {
return object.Bool(envOf(self).e.Is(name[:n-1]))
}
raise("NoMethodError", "undefined method '%s' for an instance of Rails::EnvironmentInquirer", name)
return object.NilV
})
}

// installRailsAll is the rails/all feature hook: `require "rails/all"` loads the
// whole framework, so it triggers a require of each shipped (Available) component
// whose feature the VM already provides. Components without a provided feature yet
// (their bindings not landed) are skipped, so the aggregate tracks the catalog's
// Available set as component bindings arrive.
func (vm *VM) installRailsAll() {
for _, c := range rails.AvailableComponents() {
feat := railsComponentFeature(c.Name)
if providedFeatures[feat] {
vm.doRequire(feat, false)
}
}
}
100 changes: 100 additions & 0 deletions internal/vm/rails_meta_bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
"strings"

rails "github.com/go-ruby-rails/rails"
railties "github.com/go-ruby-railties/railties"

"github.com/go-embedded-ruby/ruby/internal/object"
)

// This file is the value-model + seam half of the `rails` meta-gem binding
// (require "rails/all" and the top-level Rails.* accessors); rails_meta.go holds
// the module accessor + class registration. The meta-gem itself
// (github.com/go-ruby-rails/rails, package `rails`) ships almost no code of its
// own: it vends the top-level Rails module (Rails.application / env / root /
// logger / cache / configuration / …), the Rails::VERSION constant and the
// machine-readable component catalog, delegating application state to the
// railties Application through its App seam. This binding layers those onto the
// Rails module the railties binding already created (registerRailties), never
// re-creating it.

// RailsEnvVal wraps a rails.EnvironmentInquirer (Rails::EnvironmentInquirer, a
// Rails::StringInquirer subclass): the value Rails.env returns so `env.production?`
// and `env.local?` read naturally.
type RailsEnvVal struct {
e rails.EnvironmentInquirer
cls *RClass
}

func (v *RailsEnvVal) ToS() string { return v.e.String() }
func (v *RailsEnvVal) Inspect() string { return "\"" + v.e.String() + "\"" }
func (v *RailsEnvVal) Truthy() bool { return true }

// railsAppAdapter adapts a railties Application to the meta-gem's rails.App seam,
// so the top-level Rails.root / Rails.public_path / Rails.configuration /
// Rails.autoloaders / Rails.cache accessors — which the meta-gem resolves through
// rails.SetApplication — delegate to the bound railties application. The
// configuration/autoloaders/cache are opaque to the meta-gem (typed as any);
// railties vends a typed configuration but no autoloader set or cache store, so
// those return nil (mirroring the pre-boot Ruby result).
type railsAppAdapter struct{ app *railties.Application }

func (a railsAppAdapter) Root() string { return a.app.Paths().Root() }
func (a railsAppAdapter) Config() any { return a.app.Config() }
func (a railsAppAdapter) Autoloaders() any { return nil }
func (a railsAppAdapter) Cache() any { return nil }

// PublicPath returns the first expanded entry of paths["public"], mirroring
// Rails.public_path; it is empty when the application declares no public path.
func (a railsAppAdapter) PublicPath() string {
if p := a.app.Paths().Get("public"); p != nil {
if ex := p.Expanded(); len(ex) > 0 {
return ex[0]
}
}
return ""
}

// railsAny converts one of the meta-gem's opaque any-typed slots (logger, cache,
// autoloaders) back to a Ruby value: a stored object.Value passes through, and a
// nil (nothing set, or a slot railties does not vend) becomes Ruby nil.
func railsAny(v any) object.Value {
if rv, ok := v.(object.Value); ok {
return rv
}
return object.NilV
}

// railsPre renders the Rails::VERSION::PRE constant: the pre-release segment as a
// String, or nil when the release is final (Pre == ""), mirroring Ruby's PRE.
func railsPre(s string) object.Value {
if s == "" {
return object.NilV
}
return object.NewString(s)
}

// railsComponentFeature maps a catalog component name to the Ruby feature name
// `require "rails/all"` loads for it (the component gem's canonical require path):
// railties is reached through "rails", actionpack through "action_controller",
// and every active*/action* component through its underscored form.
func railsComponentFeature(name string) string {
switch name {
case "railties":
return "rails"
case "actionpack":
return "action_controller"
}
for _, p := range []string{"active", "action"} {
if rest := strings.TrimPrefix(name, p); rest != name {
return p + "_" + rest
}
}
return name
}
Loading
Loading