Route events through a typed Event value - #1398
Open
myronmarston wants to merge 1 commit into
Open
myronmarston wants to merge 1 commit into
myronmarston wants to merge 1 commit into
Conversation
Collaborator
Author
|
This change is part of the following stack: Change managed by git-spice. |
myronmarston
added this pull request to stack #1399
September 18, 2026 18:43
This was referenced Sep 18, 2026
myronmarston
force-pushed
the
myron/typed-event-object
branch
from
September 18, 2026 19:32
10c5980 to
cdcdefd
Compare
myronmarston
removed this pull request from stack #1399
September 18, 2026 19:44
myronmarston
added this pull request to stack #1401
September 18, 2026 19:49
This was referenced Sep 18, 2026
myronmarston
force-pushed
the
myron/typed-event-object
branch
2 times, most recently
from
September 18, 2026 20:41
98d0305 to
691f157
Compare
myronmarston
force-pushed
the
myron/typed-event-object
branch
2 times, most recently
from
September 18, 2026 23:07
42659c2 to
8e057bf
Compare
myronmarston
force-pushed
the
myron/typed-event-object
branch
from
September 19, 2026 01:33
8e057bf to
1c9e973
Compare
myronmarston
force-pushed
the
myron/typed-event-object
branch
from
September 20, 2026 01:03
1c9e973 to
1c1b78b
Compare
myronmarston
marked this pull request as ready for review
September 20, 2026 02:18
myronmarston
requested review from
BrianSigafoos-SQ,
bsorbo,
ellisandrews-toast,
jwils,
jwondrusch and
marcdaniels-toast
as code owners
September 20, 2026 02:18
myronmarston
force-pushed
the
myron/typed-event-object
branch
from
September 20, 2026 04:57
1c1b78b to
aa3b1f3
Compare
jwils
approved these changes
Sep 21, 2026
| }, | ||
| "nested_fields" => { | ||
| "max_widget_cost" => ([w1] + widgets).select { |w| w.dig("record", "cost", "currency") == "USD" }.map { |w| w.fetch("record").fetch("cost").fetch("amount_cents") }.max | ||
| "max_widget_cost" => ([w1] + widgets).select { |w| w.record.dig("cost", "currency") == "USD" }.map { |w| w.record.fetch("cost").fetch("amount_cents") }.max |
Collaborator
There was a problem hiding this comment.
Suggested change
| "max_widget_cost" => ([w1] + widgets).select { |w| w.record.dig("cost", "currency") == "USD" }.map { |w| w.record.fetch("cost").fetch("amount_cents") }.max | |
| "max_widget_cost" => ([w1] + widgets).filter_map do |w| | |
| w.record.fetch("cost").fetch("amount_cents") if w.record.dig("cost", "currency") == "USD" | |
| end.max |
| end | ||
| end | ||
|
|
||
| # Steep weirdly expects them here... |
| # @param hash [Hash<String, Object>] a validated JSON event | ||
| # @return [Event] | ||
| def self.from_validated_hash(hash) | ||
| latency_timestamps = hash["latency_timestamps"] || {} # : ::Hash[::String, ::String] |
Collaborator
There was a problem hiding this comment.
Any reason not the follow the same pattern as the other entries?
Suggested change
| latency_timestamps = hash["latency_timestamps"] || {} # : ::Hash[::String, ::String] | |
| hash.fetch("latency_timestamps", {}) |
Comment on lines
+61
to
+62
| schema_version: hash.fetch(JSON_SCHEMA_VERSION_KEY), | ||
| ingestion_format: hash.fetch(INGESTION_FORMAT_KEY, "json"), |
Collaborator
There was a problem hiding this comment.
Is there a way to put this in the json_ingestion? I don't love having the json specific keys stay here.
- Introduce Indexer::Event, a Data value object with a validated
envelope (op, type, id, version, record, schema_version,
ingestion_format, message_id, latency_timestamps).
- Swap every event["x"] / event.fetch("x") for typed accessors across
FailedEventError, IndexingFailuresError, Operation::Factory and
Update, DatastoreIndexingRouter, Processor, the JSON ingestion
adapter and EnvelopeValidator, WarehouseDumper,
TestSupport::Converters, and the affected specs and RBS files.
- Event#event_id returns the EventID for a validated event, so callers
ask the event for its id rather than building one from its fields.
EventID.from_decoded_hash covers the one case with no Event to ask:
a payload that failed envelope validation, where any envelope field
can be absent. Both keep the "type:id@vversion" format in EventID.
- FailedEventError delegates id/op/type/version/record/message_id to
its event via Forwardable instead of manual wrapper methods, and
includes IndexingFailuresError::_IndexingFailure so Steep enforces
the interface it already satisfies.
- EnvelopeValidator#events_from now returns Events via
Event.from_validated_hash instead of hashes.
- TestSupport::Converters keeps upsert_event_hash_for alongside
upsert_event_for, since the JSON hash and the Event are now distinct:
specs that exercise envelope handling need the hash.
- Add a JSONIngestion::Indexer spec proving envelope validation still
applies to a type configured to skip record validation, since that
guarantee now depends on events_from and validate_event being
separate steps rather than an explicit check in the operation
factory.
Generated with Claude Code
myronmarston
force-pushed
the
myron/typed-event-object
branch
from
September 21, 2026 00:48
aa3b1f3 to
efdd817
Compare
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.
Why
Every consumer of an indexing event reached into a
Hashwith string keys. The shape of the envelope lived only in scatteredfetchcalls, Steep could not check it, and each consumer had to repeat the defaults for optional fields. ADatavalue with typed members states the envelope once.An
Eventexists only after an ingestion adapter validates its envelope, so every instance has the typed fields the envelope declares.What
Indexer::Event, with membersop,type,id,version,record,schema_version,ingestion_format,message_id, andlatency_timestamps.event["x"]andevent.fetch("x")acrossEventID,FailedEventError,IndexingFailuresError,Operation::Factory,Operation::Update,DatastoreIndexingRouter,Processor,IngestionAdapter, the JSON ingestion adapter,WarehouseDumper, andTestSupport::Converters.FailedEventErrordelegatesid,op,type,version,record, andmessage_idto its event throughForwardable, and includesIndexingFailuresError::_IndexingFailureso that Steep enforces the interface it satisfies.events_fromreturnsEvents throughEvent.from_validated_hash.Event#recordkeeps the native record type of the adapter, which is aHashfor JSON. An ingestion format with a different record type can therefore carry its own.🤖 Generated with Claude Code