Skip to content

feat(api): add validation errors to v3 ingested events#4222

Merged
tothandras merged 2 commits intomainfrom
fix/v3-openapi
Apr 24, 2026
Merged

feat(api): add validation errors to v3 ingested events#4222
tothandras merged 2 commits intomainfrom
fix/v3-openapi

Conversation

@tothandras
Copy link
Copy Markdown
Contributor

@tothandras tothandras commented Apr 24, 2026

Summary by CodeRabbit

  • New Features

    • Event ingestion responses now include validation error details (code, message, optional attributes) when validations fail.
  • Bug Fixes / Behavior Changes

    • Public sort parameter parsing defaults to descending and rejects unsupported or malformed fields with clear errors.
    • Invalid query filters now return explicit bad-request responses identifying the offending parameter.

@tothandras tothandras added the release-note/feature Release note: Exciting New Features label Apr 24, 2026
@tothandras tothandras requested a review from a team as a code owner April 24, 2026 09:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Adds a new validation error model and exposes optional validation_errors on ingested events across the spec, generated API types, and conversion logic; also adjusts event sort and customer-id filter parsing/validation in handlers and updates tests accordingly.

Changes

Cohort / File(s) Summary
Spec: Event Model
api/spec/packages/aip/src/events/event.tsp
Adds IngestedEventValidationError (code, message, optional attributes) and an optional validation_errors?: IngestedEventValidationError[] on IngestedEvent.
Generated API Types & OpenAPI
api/v3/api.gen.go
Adds MeteringIngestedEventValidationError type and ValidationErrors *[]MeteringIngestedEventValidationError on MeteringIngestedEvent; regenerates embedded Swagger spec to include the new model.
Event Conversion & Helpers
api/v3/handlers/events/convert.go
Adds conversion of backend errors -> API ValidationError objects (filters nils, sets code="validation_error" and message=err.Error()); adds fromAPICustomerIDFilter and fromAPIEventSort helpers that validate/translate API filters and sort parameters and return BadRequest errors on invalid input.
Event Listing Handler
api/v3/handlers/events/list.go
Now uses fromAPIEventSort for sort parsing; replaces local filter/sort helper error handling to return explicit apierrors.NewBadRequestError with InvalidParameters; removes old local helper functions and local customer-id mapping.
Tests
api/v3/handlers/events/list_test.go
Renames and updates sort unit test to exercise fromAPIEventSort (same cases, expectations unchanged).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant API as API Handler\n(ListMeteringEvents)
    participant Conv as Converter\n(toAPIMeteringIngestedEvent)
    participant Backend as Backend/DB
    rect rgba(135,206,250,0.5)
    Client->>API: GET /events?sort=...
    API->>API: validate query (fromAPIEventSort, fromAPICustomerIDFilter)
    end
    rect rgba(144,238,144,0.5)
    API->>Backend: Query events with filters/sort
    Backend-->>API: events (with possible validation errors)
    end
    rect rgba(255,228,181,0.5)
    API->>Conv: convert events -> API models (include ValidationErrors)
    Conv-->>API: MeteringIngestedEvent (+ValidationErrors)
    end
    API-->>Client: 200 OK (events JSON)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • turip
  • borosr
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately summarizes the main change: adding validation error support to v3 ingested events across multiple files (spec, API generation, handlers, and tests).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/v3-openapi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tothandras tothandras changed the title Fix/v3 openapi feat(api): add validation errors to v3 ingested events Apr 24, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
api/v3/handlers/events/convert.go (2)

52-73: Small simplification opportunity.

The function works correctly, just a bit busier than it needs to be — the early len(errs) == 0 guard is redundant (lo.FilterMap on an empty slice yields an empty result, which the later len(result) == 0 check already handles), and &result works fine in place of lo.ToPtr(result) since result is already addressable. Totally optional, feel free to ignore if you prefer the current shape.

