fix: serialize range fields as bracket literals, symmetric with writes - #113
Merged
Conversation
The 7 range fields on PlantType/VarietyType (nAccumulationRange,
biomassProductionRange, optimalTemperatureRange, optimalRainfallRange,
seasonalityDaysRange, optimalAltitudeRange, phRange) were rendering
through graphql-ruby's default String coercion, i.e. Ruby's Range#to_s
("5.5..7.0", "600...1201", "0...Infinity" against prod). Writes accept
Postgres-style bracket literals ("[min,max]"); reads emitted Ruby Range
syntax. The admin SPA's range parser only understands bracket literals,
so every existing value rendered as a blank input.
Add RangeLiteral (app/services/range_literal.rb), a pure serializer that
mirrors the write-side format. Integer ranges (int4range) round-trip
through Postgres canonicalized to an inclusive-lower/exclusive-upper
Ruby Range, so a finite exclusive upper bound is decremented back to the
inclusive value an editor actually typed. Numeric ranges (numrange)
preserve whatever inclusivity was written, formatted via
BigDecimal#to_s('F') to avoid engineering notation, with a trailing
".0" stripped. Unbounded bounds (nil or infinite) render as an empty
side, always closed with "]".
Wire it into both types via Types::Concerns::RangeLiteralFields, which
define_method's the 7 resolvers from the single field list already
declared in Mutations::Concerns::RangeLiteralValidation::RANGE_FIELDS.
Also fix RecordDraftInfoType#author/#last_editor: Principal#display_name
is nil for principals created by resolve_actor's upsert (it never passes
one), so the field rendered "Unknown" for the common case. Falls back to
email, which editors (the only audience -- draft is update?-gated) can
already see via ownedBy/createdBy.
Full RSpec suite green (2324 examples), including mobile contracts
(spec/contracts -- zero range assertions there, confirmed by the run),
and rubocop clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnYLUscETuPwsQzU9nCSxu
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
curling the live plant endpoint shows the 7 range fields (nAccumulationRange, biomassProductionRange, optimalTemperatureRange, optimalRainfallRange, seasonalityDaysRange, optimalAltitudeRange, phRange) rendering as RubyRange#to_soutput -- e.g.'5.5..7.0','600...1201','0...Infinity'-- whileCreatePlant/UpdatePlantonly accept and document Postgres-style bracket literals ('[min,max]'). Reads and writes were asymmetric since these fields shipped, so the admin SPA's range parser renders a blank input for every existing value.RangeLiteral(app/services/range_literal.rb), a pure serializer symmetric with the write side:int4range) round-trip through Postgres canonicalized to inclusive-lower/exclusive-upper (an editor's"[500,2000]"comes back from the database as the Ruby Range500...2001), so a finite exclusive upper bound is decremented by 1 back to the inclusive value that was actually typed.numrange) preserve whatever inclusivity was written (Postgres doesn't canonicalize continuous ranges), formatted viaBigDecimal#to_s('F')to avoid engineering notation (BigDecimal#to_s/#inspectdefaults render"0.55e1"), with a trailing".0"stripped so a whole number like7.0renders"7".nilor infinite) render as an empty side, always closed with"]"("[10,]","[,100]"), never")".PlantTypeandVarietyTypeviaTypes::Concerns::RangeLiteralFields, whichdefine_method's the 7 resolvers from the single field list already declared inMutations::Concerns::RangeLiteralValidation::RANGE_FIELDS-- one source of truth for "the 7 range fields", no new field declarations (those already existed).RecordDraftInfoType#author/#last_editor:Principal#display_nameis nil for principals created byresolve_actor's upsert (application_controller.rb) -- it never passes a display name -- so this field rendered"Unknown"for the common case in staging. Falls back to email. No new exposure:draftis gated byupdate?, and editors who can see it already see the record's owner/creator email viaownedBy/createdBy.Test plan
docker compose run --rm -e RAILS_ENV=test web bundle exec rspec-- full suite green, 2324 examples, 0 failures.spec/contracts/**(mobile compatibility contracts) verified to carry zero range-field assertions before this change, and still green after -- the mobile app's locked-in GraphQL documents are unaffected by this fix.spec/services/range_literal_spec.rb(integer decrement, unbounded both sides, BigDecimal formatting incl. the scientific-notation trap, nil, exclusive-numeric paren form),spec/mutations/range_literal_round_trip_spec.rb(schema-level write-then-read round trips for"[10,]","[500,2000]","[5.5,7.0]"numrange, and a DRAFT-perspective read viasaveAsDraft: true+plant(perspective: DRAFT), plus a VarietyType check),spec/types/record_draft_info_type_spec.rb(display_name present / nil / blank -> email fallback).docker compose run --rm web bundle exec rubocop-- clean, 730 files, no offenses.docker compose run --rm web bundle exec rails graphql:schema:dump-- diff is exactly the twoRecordDraftInfo.author/.lastEditordescription updates; committed.Generated with Claude Code
https://claude.ai/code/session_01RnYLUscETuPwsQzU9nCSxu