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
28 changes: 20 additions & 8 deletions lib/statifier_ui/shape.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,26 @@ defmodule StatifierUI.Shape do
atoms drawn from the eight duration units and whose values are all
integers.

A **subset** of the units is enough, deliberately. `Predicator.Duration`'s
own builder returns all eight, but predicator's expression parser returns
seven - `Predicator.evaluate("3d")` omits `:milliseconds` - so requiring
the full set misses every duration produced by evaluating an expression,
which is the common case. Nothing else in the value domain can collide:
atom keys reach a fixture only from Elixir, and a datamodel forbids them
outright (`StatifierUI.Fixtures`), so the only atom-keyed maps that arrive
here are durations.
A **subset** of the units is enough, deliberately, and that is a statement
about this repository rather than about predicator. Predicator's contract is
eight keys: `Predicator.Duration.new/1` fills every unit it was not given,
and since predicator 9.0 the expression evaluator seeds all eight too, so
`Predicator.evaluate("3d")` carries `milliseconds: 0` like the rest
(px-69c). Nothing here is working around that.

The rule is wider than the contract because this is a viewer. It renders a
value stream it did not produce - a fixture written by hand, a decoded wire
message, a datamodel from an older engine or another interpreter - and the
right response to a duration missing a unit is to render it as a duration,
not to fall back to a seven-field map and make the reader work out what
they are looking at. Tightening to exactly eight would trade that tolerance
for nothing: no caller gains a guarantee, because callers that need the
full eight keys go through `StatifierUI.Value.encode/1`, which canonicalizes
to all of them on the way out.

Nothing else in the value domain can collide: atom keys reach a fixture only
from Elixir, and a datamodel forbids them outright (`StatifierUI.Fixtures`),
so the only atom-keyed maps that arrive here are durations.

Public because `StatifierUI.Fixtures` and `StatifierUI.Value` need the same
rule, and three copies of it are what let the seven-versus-eight gap go
Expand Down
24 changes: 20 additions & 4 deletions test/statifier_ui/shape_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ defmodule StatifierUI.ShapeTest do
assert Shape.infer(duration) == :duration
end

test "the seven-key duration predicator's parser actually emits infers as :duration" do
# Predicator.Duration.new/1 fills all eight units, but the expression
# parser omits :milliseconds. Requiring all eight missed every duration
# produced by evaluating an expression, which is the common path.
test "a seven-key duration from outside predicator infers as :duration" do
# No longer anything predicator emits: since 9.0 the evaluator seeds all
# eight units (px-69c). This pins the viewer's own tolerance instead -
# a hand-written fixture, an older engine, or another interpreter can
# hand us a duration short a unit, and it still renders as a duration.
seven_key = %{
years: 0,
months: 0,
Expand All @@ -106,8 +107,23 @@ defmodule StatifierUI.ShapeTest do
test "durations from real predicator expressions infer as :duration" do
for expr <- ["3d", "2w", "1h30m", "3d8h"] do
assert {:ok, value} = Predicator.evaluate(expr)

# Pins predicator's eight-key contract (px-69c, shipped in 9.0). If
# the evaluator ever goes back to seeding seven, it fails here, at the
# dependency, rather than somewhere downstream that assumed eight.
assert map_size(value) == 8, "#{expr} did not evaluate to all eight units"

assert Shape.infer(value) == :duration, "#{expr} did not infer as a duration"
assert Shape.label(Shape.infer(value)) == "duration"

# And pins the subset tolerance against real output rather than a
# hand-written map: dropping a unit must not change the verdict.
# Without this line the assertions above would still pass under a rule
# tightened to require exactly eight keys, since every expression now
# yields all eight - which is the incidental pass sui-cw0 was filed
# to catch.
assert Shape.infer(Map.delete(value, :milliseconds)) == :duration,
"#{expr} stopped being a duration when a unit was dropped"
end
end

Expand Down
Loading