♻️ Optional tidy-up
 func toAPIMeteringIngestedEventValidationErrors(errs []error) *[]api.MeteringIngestedEventValidationError {
-	if len(errs) == 0 {
-		return nil
-	}
-
 	result := lo.FilterMap(errs, func(err error, _ int) (api.MeteringIngestedEventValidationError, bool) {
 		if err == nil {
 			return api.MeteringIngestedEventValidationError{}, false
 		}
 
 		return api.MeteringIngestedEventValidationError{
 			Code:    "validation_error",
 			Message: err.Error(),
 		}, true
 	})
 
 	if len(result) == 0 {
 		return nil
 	}
 
-	return lo.ToPtr(result)
+	return &result
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/events/convert.go` around lines 52 - 73, In
toAPIMeteringIngestedEventValidationErrors remove the redundant early guard that
checks len(errs) == 0 (lo.FilterMap already handles empty slices) and return a
pointer to result using &result instead of lo.ToPtr(result); keep the
lo.FilterMap logic and the final nil check for empty result unchanged.

57-66: Every validation error gets the same "validation_error" code — that defeats the "machine readable" promise.

The TypeSpec docs describe code as "The machine readable code of the error", but right now every error (meter-not-found, data parse failure, missing customer, etc.) collapses to the same constant string. Consumers won't be able to branch on it programmatically, which is the whole point of having a separate code field alongside message.

Looking at validateEvents in openmeter/meterevent/adapter/event.go (lines 227–272), the different error sources are already distinguishable at the origin — it'd be great to carry that distinction through rather than flatten it here. A couple of options:

  • Define typed sentinel errors (e.g., ErrNoMeterMatch, ErrNoCustomer, ErrMeterParse) in meterevent and use errors.Is/errors.As here to pick a stable code.
  • Or introduce a small ValidationError struct (with Code, Message, optional Attributes) on meterevent.Event instead of []error, so the producer owns the code and this converter is a trivial mapping.

The second option also lines up nicely with the attributes field that's in the spec but currently unset.

♻️ Sketch of the sentinel-based approach
-		return api.MeteringIngestedEventValidationError{
-			Code:    "validation_error",
-			Message: err.Error(),
-		}, true
+		code := "validation_error"
+		switch {
+		case errors.Is(err, meterevent.ErrNoMeterMatch):
+			code = "no_meter_match"
+		case errors.Is(err, meterevent.ErrNoCustomer):
+			code = "no_customer"
+		case errors.As(err, new(*meter.ParseError)):
+			code = "event_data_parse_error"
+		}
+		return api.MeteringIngestedEventValidationError{
+			Code:    code,
+			Message: err.Error(),
+		}, true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/events/convert.go` around lines 57 - 66, The current mapping
in convert.go collapses all validation errors to the literal "validation_error";
update the mapping in the conversion (the lo.FilterMap over errs that returns
api.MeteringIngestedEventValidationError) to inspect each error using errors.Is
/ errors.As against sentinel errors or typed errors from the meterevent package
(e.g., meterevent.ErrNoMeterMatch, meterevent.ErrNoCustomer,
meterevent.ErrMeterParse or a meterevent.ValidationError type) and assign a
stable machine-readable Code per error kind (e.g., "meter_not_found",
"no_customer", "meter_parse_error"), preserve the original message in Message,
and populate Attributes on api.MeteringIngestedEventValidationError where
available so callers can programmatically branch on Code and use Attributes for
extra context; adjust imports to include errors and meterevent as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/spec/packages/aip/src/events/event.tsp`:
- Around line 127-130: Update the docstring for the optional field
validation_errors on the IngestedEvent type so it refers to the singular event;
change the text from "The validation errors of the ingested events." to a
singular form such as "The validation errors of the ingested event." Ensure you
update the comment above validation_errors?: IngestedEventValidationError[] to
reflect the singular phrasing.
- Around line 152-157: The validation error objects created by
toAPIMeteringIngestedEventValidationErrors are not populating the attributes
field declared as attributes?: Record<unknown>; update
toAPIMeteringIngestedEventValidationErrors in api/v3/handlers/events/convert.go
to set attributes using the existing helper
ToAPIValidationAttributes(issue.Attributes()) (same pattern as the addons
handler) when mapping each issue so the event validation errors include the
correct attributes payload.

---

Nitpick comments:
In `@api/v3/handlers/events/convert.go`:
- Around line 52-73: In toAPIMeteringIngestedEventValidationErrors remove the
redundant early guard that checks len(errs) == 0 (lo.FilterMap already handles
empty slices) and return a pointer to result using &result instead of
lo.ToPtr(result); keep the lo.FilterMap logic and the final nil check for empty
result unchanged.
- Around line 57-66: The current mapping in convert.go collapses all validation
errors to the literal "validation_error"; update the mapping in the conversion
(the lo.FilterMap over errs that returns
api.MeteringIngestedEventValidationError) to inspect each error using errors.Is
/ errors.As against sentinel errors or typed errors from the meterevent package
(e.g., meterevent.ErrNoMeterMatch, meterevent.ErrNoCustomer,
meterevent.ErrMeterParse or a meterevent.ValidationError type) and assign a
stable machine-readable Code per error kind (e.g., "meter_not_found",
"no_customer", "meter_parse_error"), preserve the original message in Message,
and populate Attributes on api.MeteringIngestedEventValidationError where
available so callers can programmatically branch on Code and use Attributes for
extra context; adjust imports to include errors and meterevent as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4c22d090-a47f-40f4-a1c2-4a7948ed0bd8

📥 Commits

Reviewing files that changed from the base of the PR and between 7210983 and dbe6e94.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (3)
  • api/spec/packages/aip/src/events/event.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/events/convert.go

Comment thread api/spec/packages/aip/src/events/event.tsp
Comment thread api/spec/packages/aip/src/events/event.tsp
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
api/v3/handlers/events/convert.go (2)

60-81: Every validation error collapses to the same Code: "validation_error". 🏷️

Since the backend meterevent.Event.ValidationErrors is just []error (per openmeter/meterevent/service.go), there's no richer info to map today — that's fine, and matches what the schema allows. Just flagging two follow-ups worth tracking:

  1. The attributes field declared in the TypeSpec model will always be omitted here. That's consistent with the current backend shape (plain error), so this isn't a bug — but it means clients will see a perfectly uniform code for two semantically different failures (unknown meter vs. missing customer, see openmeter/meterevent/adapter/event.go:226-270). Consider promoting those to a small typed error (with a code + optional attributes) at the source so this converter can propagate something meaningful.
  2. Minor: lo.FilterMap already returns an empty (non-nil) slice when no elements match, so the second len(result) == 0 guard is a safety net. Not harmful, just redundant — you could drop it or keep it, your call.

Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/events/convert.go` around lines 60 - 81,
toAPIMeteringIngestedEventValidationErrors currently collapses all backend
errors (meterevent.Event.ValidationErrors) to a single Code "validation_error";
to fix, introduce small typed validation errors in the meterevent package (e.g.,
ErrUnknownMeter, ErrMissingCustomer or a ValidationError interface that exposes
Code() string and Attributes() map[string]string) and return those from
openmeter/meterevent/adapter/event.go so this converter can inspect error types
and map them to distinct api.MeteringIngestedEventValidationError.Code and
Attributes; then update toAPIMeteringIngestedEventValidationErrors to
type-assert each error from errs, use the provided Code()/Attributes() when
present (fall back to "validation_error"), and optionally remove the redundant
second empty-slice check (len(result) == 0).

154-160: The suffix detection via strings.Fields(*sort) couples fragile re-parsing to ParseSortBy's implementation.

You're checking len(strings.Fields(*sort)) == 1 to infer "no asc/desc suffix provided," which means you're re-doing part of ParseSortBy's parsing logic. If that function ever changes how it splits input (different separators, whitespace handling, etc.), this check can silently drift out of sync and break the default-to-desc behavior.

A cleaner approach: have ParseSortBy expose whether the order was explicitly set (e.g., a bool flag, or a nullable order type), then branch on that. If that's not feasible right now, at least add a comment tying this logic to ParseSortBy's contract so the coupling is explicit for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/events/convert.go` around lines 154 - 160, Replace the
fragile re-parsing of *sort with an explicit signal from ParseSortBy: modify
ParseSortBy to return (parsed, bool orderExplicit) or make parsed.Order
nullable/optional, then in convert.go use that flag (or nil-check on
parsed.Order) instead of len(strings.Fields(*sort)) to decide whether to
override with sortx.OrderDesc; if you can't change ParseSortBy now, add a clear
comment above the current check referencing ParseSortBy's contract and why
strings.Fields(*sort) must match its splitting behavior, and mark a TODO to
refactor to an explicit flag (referencing ParseSortBy, parsed.Order,
ToSortxOrder, and sortx.OrderDesc).
api/spec/packages/aip/src/events/event.tsp (1)

126-158: Schema looks good — clean model split between IngestedEvent and IngestedEventValidationError. 🎯

The singular doc phrasing on line 128 is already sorted, and marking all fields of IngestedEventValidationError as Lifecycle.Read matches the fact these are only ever returned on reads. Nice.

One small thought for the future: the code field is declared as a machine-readable code, but it's currently just a free-form string and the Go converter sets it to the literal "validation_error" for every error. If you ever want clients to branch on specific validation failures (e.g., no_meter_found, no_customer_found), consider either promoting code to an enum or at least documenting the expected values + adding an @example. Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/packages/aip/src/events/event.tsp` around lines 126 - 158, The
IngestedEventValidationError.model uses a free-form string for the
machine-readable code (field code) which prevents clients from branching on
specific errors; update the model IngestedEventValidationError to either (a)
replace code: string with a typed enum of expected codes (e.g., no_meter_found,
no_customer_found, validation_error) or (b) keep string but add an `@example` and
a short doc list of expected values in the model comment, and then update the Go
converter logic that currently sets "validation_error" to instead emit the
specific enum/value names (or documented examples) so clients can reliably
switch on code.
api/v3/handlers/events/list.go (1)

128-220: Consider reintroducing a small helper for these filter error responses. 🧹

The summary mentions filterError was removed in favor of inline apierrors.NewBadRequestError calls, but the result is the same ~7 lines duplicated seven times, differing only in the Field name. That's quite a bit of ceremony for what is conceptually "filter X failed to parse → 400".

A tiny local helper keeps this readable and keeps field/source conventions in one place:

♻️ Example refactor
+func newFilterBadRequestError(ctx context.Context, field string, err error) error {
+	return apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
+		{
+			Field:  field,
+			Reason: err.Error(),
+			Source: apierrors.InvalidParamSourceQuery,
+		},
+	})
+}
+
 func applyFilters(ctx context.Context, req *ListMeteringEventsRequest, f *api.ListEventsParamsFilter) error {
 	id, err := filters.FromAPIFilterString(f.Id)
 	if err != nil {
-		return apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
-			{
-				Field:  "filter[id]",
-				Reason: err.Error(),
-				Source: apierrors.InvalidParamSourceQuery,
-			},
-		})
+		return newFilterBadRequestError(ctx, "filter[id]", err)
 	}
 	req.ID = id
 	// ...same treatment for source/subject/type/time/ingested_at/stored_at
 }

