action_pack_cr
Version, currently 0.2.1117 versions
- 0.2.11latestOct 22, 2026
- 0.2.10not indexedOct 22, 2026
- 0.2.9not indexedOct 22, 2026
- 0.2.8not indexedOct 22, 2026
- 0.2.7not indexedOct 22, 2026
- 0.2.6not indexedOct 22, 2026
- 0.2.5not indexedOct 22, 2026
- 0.2.4not indexedOct 22, 2026
- 0.2.3Oct 17, 2026
- 0.2.2not indexedOct 17, 2026
- 0.2.1not indexedOct 17, 2026
- 0.2.0not indexedOct 17, 2026
- 0.1.3not indexedOct 17, 2026
- 0.1.2not indexedOct 17, 2026
- 0.1.1not indexedOct 17, 2026
- 0.1.0not indexedOct 17, 2026
- master branchOct 12, 2026
github.com/OrelSokolov/action_pack.cr
No description declared in shard.yml.
Installation
# Add this to your shard.yml
dependencies:
action_pack_cr:
github: OrelSokolov/action_pack.cr
version: ~> 0.2.11Then run:
shards installshard.yml
- Crystal
>= 1.21.0
Dependencies
Runtime Dependencies
- erb_cr~> 0.1.2github: OrelSokolov/erb.cr
- action_view_cr~> 0.1.0github: OrelSokolov/action_view.cr
- attr_reader_cr~> 0.1.0github: OrelSokolov/attr_reader.cr
- active_support_cr~> 0.1.0github: OrelSokolov/active_support.cr
README
action_pack.cr
A Crystal port of Rails ActionPack 8.1.4 — controllers, routing, strong
parameters, cookies/session/flash, CSRF protection, HTTP caching and
streaming. Namespaces ActionController / ActionDispatch mirror the
original 1:1. Behavioral parity is verified against the real gem via
oracle specs (Ruby cases recorded with actionpack 8.1.4, replayed in
Crystal without edits).
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
action_pack_cr:
github: OrelSokolov/action_pack.cr
shards install
Requires Crystal >= 1.21.0. Works with active_support.cr, action_view.cr
and railties.cr (boot/middleware ladder).
Usage
class ApplicationController < ActionController::Base
end
class PostsController < ApplicationController
before_action :authenticate, only: [:index]
def index
@posts = Post.all
end
def create
post = Post.new(params.require(:post).permit(:title, :body))
if post.save
redirect_to posts_path, notice: "Created"
else
render :new, status: :unprocessable_entity
end
end
end
# config/routes.cr
CRails.application.routes.draw do
resources :posts
root "welcome#index"
end
Run the suite with make spec (or
CRAILS_VIEWS=spec/fixtures/app/views crystal spec).
Feature comparison vs actionpack 8.1.4
Controllers & callbacks
| Feature | Original (actionpack) | Port (action_pack.cr) | Status |
|---|---|---|---|
ActionController::Base inheritance | ✅ | Same class hierarchy | ✅ |
before_action / skip_before_action (only:/except:/if:/unless:) | ✅ | Same options via macro | ✅ |
around_action (symbol + block, yield continues the chain) | ✅ | Same; one generated __around_step_N per step (Crystal macro recursion limit) | ✅ |
rescue_from ... with: | ✅ | Macro-based | ✅ |
respond_to do |format| ... end | ✅ | Fixed format.html/format.json methods instead of a method_missing collector | 🔶 |
render plain:/html:/json:/template:/partial:/layout:, status:, formats:, locals:, render_to_string, head | ✅ | Same | ✅ |
.json.jbuilder action templates (implicit render for format=json) | ✅ | Via jbuilder.cr; explicit render is named-arg only (render(template:)) — positional render("...") is the JSON body | 🔶 |
redirect_to, redirect_back fallback_location:, redirect_back_or_to, notice:/alert: | ✅ | Same | ✅ |
Routing
| Feature | Original | Port | Status |
|---|---|---|---|
root, verb helpers get/post/patch/put/delete (to:, as:, defaults:, on: member/collection) | ✅ | Same; inside a resources block to: may be omitted (controller/action inferred) | ✅ |
resources / resource (only:, except:, controller:, param:, path:, as:, defaults:) with block nesting, member/collection | ✅ | Standard seven routes in Rails draw order; a block nests under the member path (/rooms/:id/messages), names follow Rails (edit_room_message) | ✅ |
resources / resource shallow:, concerns:, concern | ✅ | Not ported | ❌ |
namespace, scope (path:, as:, module:, defaults:) | ✅ | Same; module: prefixes to: controllers too (leading / opts out), defaults: fill params the path leaves unset | ✅ |
scope constraints:, concern/concerns, wildcard *path, optional segments foo(/bar), match via:, route-level redirect | ✅ | Not ported | ❌ |
Named helpers posts_path, edit_post_path(@post) | ✅ | Macro-generated from resources/get as: | ✅ |
Polymorphic array URLs url_for([:edit, @post]) | ✅ | ~70% | 🔶 |
Strong parameters
| Feature | Original | Port | Status |
|---|---|---|---|
params[:id], require(:post) (raises ParameterMissing → 400), permit, expect, to_h/to_unsafe_h/slice/except/dig/fetch | ✅ | Same; query + JSON/form body parsing, nested form keys post[title]=x | ✅ |
Nested filters permit(:title, tags: [], addresses: [:id]) | ✅ | Hash-argument form permit(:title, {"author" => [:name]}) — **opts with heterogeneous values breaks Crystal 1.21 | 🔶 |
HashWithIndifferentAccess semantics | ✅ | Symbol|String keys without distinction (approximation) | 🔶 |
Cookies / session / flash
| Feature | Original | Port | Status |
|---|---|---|---|
cookies.set with expires:/path:/domain:/secure:/httponly:/same_site:, cookies.delete | ✅ | Rack-identical Set-Cookie attribute order/format | ✅ |
cookies.signed / cookies.encrypted / cookies.permanent (+ chains) | ✅ | Byte-identical wire format (MessageVerifier SHA1 / AES-256-GCM, legacy {"_rails":...} envelope), purpose isolation, CookieOverflow > 4096 B | ✅ |
| session / flash in signed cookie | ✅ | With SECRET_KEY_BASE set; legacy Base64-JSON always readable (transition mode) | 🔶 |
flash[:notice]/now/keep/discard, add_flash_types | ✅ | Same | ✅ |
CSRF protection
| Feature | Original | Port | Status |
|---|---|---|---|
protect_from_forgery with: :exception/:null_session/:reset_session, skip_forgery_protection | ✅ | Token wire format 1:1 (BREACH mask, global + per-form HMAC-SHA256, Crypto::Subtle compare); param or X-CSRF-Token header | ✅ |
Origin check, JS-response InvalidCrossOriginRequest | ✅ | Off by default, as in the gem | ✅ |
| Per-class mutators, custom strategy classes | ✅ | Global config only; with: is a Symbol | ❌ (equivalent: global ActionController.* setters) |
HTTP caching & streaming
| Feature | Original | Port | Status |
|---|---|---|---|
fresh_when / stale? (etag/weak/strong/last_modified/public) | ✅ | strict_freshness semantics (Rails 8.1 default), weak ETag W/"sha256[0,32]" | ✅ |
expires_in, http_cache_forever, no_store | ✅ | Not ported | ⬜ |
Record validators in fresh_when(@article), template digest | ✅ | etag: accepts a String only | ❌ (equivalent: precompute the digest string) |
send_data / send_file (filename/type/disposition), RFC 5987 Content-Disposition | ✅ | Subset of Rack::Mime for type lookup; missing file → 404 | ✅ |
send_file Range requests | ✅ | Not ported | ⬜ |
ActionController::Live / SSE (response.stream, send_stream) | ✅ | Fiber-based: backpressure (buffer queue_size=10), client-disconnect cancellation, byte-exact SSE wire format | ✅ |
Beyond parity (not in actionpack)
| Feature | Port | Status |
|---|---|---|
| TypedParams — compile-time typed action params | DSL mirrors dry-schema 1.13.4 semantics (42/42 oracle cases); invalid params → 422 JSON errors | ✅ |
OpenAPI 3.0.3 codegen from the route table (openapi "/openapi.json") | Verified with openapi3_parser 0.10.1 | ✅ |
| Rate limiting | 20/20 oracle cases | ✅ |
Deviations & Crystal-specific notes
- Set-Cookie headers are written directly to
response.headers(no deferred commit); HTTPS detection usesX-Forwarded-Proto(Crystal'sHTTP::Serveris unaware of TLS termination); SameSite is not injected by default (railties territory); the cookie verifier supports only SHA1/SHA256 digests. - In
Live:IOErrormaps toIO::Error; ActionView completion is not written when an exception fires after commit (matches observed gem behavior);abortwakes a blocked producer immediately;id: 0is valid (the Ruby falsy quirk is not reproducible); SSE non-String payloads use stdlibto_json. permit/expectnested filters use a Hash argument (see table).- HTTP layer is Crystal
HTTP::Serverwith anHTTP::Handlerchain exposing the same stack operations (use/insert_before/insert_after/ swap/delete); concurrency is fiber-per-connection. - Benchmarks (
bench/results/): SSE fanout ~482–622k events/s vs Rails+Puma 31–41k (~15×); chunked ~710–887k chunks/s (~6–7×); typed params ~30× vs dry-schema.
Testing
make spec # or: CRAILS_VIEWS=spec/fixtures/app/views crystal spec
Oracle specs: Ruby cases in oracle/*_cases.rb are recorded with the real
actionpack 8.1.4 into oracle/*_expected.yml, then replayed byte-for-byte
by the Crystal mirror in spec/. Transport-level specs (Live/SSE, CSRF,
data streaming, typed params E2E) live in railties.cr/spec/ against a
real HTTP::Server.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.2.11- Tagged
- Oct 22, 2026
- Commit
87f85b61962a- Crystal
>= 1.21.0- Indexed
- yes
Dependents
Repository
github.com/OrelSokolov/action_pack.cr
Metadata
- Created
- Oct 13, 2026
- Updated
- Oct 24, 2026
- Synced
- Oct 24, 2026
- Versions
- 17