-
Notifications
You must be signed in to change notification settings - Fork 24
HYPERFLEET-1369 - refactor: Flatten handlerConfig #316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
openshift-merge-bot
merged 1 commit into
openshift-hyperfleet:main
from
ma-hill:HYPERFLEET-1369
Jul 30, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,115 @@ | ||
| # Claude Code Guidelines for Handlers | ||
|
|
||
| Handlers use the `handlerConfig` pipeline defined in `framework.go`. | ||
| ## Request Processing | ||
|
|
||
| ## Pipeline Functions | ||
| Handlers follow a standard pipeline: | ||
|
|
||
| - `handle(w, r, cfg, httpStatus)` — Full pipeline: unmarshal `cfg.MarshalInto` from request body → run `cfg.Validate` functions → execute `cfg.Action` → write response | ||
| - `handleGet(w, r, cfg)` — No body read, action only, returns 200 | ||
| - `handleList(w, r, cfg)` — No body read, action only, returns 200 | ||
| - `handleDelete(w, r, cfg)` — No body read, action only, returns 204 | ||
| - `handleCreateWithNoContent(w, r, cfg)` — Body read + validate, returns 204 if action returns nil | ||
| 1. **Decode and validate** — Use `decodeAndValidate(r, &req, validateFuncs)` to unmarshal request body and run validation | ||
| - Pass `"strict"` as 4th parameter for PATCH requests only to reject unknown/immutable fields via `DisallowUnknownFields()` | ||
| - Validation functions return `*errors.ServiceError` on failure | ||
| 2. **Convert** — Use `presenters.Convert*()` to convert request types to internal models | ||
| 3. **Call service** — Pass converted models to service layer methods | ||
| 4. **Present** — Use `presenters.Present*()` to convert internal models to response types | ||
| 5. **Write response** — Use `writeJSONResponse(w, r, statusCode, data)` or `handleError(r, w, err)` | ||
|
|
||
| ## handlerConfig Fields | ||
| ## Handler Types | ||
|
|
||
| - `MarshalInto`: pointer to struct for JSON unmarshalling (e.g., `&openapi.ResourceCreateRequest{}`) | ||
| - `Validate`: slice of `func() *errors.ServiceError` — each returns nil on success | ||
| - `Action`: `func() (interface{}, *errors.ServiceError)` — core business logic | ||
| - `ErrorHandler`: optional custom error handler (rarely used) | ||
| - **ResourceHandler** — Handles both flat (`/api/hyperfleet/v1/clusters`) and owner-nested (`/api/hyperfleet/v1/clusters/{parent_id}/nodepools`) routes for a single entity kind | ||
| - Extract parent_id to determine route type - `r.PathValue("parent_id")` | ||
| - See struct comment for safety invariant (plugins must register correctly) | ||
| - **RootResourceHandler** — Handles kind-agnostic routes (`/api/hyperfleet/v1/resources`, `/api/hyperfleet/v1/resources/{id}`) | ||
| - Resolves entity kind dynamically via `registry.Get(kind)` | ||
| - Handles adapter status operations (`/api/hyperfleet/v1/resources/{id}/statuses`) | ||
|
|
||
| ## Patterns | ||
| ## Validation Patterns | ||
|
|
||
| - Validation helpers check fields on the unmarshalled struct and return `errors.Validation("reason")` on failure | ||
| - Use `presenters.Convert*()` to convert request types to internal models before calling services | ||
| - Use `presenters.Present*()` to convert internal models to response types | ||
| - Handler structs hold service interfaces — never access DAOs directly from handlers | ||
| - Extract path params via `r.PathValue("id")` (stdlib `net/http.ServeMux`, Go 1.22+) | ||
| Validation functions close over the request struct and return `func() *errors.ServiceError`: | ||
|
|
||
| ## Error Responses | ||
| ```go | ||
| validateFuncs := []validate{ | ||
| validateKind(&req, "Kind", "kind", expectedKind), | ||
| validateName(&req, "Name", "name", minLen, maxLen), | ||
| validateSpec(&req, "Spec", "spec"), | ||
| validateLabels(&req, "Labels"), | ||
| } | ||
| ``` | ||
|
|
||
| Errors are written via `handleError()` in `framework.go` which calls `err.AsProblemDetails()` for RFC 9457 format. The trace ID comes from `r.Header.Get("X-Request-Id")`. | ||
| Common validators (in `validation.go`): | ||
| - `validateKind(req, fieldName, jsonName, expectedKind)` — Ensures `kind` matches descriptor | ||
| - `validateName(req, fieldName, jsonName, minLen, maxLen)` — Name length constraints | ||
| - `validateSpec(req, fieldName, jsonName)` — Spec is non-nil | ||
| - `validateLabels(req, fieldName)` — Labels follow RFC 1123 rules | ||
| - `validatePatchRequest(req)` — At least one field is set | ||
| - `validateNotEmpty(req, fieldName, jsonName)` — String field is non-empty | ||
| - `validateMaxLength(req, fieldName, jsonName, maxLen)` — String length constraint | ||
|
|
||
| ## Related CLAUDE.md Files | ||
| ## Owner Verification | ||
|
|
||
| For nested routes, verify the child belongs to the parent: | ||
|
|
||
| ```go | ||
| if err := h.checkOwnership(r, id); err != nil { | ||
| handleError(r, w, err) | ||
| return | ||
| } | ||
| ``` | ||
|
|
||
| This is a no-op for flat routes (no `parent_id` in path). | ||
|
|
||
| ## Error Handling | ||
|
|
||
| Use `handleError(r, w, err)` to write RFC 9457 Problem Details responses: | ||
| - Logs errors with structured fields (trace ID, error code, HTTP status) | ||
| - Automatically extracts trace ID from request context | ||
| - Sets `instance` to the request path | ||
| - Error constructor functions (from `pkg/errors`) | ||
|
|
||
|
|
||
| ## Path Parameters | ||
|
|
||
| Extract route parameters with r.PathValue | ||
|
|
||
| ```go | ||
| id := r.PathValue("id") | ||
| // Extract parent_id | ||
| if parentID := r.PathValue("parent_id"); parentID != "" { | ||
| if _, svcErr := h.resourceService.GetByOwner(ctx, h.descriptor.Kind, id, parentID); svcErr != nil { | ||
| handleError(r, w, svcErr) | ||
| return | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Field Filtering | ||
|
|
||
| Apply `?fields=` query parameter filtering: | ||
|
|
||
| ```go | ||
| result, svcErr := applyFieldFilter(r, presenters.PresentResource(resource)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
| if svcErr != nil { | ||
| handleError(r, w, svcErr) | ||
| return | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| For list responses: | ||
| ```go | ||
| if listArgs.Fields != nil { | ||
| filtered, svcErr := presenters.SliceFilter(listArgs.Fields, result) | ||
| if svcErr != nil { | ||
| handleError(r, w, svcErr) | ||
| return // REQUIRED — omitting this causes a double-write | ||
| } | ||
| writeJSONResponse(w, r, http.StatusOK, filtered) | ||
| return // REQUIRED — prevents falling through to the unfiltered write below | ||
| } | ||
| writeJSONResponse(w, r, http.StatusOK, result) | ||
| ``` | ||
|
|
||
| ## Related Files | ||
|
|
||
| - `pkg/services/CLAUDE.md` — Service layer patterns | ||
| - `pkg/errors/CLAUDE.md` — Error constructors and codes | ||
| - `helpers.go` — `writeJSONResponse`,`decodeAndValidate`, `handleError`, `applyFieldFilter` implementations | ||
| - `validation.go` — Validator functions (`validateKind`, `validateName`, `validateSpec`, etc.) | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
h.resourceServiceis the field onResourceStatusHandler, notResourceHandler. This ownership example is aResourceHandlerpattern, where the field ish.service. Might be worth changing on the next PR.