Skip to content

Route events through a typed Event value - #1398

Open
myronmarston wants to merge 1 commit into
myron/validate-envelope-up-frontfrom
myron/typed-event-object
Open

myronmarston wants to merge 1 commit into
myron/validate-envelope-up-frontfrom
myron/typed-event-object

Conversation

@myronmarston

Copy link
Copy Markdown
Collaborator

Why

Every consumer of an indexing event reached into a Hash with string keys. The shape of the envelope lived only in scattered fetch calls, Steep could not check it, and each consumer had to repeat the defaults for optional fields. A Data value with typed members states the envelope once.

An Event exists only after an ingestion adapter validates its envelope, so every instance has the typed fields the envelope declares.

What

  • Indexer::Event, with members op, type, id, version, record, schema_version, ingestion_format, message_id, and latency_timestamps.
  • Typed accessors in place of event["x"] and event.fetch("x") across EventID, FailedEventError, IndexingFailuresError, Operation::Factory, Operation::Update, DatastoreIndexingRouter, Processor, IngestionAdapter, the JSON ingestion adapter, WarehouseDumper, and TestSupport::Converters.
  • FailedEventError delegates id, op, type, version, record, and message_id to its event through Forwardable, and includes IndexingFailuresError::_IndexingFailure so that Steep enforces the interface it satisfies.
  • The JSON adapter's events_from returns Events through Event.from_validated_hash.

Event#record keeps the native record type of the adapter, which is a Hash for JSON. An ingestion format with a different record type can therefore carry its own.

🤖 Generated with Claude Code

@myronmarston
myronmarston added this pull request to stack #1399 September 18, 2026 18:43
@myronmarston
myronmarston force-pushed the myron/typed-event-object branch from 10c5980 to cdcdefd Compare September 18, 2026 19:32
@myronmarston
myronmarston removed this pull request from stack #1399 September 18, 2026 19:44
@myronmarston
myronmarston added this pull request to stack #1401 September 18, 2026 19:49
@myronmarston
myronmarston force-pushed the myron/typed-event-object branch 2 times, most recently from 98d0305 to 691f157 Compare September 18, 2026 20:41
@myronmarston
myronmarston force-pushed the myron/typed-event-object branch 2 times, most recently from 42659c2 to 8e057bf Compare September 18, 2026 23:07
@myronmarston
myronmarston force-pushed the myron/typed-event-object branch from 8e057bf to 1c9e973 Compare September 19, 2026 01:33
@myronmarston
myronmarston force-pushed the myron/typed-event-object branch from 1c9e973 to 1c1b78b Compare September 20, 2026 01:03
@myronmarston
myronmarston marked this pull request as ready for review September 20, 2026 02:18
},
"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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why?

# @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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants