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
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ require (
github.com/go-ruby-abbrev/abbrev v0.0.0-20260629150957-97117892cd38
github.com/go-ruby-acme/acme v0.0.0-20260704112859-415fad2a4cbe
github.com/go-ruby-actioncable/actioncable v0.0.0-20260706115409-19f2e354a783
github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260706115414-f116fa6c4917
github.com/go-ruby-actionpack/actionpack v0.0.0-20260706115418-8ba13fbf84e5
github.com/go-ruby-actionview/actionview v0.0.0-20260706115423-0e293eed051a
github.com/go-ruby-activejob/activejob v0.0.0-20260706172137-830f5bbe94b4
github.com/go-ruby-activemodel/activemodel v0.0.0-20260706115433-f36d70b64b7a
github.com/go-ruby-activerecord/activerecord v0.0.0-20260702222646-da57bd9e07f6
Expand Down Expand Up @@ -108,6 +111,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
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ github.com/go-ruby-acme/acme v0.0.0-20260704112859-415fad2a4cbe h1:oYFDl+kcAkTOX
github.com/go-ruby-acme/acme v0.0.0-20260704112859-415fad2a4cbe/go.mod h1:Bh8Vr5Vy9UO7JqU5f6myUOWl0jjnnvtlGdIVTBbdLRw=
github.com/go-ruby-actioncable/actioncable v0.0.0-20260706115409-19f2e354a783 h1:VAFOmKa1u2HKA2LFQhelusPVIAtgp+wAOkXl8IjURbQ=
github.com/go-ruby-actioncable/actioncable v0.0.0-20260706115409-19f2e354a783/go.mod h1:5DS3icNZKImRwvDP7Kf9IHv+9d6EYtcvRpBALgh/s/Y=
github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260706115414-f116fa6c4917 h1:f1xzBCccdUn49fRLh8XBn/d9OHpcrNa46w6aW0hGjyY=
github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260706115414-f116fa6c4917/go.mod h1:CgixBW5KpbPEx+8SCZJQbeVa2WLNB7ObGRRCsjDEY+Y=
github.com/go-ruby-actionpack/actionpack v0.0.0-20260706115418-8ba13fbf84e5 h1:+CxMtA9K0j7BWiWdq+/rMfgeWjViJStyfVKcvMlDSvA=
github.com/go-ruby-actionpack/actionpack v0.0.0-20260706115418-8ba13fbf84e5/go.mod h1:3lXE8Ou6UitS4sOxQhn/KCI6eKLdREUcFxsky4oMEbk=
github.com/go-ruby-actionview/actionview v0.0.0-20260706115423-0e293eed051a h1:NhatSlzDFDTNAIYng5hhUltGPS0O5xiPr5efQ+RsL9k=
github.com/go-ruby-actionview/actionview v0.0.0-20260706115423-0e293eed051a/go.mod h1:n9SEnqxWmYIXKgtrIZEiRvGui0lWdCd2GJELRpa44ls=
github.com/go-ruby-activejob/activejob v0.0.0-20260706172137-830f5bbe94b4 h1:vRJAWboG75rQWpfTiNQ8XkFRJ1Am9RNKdRKSlTLQBiA=
github.com/go-ruby-activejob/activejob v0.0.0-20260706172137-830f5bbe94b4/go.mod h1:HBZ9KJKoGTzo4h6/5+gaYyzMYWQtucT660mE68zHZGw=
github.com/go-ruby-activemodel/activemodel v0.0.0-20260706115433-f36d70b64b7a h1:d8kCh4DxIJEQJhd/ZD6niTnxEzWgnZ5NlOmxfdBRDZQ=
Expand Down Expand Up @@ -378,6 +384,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
305 changes: 305 additions & 0 deletions internal/vm/actionmailer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

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

// registerActionMailer installs the Action Mailer surface (require
// "action_mailer"), backed by the pure-Go engine of
// github.com/go-ruby-actionmailer/actionmailer:
//
// - ActionMailer::Base, the class every `class MyMailer < ActionMailer::Base`
// subclasses: the class-level DSL (default, delivery_method=,
// perform_deliveries=, raise_delivery_errors=, register_interceptor /
// register_observer), the shared deliveries Array, and the render/enqueue
// injection seams (renderer= / enqueuer=). Each mailer action method
// (`def welcome`) becomes callable as a class method via the method_added
// hook, returning an ActionMailer::MessageDelivery;
// - ActionMailer::MessageDelivery, the lazy proxy `MyMailer.welcome(user)`
// returns: deliver_now / deliver_later / message;
// - the mailer instance surface a `def welcome` body runs against: mail(...),
// attachments (attachments[name] = data / attachments.inline[name] = data)
// and headers(hash).
//
// The message composition, the multipart MIME assembly, the delivery-method
// registry and the interceptor/observer hooks live in the library; this file is
// the class and method wiring, and actionmailer_bind.go holds the four Ruby
// seams (Action, RenderBody, DeliveryMethod, EnqueueJob) and the Ruby <->
// actionmailer value conversions. Every seam runs INLINE on the VM goroutine
// under the GVL, so a mailer action composes synchronously and (with the default
// :test delivery) deliver_now appends to ActionMailer::Base.deliveries.
func (vm *VM) registerActionMailer() {
mod := newClass("ActionMailer", nil)
mod.isModule = true
vm.consts["ActionMailer"] = mod

vm.amDeliveries = object.NewArrayFromSlice(nil)

base := newClass("ActionMailer::Base", vm.cObject)
mod.consts["Base"] = base
vm.consts["ActionMailer::Base"] = base
vm.cActionMailerBase = base

del := newClass("ActionMailer::MessageDelivery", vm.cObject)
mod.consts["MessageDelivery"] = del
vm.consts["ActionMailer::MessageDelivery"] = del
vm.cActionMailerDelivery = del
vm.registerActionMailerDelivery(del)

// An internal proxy class for the `attachments` accessor (mirroring
// Mail::AttachmentsList); not a documented constant, but carried on the value
// so classOf reports it.
att := newClass("ActionMailer::Base::Attachments", vm.cObject)
base.consts["Attachments"] = att
vm.cActionMailerAttachments = att
vm.registerActionMailerAttachments(att)

vm.registerActionMailerDSL(base)
vm.registerActionMailerInstance(base)
}

// registerActionMailerDSL installs the class-level Action Mailer DSL on
// ActionMailer::Base (inherited by every mailer subclass): the method_added hook
// that turns each mailer action method into a class method, the default /
// delivery / hook declarations, the shared deliveries accessor and the
// render/enqueue injection seams.
func (vm *VM) registerActionMailerDSL(base *RClass) {
sm := func(name string, fn NativeFn) {
base.smethods[name] = &Method{name: name, owner: base, native: fn}
}

// method_added(:name) fires for every instance method a mailer subclass
// defines; each becomes a class method that runs the action and returns a
// MessageDelivery (the analogue of Rails resolving action_methods lazily).
sm("method_added", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
if cls, ok := self.(*RClass); ok && len(args) > 0 {
vm.amDefineAction(cls, amKeyStr(args[0]))
}
return object.NilV
})

// default(key: value, …) records the class's default headers/params, merged
// down the ancestor chain at delivery time.
sm("default", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
d := vm.amDefOf(self.(*RClass))
if h := amTrailingHash(args); h != nil {
for _, k := range h.Keys {
v, _ := h.Get(k)
d.defaults[amKeyStr(k)] = v.ToS()
}
}
return object.NilV
})

