Purpose: This completes Phase 2 by migrating the last generator (openapiv3) and addressing the two remaining requirements: FOUND-04 (serialization audit) and cross-file annotation resolution error handling. After this plan, all duplicated annotation code is eliminated.
Output: All 4 generators use shared annotations, error suppression fixed, serialization audit confirmed, full test suite green.
<execution_context> @/Users/sebastienmelki/.claude/get-shit-done/workflows/execute-plan.md @/Users/sebastienmelki/.claude/get-shit-done/templates/summary.md </execution_context>
Move the following from http_annotations.go to openapiv3/types.go (types.go is 298 lines -- adding ~80 lines of these functions keeps it well under 400):
convertHeadersToParametersfunction (~30 lines)mapHeaderTypeToOpenAPIfunction (~20 lines)- Header type constants:
headerTypeString,headerTypeInt32,headerTypeInt64,headerTypeInteger,headerTypeNumber,headerTypeFloat,headerTypeDouble
These are OpenAPI-specific type conversion functions, NOT annotation parsing. They stay in the openapiv3 package.
Also move TestMapHeaderTypeToOpenAPI and BenchmarkMapHeaderTypeToOpenAPI from http_annotations_test.go to an existing test file in the openapiv3 package (e.g., internal/openapiv3/exhaustive_golden_test.go if it has unit tests, or internal/openapiv3/types_test.go if it exists, or create internal/openapiv3/types_test.go). Check which test files exist and pick the most appropriate one.
The HTTP method lowercase constants (httpMethodGet, etc.) are NO LONGER needed -- openapiv3 will use strings.ToLower(cfg.Method) instead. Do not move them.
Step 2: Update openapiv3/generator.go
Add "github.com/SebastienMelki/sebuf/internal/annotations" import. Replace ALL annotation function calls:
-
getServiceHTTPConfig(service)at line ~327 -> replace withannotations.GetServiceBasePath(service). The current code does:serviceConfig := getServiceHTTPConfig(service)and later accesses
serviceConfig.BasePath. Replace withservicePath := annotations.GetServiceBasePath(service). -
getMethodHTTPConfig(method)at line ~328 ->annotations.GetMethodHTTPConfig(method)CRITICAL: openapiv3'sgetMethodHTTPConfigreturns lowercase methods (via its ownhttpMethodToString). The sharedGetMethodHTTPConfigreturns UPPERCASE methods (viaHTTPMethodToString). The openapiv3 generator uses the method string inaddOperationToPathwhere lowercase is required for OpenAPI. Fix this by lowercasing at the usage site: after callingannotations.GetMethodHTTPConfig(method), usestrings.ToLower(cfg.Method)where the lowercase method is needed (line ~346 area inaddOperationToPathand line ~353 where it's set on the path item). -
buildHTTPPath(servicePath, methodPath)at line ~346 ->annotations.BuildHTTPPath(servicePath, methodPath) -
getQueryParams(method.Input)at line ~382 ->annotations.GetQueryParams(method.Input)openapiv3's QueryParam had FieldName, ParamName, Required, Field. The shared QueryParam has all these plus extras. Verify access patterns:.FieldName,.ParamName,.Required,.Field-- all still valid. -
combineHeaders(getServiceHeaders(service), getMethodHeaders(method))at line ~475 ->annotations.CombineHeaders(annotations.GetServiceHeaders(service), annotations.GetMethodHeaders(method)) -
hasUnwrapAnnotation(field)at line ~216 ->annotations.HasUnwrapAnnotation(field)(in generator.go's schema generation) -
getUnwrapField(valueField.Message)at line ~238 ->annotations.FindUnwrapField(valueField.Message)(openapiv3 uses the simple version that returns*protogen.Field, not the full*UnwrapFieldInfo. The sharedFindUnwrapFieldmatches this signature exactly.)
Step 3: Update openapiv3/types.go
Add "github.com/SebastienMelki/sebuf/internal/annotations" import. Replace:
getFieldExamples(field)at line ~122 ->annotations.GetFieldExamples(field)getUnwrapField(valueField.Message)at line ~202 ->annotations.FindUnwrapField(valueField.Message)(returns*protogen.Field, used ingetMapValueSchema)- Remove
getUnwrapFieldfunction definition (lines ~242-249) -- replaced byannotations.FindUnwrapField - Remove
hasUnwrapAnnotationfunction definition (lines ~252-271) -- replaced byannotations.HasUnwrapAnnotation - Remove
getFieldExamplesfunction definition (lines ~273-298) -- replaced byannotations.GetFieldExamples
Keep convertField, convertScalarField, convertEnumField, convertMapField, getMapValueSchema, getMapValueField, createUnwrapArraySchema -- these are OpenAPI-specific type conversion logic, not annotation parsing.
Step 4: Delete openapiv3/http_annotations.go
Delete internal/openapiv3/http_annotations.go (406 lines). At this point, all reusable functions have been migrated to shared annotations, and OpenAPI-specific functions have been moved to types.go in Step 1.
Step 5: Delete openapiv3/http_annotations_test.go
Delete internal/openapiv3/http_annotations_test.go (390 lines). Tests for pure shared functions were moved to the shared package in plan 02-01. TestMapHeaderTypeToOpenAPI and BenchmarkMapHeaderTypeToOpenAPI were moved in Step 1. The remaining struct tests (TestHTTPConfig_Struct, TestQueryParam_Struct, TestServiceHTTPConfig_Struct) tested local types that no longer exist and have equivalents in the shared package.
Step 6: Verify
go build ./internal/openapiv3/go build ./cmd/protoc-gen-openapiv3/go test ./internal/openapiv3/...go test -v -run TestExhaustiveGoldenFiles ./internal/openapiv3/...-- MUST pass with ZERO golden file changesmake lint-fix
Do NOT update golden files. If they need updating, the migration is wrong.
go build ./cmd/protoc-gen-openapiv3/ succeeds.
go test ./internal/openapiv3/... all tests pass.
Golden file tests pass WITHOUT updating golden files.
ls internal/openapiv3/http_annotations.go returns "No such file or directory".
ls internal/openapiv3/http_annotations_test.go returns "No such file or directory".
make lint-fix clean.
-
collectFileUnwrapFields(line ~88-101): Currently doescontinueon error:info, err := annotations.GetUnwrapField(msg) if err != nil { // Log error but continue continue }Change to: Return the error. This requires changing the function signature from
func collectFileUnwrapFields(messages []*protogen.Message, global *GlobalUnwrapInfo)tofunc collectFileUnwrapFields(messages []*protogen.Message, global *GlobalUnwrapInfo) error. On error, return a descriptive error wrapping the original:return fmt.Errorf("collecting unwrap fields: %w", err). The callercollectGlobalUnwrapInfomust propagate this error. -
collectUnwrapFieldsRecursive(line ~134-148): Same pattern --continueon error. Change to return error. Update signature and propagate. The callercollectAllUnwrapFieldsmust also return an error and its callers must handle it.
Trace the call chain upward to ensure errors propagate to the point where generation can be stopped:
collectGlobalUnwrapInfo-> called from Generator's main generation functioncollectAllUnwrapFields-> called fromcollectUnwrapContextcollectUnwrapContext-> called from Generator
Make sure the error reaches the generator's Run or Generate method and causes it to return an error to protoc (which stops code generation with an error message).
Every error message MUST include the proto file/message/field name. The UnwrapValidationError already has MessageName and FieldName. When wrapping, preserve this context.
After making changes:
go build ./internal/httpgen/go build ./cmd/protoc-gen-go-http/go test ./internal/httpgen/...make lint-fix
The research already confirmed that encoding/json usage in httpgen is CORRECT (used for json.Marshaler/json.Unmarshaler interface checks on unwrap types, not for proto message serialization). Verify by grep:
grep -rn "encoding/json" internal/httpgen/*.go-- confirm it appears only in unwrap.go for json.Marshaler/json.Unmarshaler interface checks and in binding-related generated code templatesgrep -rn "protojson" internal/httpgen/*.go-- confirm protojson is used for proto message serializationgrep -rn "json.Marshal\|json.Unmarshal" internal/httpgen/*.go-- confirm these are NOT used with proto.Message arguments (they should only appear in generated code templates for unwrap types that implement json.Marshaler/Unmarshaler)
If all three checks confirm correct usage: no code changes needed. This is intentionally verification-only.
Part 2: Final full-suite verification
Run the complete test suite to confirm all 4 generators work correctly:
go build ./cmd/protoc-gen-go-http/ ./cmd/protoc-gen-go-client/ ./cmd/protoc-gen-ts-client/ ./cmd/protoc-gen-openapiv3/./scripts/run_tests.sh(full run with coverage, not --fast)- Verify no golden file changes across any generator
make lint-fix
Part 3: Verify duplication is eliminated
wc -l internal/httpgen/annotations.go internal/clientgen/annotations.go internal/tsclientgen/annotations.go internal/openapiv3/http_annotations.go 2>/dev/nullshould show all 4 files missingwc -l internal/annotations/*.goshould show the shared package existsgrep -r "proto.GetExtension" internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/should return zero results (all extension access is in the shared package now)
go build ./cmd/protoc-gen-go-http/ ./cmd/protoc-gen-go-client/ ./cmd/protoc-gen-ts-client/ ./cmd/protoc-gen-openapiv3/ all succeed.
./scripts/run_tests.sh full suite passes with coverage above 85%.
make lint-fix clean.
Zero golden file changes across all generators.
grep -r "proto.GetExtension" internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/ returns zero results.
All 4 old annotation files confirmed deleted.
Serialization audit confirms encoding/json used correctly (interface checks only, not proto message serialization).
<success_criteria>
- Phase 2 success criteria 1: internal/annotations package exists, all 4 generators import it
- Phase 2 success criteria 2: All golden file tests pass without changes
- Phase 2 success criteria 3: ~1,289 lines of duplicated annotation parsing removed
- Phase 2 success criteria 4: protojson used exclusively for proto message serialization (confirmed by audit)
- Phase 2 success criteria 5: Cross-file annotation resolution never silently suppresses errors
- Full test suite green with coverage >= 85% </success_criteria>
