Fix :datetime raising NameError without Rails - #30
Conversation
Code review — #30 :datetime without RailsVerdict: mergeable with fixes. The headline bug is real and fixed, and the subprocess spec is genuinely Rails-free. But the require is one level too narrow: Verified
Strengths
Important
Minor
|
| # cast_datetime names ActiveSupport::TimeWithZone, which activesupport does not | ||
| # load by default. A Rails app has it via active_support/time at boot; a | ||
| # standalone host (a Contract validating a webhook payload or a job argument) | ||
| # has nothing that loads it, and every :datetime cast raised NameError there. |
There was a problem hiding this comment.
require "active_support/time_with_zone" fixes the uninitialized constant error when Permittable::Contract runs in background workers or scripts without full Rails boot.
| expect(out).to eq('{"user":{"test_key":1},"address_attributes":{"location":2}}') | ||
| end | ||
|
|
||
| it "casts every scalar type with only `require \"permittable\"`" do |
There was a problem hiding this comment.
Running the standalone contract in an isolated Ruby subprocess (Open3.capture3) reliably guards against future dependency leaks in tests.
cast_datetime names ActiveSupport::TimeWithZone unguarded, and nothing
in the gem loads it — activesupport does not load it by default:
$ ruby -e 'require "permittable"; ...'
lib/permittable.rb:336:in `cast_datetime':
uninitialized constant ActiveSupport::TimeWithZone (NameError)
A Rails app gets the constant from active_support/time at boot, which
is exactly why the spec suite never saw this: spec_helper requires
active_record, which pulls in every core extension and masks a require
the gem itself forgot. A standalone Contract — a webhook payload, a job
argument, the controller-free use the gem advertises — loads nothing
that defines it, so every :datetime field raised instead of validating.
This is the same shape as the 0.5.1 nested-hash bug, and the spec added
for that one already warned about the masking in its own comment. So
rather than add a new bare-subprocess spec, that one now exercises
EVERY scalar type instead of only nested hashes, which is what would
have caught this. A companion spec pins that a real TimeWithZone is
still accepted and normalised to UTC, so the fix cannot regress into a
guard that quietly stops handling zoned times.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e9985c6 to
55e6e38
Compare
The bug
cast_datetimenamesActiveSupport::TimeWithZoneunguarded, and nothing in the gem loads it — activesupport doesn't load it by default:So every
:datetimefield raised instead of validating, in any host that hadn't loaded ActiveSupport's time extensions — a standaloneContractvalidating a webhook payload or a job argument, which is exactly the usePermittable::Contractexists for.A Rails app gets the constant from
active_support/timeat boot, so this never showed up there.Why the suite didn't catch it
spec_helperrequiresactive_record, which pulls in every core extension and masks a require the gem itself forgot. The spec added for the 0.5.1 nested-hash bug says so in its own comment:Same warning, same shape of bug, one type it didn't cover.
The fix
One precise require —
require "active_support/time_with_zone"— rather than the broaderactive_support/time, matching how the gem already requires only the core extensions it uses.The specs
Rather than add another bare-subprocess spec, the existing one now exercises every scalar type instead of only nested hashes, which is what would have caught this:
A companion spec pins that a real
ActiveSupport::TimeWithZoneis still accepted and normalised to UTC, so the fix can't regress into a guard that quietly stops handling zoned times.Verification
NameErrorbefore the fix)runtime-depsCI job would have caught this class of bug, but its smoke script's contract has no:datetimefield. Worth extending it when that PR lands — I'll do it if you want.