// delivery_method = :test | a #deliver object.
sm("delivery_method=", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
choice := amArg(args)
vm.amValidateDelivery(choice)
vm.amDefOf(self.(*RClass)).delivery = choice
return choice
})
sm("delivery_method", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
if d := vm.amDefs[self.(*RClass)]; d != nil && d.delivery != nil {
return d.delivery
}
return object.Symbol("test")
})

// perform_deliveries= / raise_delivery_errors= gate delivery.
sm("perform_deliveries=", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
b := amArg(args).Truthy()
vm.amDefOf(self.(*RClass)).perform = &b
return amArg(args)
})
sm("perform_deliveries", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
if d := vm.amDefs[self.(*RClass)]; d != nil && d.perform != nil {
return object.Bool(*d.perform)
}
return object.Bool(true)
})
sm("raise_delivery_errors=", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
b := amArg(args).Truthy()
vm.amDefOf(self.(*RClass)).raiseErrors = &b
return amArg(args)
})
sm("raise_delivery_errors", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
if d := vm.amDefs[self.(*RClass)]; d != nil && d.raiseErrors != nil {
return object.Bool(*d.raiseErrors)
}
return object.Bool(true)
})

// register_interceptor / register_observer add a delivery hook.
sm("register_interceptor", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
d := vm.amDefOf(self.(*RClass))
d.interceptors = append(d.interceptors, amArg(args))
return object.NilV
})
sm("register_observer", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
d := vm.amDefOf(self.(*RClass))
d.observers = append(d.observers, amArg(args))
return object.NilV
})

// deliveries / deliveries= expose the shared test-delivery sink.
sm("deliveries", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return vm.amDeliveries
})
sm("deliveries=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
if arr, ok := amArg(args).(*object.Array); ok {
vm.amDeliveries = arr
} else {
vm.amDeliveries = object.NewArrayFromSlice(nil)
}
return vm.amDeliveries
})

// renderer= / enqueuer= inject the Action View and Active Job seams (wired to
// the bound go-ruby-actionview / go-ruby-activejob in the consolidated binary,
// or a stub in tests).
sm("renderer=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
vm.amRenderer = amArg(args)
return vm.amRenderer
})
sm("renderer", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.amRenderer == nil {
return object.NilV
}
return vm.amRenderer
})
sm("enqueuer=", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
vm.amEnqueuer = amArg(args)
return vm.amEnqueuer
})
sm("enqueuer", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
if vm.amEnqueuer == nil {
return object.NilV
}
return vm.amEnqueuer
})
}

// amDefineAction installs the class method that runs a mailer action: called on a
// mailer class it processes the action with the given arguments and returns the
// ActionMailer::MessageDelivery proxy. Defined once per name (a redefinition of
// the same action keeps the first, which dispatches to the current instance
// method anyway).
func (vm *VM) amDefineAction(cls *RClass, name string) {
if _, exists := cls.smethods[name]; exists {
return
}
cls.smethods[name] = &Method{name: name, owner: cls, native: func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
return vm.amProcess(self.(*RClass), name, args)
}}
}

// registerActionMailerInstance installs the mailer instance surface a `def
// welcome` body runs against: mail(...), attachments and headers(hash). Each
// resolves the library *Mailer bound to the running action.
func (vm *VM) registerActionMailerInstance(base *RClass) {
base.define("mail", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
m := vm.amMailerFor(self)
if err := m.Mail(vm.amMailOptions(args)); err != nil {
return vm.amRaise(err)
}
return object.NilV
})
base.define("attachments", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return &ActionMailerAttachments{a: vm.amMailerFor(self).Attachments(), cls: vm.cActionMailerAttachments}
})
base.define("headers", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
m := vm.amMailerFor(self)
if h := amTrailingHash(args); h != nil {
hdr := map[string]string{}
for _, k := range h.Keys {
v, _ := h.Get(k)
hdr[amKeyStr(k)] = v.ToS()
}
m.Headers(hdr)
}
return object.NilV
})
}

// registerActionMailerDelivery installs the ActionMailer::MessageDelivery proxy
// surface: deliver_now / deliver (immediate), deliver_later (through the enqueue
// seam) and message (the composed Mail::Message). A composition or delivery error
// captured on the delivery re-raises here as its Ruby exception.
func (vm *VM) registerActionMailerDelivery(cls *RClass) {
self := func(v object.Value) *ActionMailerDelivery { return v.(*ActionMailerDelivery) }

deliverNow := func(vm *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
d := self(v).d
if err := d.DeliverNow(); err != nil {
return vm.amRaise(err)
}
msg, _ := d.Message()
return amMessageValue(msg)
}
cls.define("deliver_now", deliverNow)
cls.define("deliver", deliverNow)

cls.define("deliver_later", func(vm *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
if err := self(v).d.DeliverLater(); err != nil {
return vm.amRaise(err)
}
return object.NilV
})
cls.define("message", func(vm *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
msg, err := self(v).d.Message()
if err != nil {
return vm.amRaise(err)
}
return amMessageValue(msg)
})
}

// registerActionMailerAttachments installs the `attachments` proxy surface:
// attachments[name] = data (a regular attachment), attachments.inline (the inline
// sub-proxy) and attachments[name] (the stored bytes, or nil).
func (vm *VM) registerActionMailerAttachments(cls *RClass) {
self := func(v object.Value) *ActionMailerAttachments { return v.(*ActionMailerAttachments) }

cls.define("[]=", func(vm *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) < 2 {
raise("ArgumentError", "wrong number of arguments (given %d, expected 2)", len(args))
}
a := self(v)
name := args[0].ToS()
data, ct := amAttachmentData(args[1])
var at = a.a.Set(name, data)
if a.inline {
at = a.a.SetInline(name, data)
}
if ct != "" {
at.ContentType = ct
}
return args[1]
})
cls.define("[]", func(vm *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
at := self(v).a.Get(args[0].ToS())
if at == nil {
return object.NilV
}
return object.NewStringBytes(at.Data)
})
cls.define("inline", func(vm *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
a := self(v)
return &ActionMailerAttachments{a: a.a, inline: true, cls: a.cls}
})
}

// amArg returns the first argument, or Ruby nil when none was given.
func amArg(args []object.Value) object.Value {
if len(args) == 0 {
return object.NilV
}
return args[0]
}
Loading
Loading