As per coding guidelines: "In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/events/list.go` around lines 128 - 220, The applyFilters
function repeats the same apierrors.NewBadRequestError construction for each
filter parse failure; add a small local helper (e.g., filterError or
newFilterBadRequest) inside applyFilters that accepts (ctx context.Context, err
error, name string) and returns apierrors.NewBadRequestError(ctx, err,
apierrors.InvalidParameters{{Field: fmt.Sprintf("filter[%s]", name), Reason:
err.Error(), Source: apierrors.InvalidParamSourceQuery}}), then replace each
duplicated error block (for Id, Source, Subject, Type, Time, IngestedAt,
StoredAt) to call that helper (keep the existing calls to
filters.FromAPIFilterString and filters.FromAPIFilterDateTime and
fromAPICustomerIDFilter unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@api/spec/packages/aip/src/events/event.tsp`:
- Around line 126-158: The IngestedEventValidationError.model uses a free-form
string for the machine-readable code (field code) which prevents clients from
branching on specific errors; update the model IngestedEventValidationError to
either (a) replace code: string with a typed enum of expected codes (e.g.,
no_meter_found, no_customer_found, validation_error) or (b) keep string but add
an `@example` and a short doc list of expected values in the model comment, and
then update the Go converter logic that currently sets "validation_error" to
instead emit the specific enum/value names (or documented examples) so clients
can reliably switch on code.

In `@api/v3/handlers/events/convert.go`:
- Around line 60-81: toAPIMeteringIngestedEventValidationErrors currently
collapses all backend errors (meterevent.Event.ValidationErrors) to a single
Code "validation_error"; to fix, introduce small typed validation errors in the
meterevent package (e.g., ErrUnknownMeter, ErrMissingCustomer or a
ValidationError interface that exposes Code() string and Attributes()
map[string]string) and return those from openmeter/meterevent/adapter/event.go
so this converter can inspect error types and map them to distinct
api.MeteringIngestedEventValidationError.Code and Attributes; then update
toAPIMeteringIngestedEventValidationErrors to type-assert each error from errs,
use the provided Code()/Attributes() when present (fall back to
"validation_error"), and optionally remove the redundant second empty-slice
check (len(result) == 0).
- Around line 154-160: Replace the fragile re-parsing of *sort with an explicit
signal from ParseSortBy: modify ParseSortBy to return (parsed, bool
orderExplicit) or make parsed.Order nullable/optional, then in convert.go use
that flag (or nil-check on parsed.Order) instead of len(strings.Fields(*sort))
to decide whether to override with sortx.OrderDesc; if you can't change
ParseSortBy now, add a clear comment above the current check referencing
ParseSortBy's contract and why strings.Fields(*sort) must match its splitting
behavior, and mark a TODO to refactor to an explicit flag (referencing
ParseSortBy, parsed.Order, ToSortxOrder, and sortx.OrderDesc).

In `@api/v3/handlers/events/list.go`:
- Around line 128-220: The applyFilters function repeats the same
apierrors.NewBadRequestError construction for each filter parse failure; add a
small local helper (e.g., filterError or newFilterBadRequest) inside
applyFilters that accepts (ctx context.Context, err error, name string) and
returns apierrors.NewBadRequestError(ctx, err,
apierrors.InvalidParameters{{Field: fmt.Sprintf("filter[%s]", name), Reason:
err.Error(), Source: apierrors.InvalidParamSourceQuery}}), then replace each
duplicated error block (for Id, Source, Subject, Type, Time, IngestedAt,
StoredAt) to call that helper (keep the existing calls to
filters.FromAPIFilterString and filters.FromAPIFilterDateTime and
fromAPICustomerIDFilter unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0761a894-8691-44be-bc57-fe5fc3e311ec

📥 Commits

Reviewing files that changed from the base of the PR and between dbe6e94 and ee925cd.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (5)
  • api/spec/packages/aip/src/events/event.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/events/convert.go
  • api/v3/handlers/events/list.go
  • api/v3/handlers/events/list_test.go
✅ Files skipped from review due to trivial changes (2)
  • api/v3/handlers/events/list_test.go
  • api/v3/api.gen.go

@tothandras tothandras enabled auto-merge (squash) April 24, 2026 10:18
@tothandras tothandras merged commit 76201dc into main Apr 24, 2026
27 of 28 checks passed
@tothandras tothandras deleted the fix/v3-openapi branch April 24, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants