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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ require (
github.com/go-ruby-prime/prime v0.0.0-20260629151744-aa26747e21a8
github.com/go-ruby-pstore/pstore v0.0.0-20260630081017-0dd55a12f94e
github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260630151503-f308d4002444
github.com/go-ruby-rack/rack v0.0.0-20260630124607-8a4f9cfcdbb5
github.com/go-ruby-redis/redis v0.0.0-20260701125752-5de216f6ad92
github.com/go-ruby-regexp/regexp v0.0.0-20260701043846-fa3d6604e1ee
github.com/go-ruby-resolv/resolv v0.0.0-20260629153520-df410a5796ac
Expand All @@ -76,6 +77,7 @@ require (
github.com/go-ruby-sequel/sequel v0.0.0-20260702151352-66413b601977
github.com/go-ruby-set/set v0.0.0-20260630204346-ab6a8a92a910
github.com/go-ruby-shellwords/shellwords v0.0.0-20260629114104-e941e4210818
github.com/go-ruby-sinatra/sinatra v0.0.0-20260630133746-2c894e9d172c
github.com/go-ruby-slim/slim v0.0.0-20260701141524-ade9ddf6aec4
github.com/go-ruby-sqlite3/sqlite3 v0.0.0-20260702143910-c9f337771f41
github.com/go-ruby-strscan/strscan v0.0.0-20260701044334-d0cc926643a8
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/go-ruby-pstore/pstore v0.0.0-20260630081017-0dd55a12f94e h1:JmbJL+0nq
github.com/go-ruby-pstore/pstore v0.0.0-20260630081017-0dd55a12f94e/go.mod h1:tbYmHYXjnNXJCNYABDx9nWfuGBCXeKrp134bNsQeTpM=
github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260630151503-f308d4002444 h1:vkMPJDpZHmu09pVxr7CFppoh1b5Kc8LkhKpxkSBoyxA=
github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260630151503-f308d4002444/go.mod h1:Wq3cgP6klsULbO4ee0DOavVIzifgmddOWnYpqljbER4=
github.com/go-ruby-rack/rack v0.0.0-20260630124607-8a4f9cfcdbb5 h1:8jrY0g3k1+TbiXz7jV8sXP8UcXSlB1FKcXWkyXZPBQ8=
github.com/go-ruby-rack/rack v0.0.0-20260630124607-8a4f9cfcdbb5/go.mod h1:rKZ3vD81gJq61NvZ3XX9cr6AxtIV3+lFZKG7lPKV3PM=
github.com/go-ruby-redis/redis v0.0.0-20260701125752-5de216f6ad92 h1:EXqNsTpyz5KPZURl94uOjcIIyJ0mCyMA7/AFAXvFgdg=
github.com/go-ruby-redis/redis v0.0.0-20260701125752-5de216f6ad92/go.mod h1:gt67ft80L9J5DpRGe+x3qPy2hg1g75aWQ6h52inm7og=
github.com/go-ruby-regexp/regexp v0.0.0-20260701043846-fa3d6604e1ee h1:ctauEAdAq129WCrU+dndYcOjNlo1Y94vWNBtgXp1heE=
Expand All @@ -160,6 +162,8 @@ github.com/go-ruby-set/set v0.0.0-20260630204346-ab6a8a92a910 h1:Q41u365suJ1TlrX
github.com/go-ruby-set/set v0.0.0-20260630204346-ab6a8a92a910/go.mod h1:F/P0PXeE5ybEPeWYfb1JAKWny1Li3dnlm6YxG2/vffE=
github.com/go-ruby-shellwords/shellwords v0.0.0-20260629114104-e941e4210818 h1:z9b99kWJht8AgtlIbdiq7Y02j6eddxW+rr4MJpCS6kw=
github.com/go-ruby-shellwords/shellwords v0.0.0-20260629114104-e941e4210818/go.mod h1:gJ+9ba+y8+DKenwjZKbTLVSuX6Xp7V5R7pZ6NwtD6x0=
github.com/go-ruby-sinatra/sinatra v0.0.0-20260630133746-2c894e9d172c h1:zq73EWvSARNK2T1R0WC3jl+YH7g6TutOkWd1btI8fIU=
github.com/go-ruby-sinatra/sinatra v0.0.0-20260630133746-2c894e9d172c/go.mod h1:nuEOODfga3YvXINu2Ckvp/dNTTZlx+LUyjpWI+9TMck=
github.com/go-ruby-slim/slim v0.0.0-20260701141524-ade9ddf6aec4 h1:FYfHAxVaFU2q7BQ8wsKesUxO2uAQ2VK/TdTVut1/wfg=
github.com/go-ruby-slim/slim v0.0.0-20260701141524-ade9ddf6aec4/go.mod h1:+y2RCJmyKesyDRfmIlOSHFbBzTgEIpch7rqkFqPunAw=
github.com/go-ruby-sqlite3/sqlite3 v0.0.0-20260702143910-c9f337771f41 h1:KHSs2oPQsdj/VLap1eXUPWrlO6x4zyZlYoqga/rKavs=
Expand Down
2 changes: 2 additions & 0 deletions internal/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,9 @@ func (vm *VM) bootstrap() {
vm.registerNokogiriXSLT() // Nokogiri::XSLT(str) -> Stylesheet#transform/apply_to (require "nokogiri"), backed by go-ruby-xslt over go-ruby-nokogiri; needs registerNokogiri first
vm.registerRSpec() // RSpec matcher + expect surface (require "rspec"), backed by go-ruby-rspec; needs Exception for ExpectationNotMetError
vm.registerRuboCop() // RuboCop::Runner#inspect/autocorrect + Cop::Offense (require "rubocop"), backed by go-ruby-rubocop over go-ruby-parser; needs StandardError for RuboCop::Error
vm.registerRack() // Rack::Request/Response/Utils (require "rack" / "rack/utils"), backed by go-ruby-rack; deterministic env/query/escape, no socket
vm.registerGrape() // Grape::Router/Validator/Formatter (require "grape"), backed by go-ruby-grape; endpoint-block exec + Rack env are host seams; needs StandardError for Grape::Exceptions
vm.registerSinatra() // Sinatra::Base class-DSL + #call Rack adapter (require "sinatra/base"), backed by go-ruby-sinatra over go-ruby-rack; route/filter block eval is the rbgo seam; needs Rack + StandardError (Sinatra::NotFound), 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
vm.registerRQRCode() // RQRCode::QRCode (require "rqrcode"), backed by go-ruby-rqrcode; needs StandardError for RQRCode::QRCode*Error
vm.registerDotenv() // Dotenv module (require "dotenv"), backed by go-ruby-dotenv; wires ENV read/write + shell seams
Expand Down
10 changes: 10 additions & 0 deletions internal/vm/object_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,16 @@ func (vm *VM) classOf(v object.Value) *RClass {
return vm.cCSVRow
case *CSVTable:
return vm.cCSVTable
case *RackRequest:
// A Rack::Request wrapper reports the class stamped on it at construction.
return x.cls
case *RackResponse:
return x.cls
case *SinatraCtx:
// The self a Sinatra route/filter block runs against.
return x.cls
case *SinatraSettings:
return x.cls
case *REXMLDocument:
return vm.cREXMLDocument
case *REXMLElement:
Expand Down
309 changes: 309 additions & 0 deletions internal/vm/rack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import (
rack "github.com/go-ruby-rack/rack"

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

// rackRelease is the Rack::RELEASE version string the binding advertises. Rack
// is a wire/contract library, so RELEASE is the SPEC generation it implements
// (Rack 3), not the go-ruby-rack module version.
const rackRelease = "3.1.0"

// RackRequest is the Ruby wrapper around a *rack.Request — the read-mostly view
// over a Rack env (Rack::Request). The env parsing, query decoding and header
// normalisation all live in github.com/go-ruby-rack/rack; this shell exposes the
// accessor surface (#path_info / #request_method / #params / …) to Ruby. The
// wrapper carries its own class so classOf reports Rack::Request.
type RackRequest struct {
req *rack.Request
cls *RClass
}

func (r *RackRequest) ToS() string { return "#<Rack::Request>" }
func (r *RackRequest) Inspect() string { return r.ToS() }
func (r *RackRequest) Truthy() bool { return true }

// RackResponse is the Ruby wrapper around a *rack.Response — the buffered
// status/headers/body a handler assembles (Rack::Response), yielding the SPEC
// [status, headers, body] triple through #finish / #to_a.
type RackResponse struct {
resp *rack.Response
cls *RClass
}

func (r *RackResponse) ToS() string { return "#<Rack::Response>" }
func (r *RackResponse) Inspect() string { return r.ToS() }
func (r *RackResponse) Truthy() bool { return true }

// registerRack installs the Rack module and its Request / Response value objects
// plus the Rack::Utils escaping/query surface (require "rack" / "rack/utils"):
// Rack::Request.new(env) exposes the request accessors over go-ruby-rack;
// Rack::Response.new(body, status, headers) buffers a response and #finish
// returns the [status, headers, body] triple a Rack server consumes;
// Rack::Utils.escape / unescape / escape_html / parse_query / build_query bind
// straight through to the library so encoding comes from one authoritative
// source. The whole contract is deterministic Go — no socket, no network.
func (vm *VM) registerRack() {
mod := newClass("Rack", nil)
mod.isModule = true
vm.consts["Rack"] = mod

mod.consts["RELEASE"] = object.NewString(rackRelease)

vm.registerRackRequest(mod)
vm.registerRackResponse(mod)
vm.registerRackUtils(mod)
}

// registerRackRequest installs Rack::Request and its accessor methods.
func (vm *VM) registerRackRequest(mod *RClass) {
cls := newClass("Rack::Request", vm.cObject)
mod.consts["Request"] = cls
vm.consts["Rack::Request"] = cls

cls.smethods["new"] = &Method{name: "new", owner: cls, native: 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)")
}
return &RackRequest{req: rack.NewRequest(rackEnv(args[0])), cls: cls}
}}

self := func(v object.Value) *rack.Request { return v.(*RackRequest).req }

// String accessors straight off the env.
str := func(fn func(*rack.Request) string) NativeFn {
return func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(fn(self(v)))
}
}
cls.define("request_method", str((*rack.Request).RequestMethod))
cls.define("path_info", str((*rack.Request).PathInfo))
cls.define("script_name", str((*rack.Request).ScriptName))
cls.define("query_string", str((*rack.Request).QueryString))
cls.define("server_name", str((*rack.Request).ServerName))
cls.define("server_port", str((*rack.Request).ServerPort))
cls.define("content_type", str((*rack.Request).ContentType))
cls.define("media_type", str((*rack.Request).MediaType))
cls.define("scheme", str((*rack.Request).Scheme))
cls.define("host", str((*rack.Request).Host))
cls.define("base_url", str((*rack.Request).BaseURL))
cls.define("path", str((*rack.Request).Path))
cls.define("fullpath", str((*rack.Request).Fullpath))
cls.define("url", str((*rack.Request).URL))
cls.define("ip", str((*rack.Request).IP))

// Boolean predicates.
boolean := func(fn func(*rack.Request) bool) NativeFn {
return func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(fn(self(v)))
}
}
cls.define("get?", boolean((*rack.Request).IsGet))
cls.define("post?", boolean((*rack.Request).IsPost))
cls.define("put?", boolean((*rack.Request).IsPut))
cls.define("patch?", boolean((*rack.Request).IsPatch))
cls.define("delete?", boolean((*rack.Request).IsDelete))
cls.define("head?", boolean((*rack.Request).IsHead))
cls.define("options?", boolean((*rack.Request).IsOptions))
cls.define("xhr?", boolean((*rack.Request).XHR))
cls.define("ssl?", boolean((*rack.Request).SSL))

cls.define("port", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Integer(int64(self(v).Port()))
})

// #get_header(name) returns the raw env entry (or nil).
cls.define("get_header", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
raw, ok := self(v).GetHeaderRaw(rackStr(args[0]))
if !ok {
return object.NilV
}
return rackFromGo(raw)
})
cls.define("has_header?", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
return object.Bool(self(v).HasHeader(rackStr(args[0])))
})

// Parsed params as Ruby Hashes.
cls.define("params", rackParamsMethod((*rack.Request).Params))
cls.define("GET", rackParamsMethod((*rack.Request).GET))
cls.define("POST", rackParamsMethod((*rack.Request).POST))
cls.define("cookies", func(vm *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return rackParamsToHash(self(v).Cookies())
})
}

// rackParamsMethod adapts a (*rack.Request) accessor returning (*Params, error)
// into a native method returning a Ruby Hash, raising on a parse error.
func rackParamsMethod(fn func(*rack.Request) (*rack.Params, error)) NativeFn {
return func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
p, err := fn(v.(*RackRequest).req)
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return rackParamsToHash(p)
}
}

// registerRackResponse installs Rack::Response and its buffering surface.
func (vm *VM) registerRackResponse(mod *RClass) {
cls := newClass("Rack::Response", vm.cObject)
mod.consts["Response"] = cls
vm.consts["Rack::Response"] = cls

cls.smethods["new"] = &Method{name: "new", owner: cls, native: func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
body := rackResponseBody(args)
status := 200
if len(args) > 1 {
status = rackInt(args[1], 200)
}
var headers *rack.Headers
if len(args) > 2 {
headers = rackHeadersFrom(args[2])
}
return &RackResponse{resp: rack.NewResponse(body, status, headers), cls: cls}
}}

self := func(v object.Value) *rack.Response { return v.(*RackResponse).resp }

cls.define("write", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
chunk := rackStr(rackArg(args))
self(v).Write(chunk)
return object.NewString(chunk)
})
cls.define("status", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Integer(int64(self(v).Status()))
})
cls.define("status=", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
self(v).SetStatus(rackInt(rackArg(args), 200))
return rackArg(args)
})
cls.define("headers", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return rackHeadersToHash(self(v).Headers())
})
cls.define("[]", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
return rackFromGo(self(v).GetHeader(rackStr(args[0])))
})
cls.define("[]=", func(_ *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))
}
self(v).SetHeader(rackStr(args[0]), rackStr(args[1]))
return args[1]
})
cls.define("set_header", func(_ *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))
}
self(v).SetHeader(rackStr(args[0]), rackStr(args[1]))
return args[1]
})
cls.define("content_type", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(self(v).ContentType())
})
cls.define("content_type=", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
self(v).SetContentType(rackStr(rackArg(args)))
return rackArg(args)
})
cls.define("location", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(self(v).Location())
})
cls.define("redirect", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1..2)")
}
status := 302
if len(args) > 1 {
status = rackInt(args[1], 302)
}
self(v).Redirect(rackStr(args[0]), status)
return object.NilV
})
cls.define("body", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return rackBodyArray(self(v).Body())
})
cls.define("empty?", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(self(v).Empty())
})
cls.define("finish", rackFinishMethod(self))
cls.define("to_a", rackFinishMethod(self))
cls.define("redirect?", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(self(v).IsRedirect())
})
cls.define("ok?", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(self(v).OK())
})
cls.define("not_found?", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.Bool(self(v).NotFound())
})
}

// rackFinishMethod returns the [status, headers, body] triple as a Ruby Array,
// the SPEC form (#finish / #to_a).
func rackFinishMethod(self func(object.Value) *rack.Response) NativeFn {
return func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
status, headers, body := self(v).Finish()
return &object.Array{Elems: []object.Value{
object.Integer(int64(status)),
rackHeadersToHash(headers),
rackBodyArray(body),
}}
}
}

// registerRackUtils installs the Rack::Utils module of pure encoding/query
// helpers (also the target of require "rack/utils").
func (vm *VM) registerRackUtils(mod *RClass) {
util := newClass("Rack::Utils", nil)
util.isModule = true
mod.consts["Utils"] = util
vm.consts["Rack::Utils"] = util

def := func(name string, fn NativeFn) { util.smethods[name] = &Method{name: name, owner: util, native: fn} }

strFn := func(fn func(string) string) NativeFn {
return func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fn(rackStr(rackArg(args))))
}
}
def("escape", strFn(rack.Escape))
def("escape_path", strFn(rack.EscapePath))
def("escape_html", strFn(rack.EscapeHTML))
def("unescape_html", strFn(rack.UnescapeHTML))
def("unescape_path", strFn(rack.UnescapePath))
def("unescape", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
s, err := rack.Unescape(rackStr(rackArg(args)))
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return object.NewString(s)
})
def("parse_query", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p, err := rack.ParseQuery(rackStr(rackArg(args)), "&")
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return rackParamsToHash(p)
})
def("build_query", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(rack.BuildQuery(rackParamsFromHash(rackArg(args))))
})
def("status_code", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.Integer(int64(rackInt(rackArg(args), 500)))
})
}
Loading
Loading