active_support.cr

active_support.cr

A Crystal port of Rails ActiveSupport 8.1.4 — core extensions (blank?, Duration, date math, Inflector, deep hash ops), Concern, delegate, Notifications, cache stores, I18n, number helpers, message verification/encryption and time zones. Namespace ActiveSupport mirrors the original 1:1; the goal is that Ruby snippets from the Rails docs work unchanged. Estimated parity ~85%. Core extensions are implemented by reopening Crystal stdlib types (adding methods is allowed; overriding existing ones is not) and by macros in place of define_method.

⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.

Installation

dependencies:
  active_support_cr:
    github: OrelSokolov/active_support.cr
shards install

Requires Crystal >= 1.21.0. pg and redis are needed only for the optional adapters/* cache stores.

Usage

require "active_support_cr"

2.days.ago
%w[a b c].to_sentence            # => "a, b, and c"
"post".pluralize                  # => "posts"
hash.deep_merge(other)
"42".present?                     # => true
Time.use_zone("Europe/Berlin") { Time.current }

Feature comparison vs activesupport 8.1.4

Core extensions

Feature Original (activesupport) Port (active_support.cr) Status
Object: blank?, present?, presence, in?, presence_in, deep_dup, to_param ✅ Same ✅
NilClass: blank?, present?, try ✅ Same ✅
String: squish, truncate, truncate_bytes, at/from/to, first/last, starts_with?/ends_with?, exclude?, remove, indent, strip_heredoc ✅ Same ✅
Inflector: pluralize, camelize, underscore, tableize, classify, humanize, titleize, parameterize, demodulize, foreign_key, ordinalize ✅ Same ✅
Hash: symbolize_keys, deep_symbolize_keys, stringify_keys, except, slice, reverse_merge, with_defaults, deep_merge, deep_transform_keys/values, compact_blank, assert_valid_keys ✅ Same ✅
Array: in_groups_of, in_groups, split, extract!, to_sentence, second..fifth, second_to_last, wrap, excluding, including ✅ Same ✅
Enumerable: index_by, index_with, many?, exclude?, pluck, pick, sole, in_order_of, compact_blank ✅ Same ✅
Int/Numeric: seconds..years, bytes..exabytes, multiple_of?, in_milliseconds (Duration mechanics) ✅ Duration is a fixed Time::Span (see deviations) 🔶
Date/Time math: beginning/end_of_*, all_day/..., ago/since, advance, change, yesterday/tomorrow, next_week/prev_*, months_ago/..., next_occurring, past?/future?/today?, to_fs(:db/:short/:long/:iso8601) ✅ Same ✅
constantize / String#constantize ✅ Not portable — dependent shards use compile-time registries ❌ (equivalent: compile-time class registry)

Time zones

Feature Original Port Status
Time.zone, TimeWithZone, in_time_zone, utc/localtime, formatted_offset ✅ TimeZone built on Time::Location; TWZ arithmetic, <=>/== by UTC instant, zoned strftime, to_fs ✅
Per-user timezone: Time.use_zone(zone, &) ✅ Fiber-local override registry + ensure-restore ✅
Time.current, TimeZone#now, I18n.l with TWZ ✅ Same ✅
DST-aware calendar Duration arithmetic across TWZ ✅ Duration is a fixed span — twz + 1.day always shifts 24 h ❌ (equivalent: shift via Time#shift calendar math)

Infrastructure

Feature Original Port Status
ActiveSupport::Concern (included do / class_methods do) ✅ Crystal included hook + macros ✅
delegate :x, to: :y, prefix:, allow_nil: / delegate_missing_to ✅ Macros ✅
mattr_accessor/cattr_accessor, class_attribute, thread_mattr_* ✅ Macros over @@/fiber-locals ✅
ActiveSupport::Notifications (instrument/subscribe/publish) ✅ Same ✅
ActiveSupport::Cache: MemoryStore, NullStore, FileStore; fetch/read/write/exist?/delete, expires_in:, race_condition_ttl: ✅ Same Store API ✅
RedisCacheStore ✅ adapters/redis_cache.cr (SET/GET EX, INCRBY counters, namespace); stores raw strings — INCR on a write-stored integer succeeds (Rails wraps values in a coder and returns nil) 🔶
SolidCache (PG table) separate gem adapters/pg_cache.cr: solid_cache_entries(key, value, expires_at) + atomic SQL counters + delete_expired 🔶
I18n: t(key, scope:, default:, count:), yml locales, pluralization, l, available_locales, fallbacks ✅ Same ✅
NumberHelper family ✅ Same (locale data) ✅
MessageVerifier / MessageEncryptor (signed/encrypted cookies) ✅ SHA1/SHA256 digests, KeyGenerator-derived keys, legacy {"_rails":...} envelope read/write (wire format of actionpack 8.1 cookies) ✅
SecureRandom.base58/base36, Digest::UUID ✅ Same ✅
CurrentAttributes ✅ Fiber-locals ✅
TaggedLogging, Deprecation, BacktraceCleaner ✅ Same ✅
HashWithIndifferentAccess ✅ Approximation used by params: Symbol|String keys 🔶

Deviations & Crystal-specific notes

  • Core extensions reopen stdlib types (struct Int, class String); overriding existing stdlib methods is forbidden, so those keep Crystal semantics.
  • Duration is a fixed Time::Span: calendar months/years have variable length, and TWZ arithmetic does not cross DST boundaries the way Rails durations do.
  • The fiber-local zone registry is not cleaned up if a fiber dies inside a use_zone block; Time.zone without a fiber override is process-global (like Rails' Time.zone=).
  • FileStore writes timestamps with sub-second precision (expiry < 1 s).
  • Cache stores keep raw string values (see RedisCacheStore row).

Testing

crystal spec

Suite: 319/319. Oracle specs (oracle/cases.rb → expected.yml, including 19 tz_* timezone cases) are recorded with the real activesupport 8.1.4 and replayed unchanged; cache stores are additionally verified against a real redis-server (oracle/cache_stores_cases.rb) and a shared contract spec runs across all four stores.

Repository

active_support.cr

Owner
Statistic
  • 0
  • 0
  • 0
  • 5
  • 2
  • 2 days ago
  • October 12, 2026
License

Links
Synced at

Wed, 14 Oct 2026 09:08:03 GMT

Languages