Repository navigation
fix(sinatra): persist handler @ivars across before/route/after + MRI-identity oracle vs sinatra gem 4.2.1 - #157
Merged
Conversation
…oracle The routing/dispatch core of go-ruby-sinatra is already bound (#104). This lands two things a byte-for-byte diff against the real sinatra gem 4.2.1 surfaced: 1. Fix instance-variable persistence in the block-action bridge. Real Sinatra runs a request's before filter(s), route body and after filter(s) against a single instance, so an @ivar set in `before` (e.g. @user) is visible in the route and after blocks. The binding created a fresh SinatraCtx per action and SinatraCtx could not hold ivars at all (ivarTable returned nil for it), so `before { @uid = ... }` was silently dropped by the time the route ran. Give SinatraCtx an ivar table and reuse one SinatraCtx per request (keyed by the library's per-request *sinatra.Context, reset around each dispatch) so ivars persist within a request and do not leak across requests. 2. Add TestSinatraGemOracle: a run-conformance proof THROUGH rbgo that a real Sinatra app (apps/sinatra_oracle.rb: :name capture, query param, before-ivar, splat, optional param, redirect+Location, halt, custom error(code), custom not_found, after-filter) answers byte-for-byte identically to the sinatra gem 4.2.1. The golden vector is captured from that gem run and embedded so the suite stays hermetic (no ruby/gem at CI time). Refresh the stale webapp/doc.go (stage 3 now runs, not a gap). Two response headers are excluded from the oracle diff and flagged as go-ruby-sinatra LIBRARY follow-ups, not binding concerns: Content-Length (go-ruby-rack does not finalize it) and X-Cascade: pass on 404s. The default 404 HTML page is likewise a library default (empty body) vs the gem's styled page. 100% coverage on the binding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Jul 6, 2026
Close the templating half of the Sinatra binding (PR #157 landed routing/dispatch and deferred template rendering). A route/filter action runs against a SinatraCtx (self); the new `erb` helper compiles a template through the already-bound go-ruby-erb compiler and evaluates the resulting Ruby source in the handler's binding — self = the SinatraCtx, so the action's @Ivars and the request helpers (params, request, …) resolve, plus any `locals:` bound as local variables — capturing the _erbout buffer and returning the rendered String, which becomes the Rack response body. Render paths: * inline-String template (needs no filesystem) * :symbol template naming views/<sym>.erb, read via the File binding with the views dir taken from settings :views (default "./views") Trim mode "<>" and eoutvar "_erbout" match Tilt::ERBTemplate's defaults. <%= %> interpolates unescaped, <% %> is control flow. The eval-in-binding uses the front-end (parser + CompileWithLocals), so it is split open/closed: a closed-world binary raises NotImplementedError for `erb`, exactly as Binding#eval / Kernel#eval-on-source already do. Proof (through the rbgo binary, byte-for-byte vs sinatra gem 4.2.1): apps/sinatra_erb_oracle.rb + TestSinatraErbGemOracle exercise inline and :symbol/file templates, param + @ivar interpolation, control flow, positional and options[:locals] locals, and trim behaviour — all rendered identically to the reference gem. 100% coverage on the new binding code. Flagged follow-ups (not binding gaps): * Sinatra 4.x renders :erb via Tilt::ErubiTemplate (erubi); go-ruby-erb mirrors MRI's ERB::Compiler. With trim "<>" the two agree byte-for-byte for single-line and idiomatic multiline templates (a code-only <% %> alone on its line); they diverge only for the unusual line that both opens a code block and ends with an expression tag with trailing content. Full erubi trim-parity is a go-ruby-erb library follow-up. * layouts / partials * :erb escape_html * the Sinatra session store (also deferred by PR #157) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Jul 6, 2026
…r) (#169) * feat(sinatra): cookie session store (enable :sessions / session helper) Implement the Sinatra session seam PR #157 deferred: `enable :sessions` plus a working `session` helper backed by a signed cookie, matching MRI Sinatra's Rack::Session::Cookie semantics. On dispatch the session is loaded from the request's "rack.session" cookie (base64(Marshal.dump)--HMAC-SHA1(session_secret)); a missing, malformed or tampered cookie yields a fresh empty session. The `session` helper returns the live Hash so session[:k]=v and session.clear persist, and after the handler runs the (possibly mutated) session is committed back into a signed, HttpOnly, path=/ Set-Cookie. `set :session_secret, "…"` sets the key; without one a per-VM random default is used (like MRI's warning path). Serialization reuses the bound go-ruby-marshal engine, so the payload is byte-identical to MRI's default Base64::Marshal coder; base64 + HMAC-SHA1 match Rack's, making the cookie byte-exact to Rack::Session::Cookie given the same secret (proven by TestSinatraSessionCookieByteExact). The through-binary proof is the functional round-trip + tamper rejection (webapp stage 5 and the internal round-trip/tamper/clear tests), which an MRI Sinatra app reproduces observably. 100% coverage on the new binding code; go build/vet/test + conformance green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(conformance): add stage 5 (Sinatra cookie session) to the webapp map Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The go-ruby-sinatra routing/dispatch core is already bound into rbgo (#104): the
Sinatra::Baseclass-DSL (get/post/…),before/after,halt/redirect/status/content_type/headers/body/pass,not_found/error(code),set/enable/disable, params (query+captures+splat), inheritance and the Rack#call(env)adapter, with the Rubydo…endblock re-entering the evaluator as asinatra.Action— all at 100% coverage.Driving a real Sinatra app through the rbgo binary and diffing byte-for-byte against the real
sinatragem 4.2.1 (MRI ruby 4.0.5) surfaced one binding bug and produced a lasting MRI-identity proof. This PR lands both.1. Fix: instance variables lost across the filter chain
Real Sinatra runs a request's
beforefilter(s), the route body and theafterfilter(s) against one instance, so an@ivarset inbefore(e.g.@user) is visible in the route andafterblocks. The binding:SinatraCtxper action, andSinatraCtxcould not hold ivars at all —ivarTablereturnednilfor it, so@x =was silently dropped.Result:
before { @uid = "u42" }thenget("/x"){ "uid=#{@uid}" }produceduid=in rbgo vsuid=u42in the gem.Fix: give
SinatraCtxan ivar table (with a load-bearing lazy alloc — writing a nil map panics) and reuse oneSinatraCtxper request, keyed by the library's per-request*sinatra.Context(one per dispatch) and reset around each#callso ivars persist within a request and do not leak across requests.2. Proof: MRI-identity oracle through rbgo
TestSinatraGemOracle(scripts/conformance/webapp/) runsapps/sinatra_oracle.rbthrough rbgo and asserts the[status, headers, body]dump is byte-for-byte the response the sinatra gem 4.2.1 produced from the same app + request envs. Scenarios::namecapture, query param, before-filter ivar persistence, splat, optional param, redirect +Location,halt, customerror(500), customnot_found, after-filter header mutation. The golden vector is captured from the gem run and embedded, so the suite stays hermetic (no ruby/gem at CI time; runs under qemu on every 64-bit target).webapp/doc.gorefreshed — stage 3 now runs (was stale "gap: no sinatra binding").Scoped to follow-up (go-ruby-sinatra LIBRARY, not the binding)
Excluded from the oracle diff and flagged upstream — the binding is a faithful pass-through and cannot invent these:
Content-Length— the gem sets it; go-ruby-rack does not finalize it (matches for non-empty bodies, absent on empty/redirect).X-Cascade: passon 404s — the gem adds it to every 404; go-ruby-sinatra does not.not_foundbody is empty (a customnot_foundblock is byte-identical).Session store and template rendering (→ go-ruby-erb) remain documented seams, unchanged here.
Verification
GOWORK=off go build ./... && go vet ./... && go test ./...green.sinatraCtxFor) and theivarTable*SinatraCtxarm.🤖 Generated with Claude Code