diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..82948f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,13 @@ +name: CI +on: + push: {} + pull_request: {} + workflow_dispatch: {} +permissions: + contents: read +jobs: + build: + uses: oapi-codegen/actions/.github/workflows/ci.yml@6cf35d4f044f2663dae54547ff6d426e565beb48 # v0.6.0 + with: + excluding_versions: '["1.24"]' + lint_versions: '["1.25"]' diff --git a/codegen/internal/codegen.go b/codegen/internal/codegen.go index 3035da9..31a9117 100644 --- a/codegen/internal/codegen.go +++ b/codegen/internal/codegen.go @@ -3,6 +3,7 @@ package codegen import ( "fmt" + "sort" "strings" "github.com/pb33f/libopenapi" @@ -1041,12 +1042,16 @@ func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, membe } } - // Build reverse mapping: $ref path → []discriminator values + // Build reverse mapping: $ref path → []discriminator values. + // Sort values so output is deterministic regardless of map iteration order. refToDiscValues := make(map[string][]string) if parentDesc != nil && parentDesc.Discriminator != nil { for discValue, refPath := range parentDesc.Discriminator.Mapping { refToDiscValues[refPath] = append(refToDiscValues[refPath], discValue) } + for ref := range refToDiscValues { + sort.Strings(refToDiscValues[ref]) + } } for i, proxy := range memberProxies { diff --git a/codegen/internal/dce/dce.go b/codegen/internal/dce/dce.go index f4e7203..bc7c986 100644 --- a/codegen/internal/dce/dce.go +++ b/codegen/internal/dce/dce.go @@ -94,18 +94,53 @@ func EliminateDeadCode(src string) (string, error) { } } - // Keep only reachable declarations. - var kept []ast.Decl - for _, d := range roots { - kept = append(kept, d) - } + // Keep only reachable declarations; collect positions of eliminated ones. + kept := append([]ast.Decl{}, roots...) + var eliminated []ast.Decl for _, c := range candidates { if isReachable(c.names, reachable) { kept = append(kept, c.decl) + } else { + eliminated = append(eliminated, c.decl) } } f.Decls = kept + // Remove comments associated with eliminated declarations so + // go/printer doesn't emit orphaned doc/inline comments. + if len(eliminated) > 0 { + // Build line ranges for eliminated declarations: from the doc + // comment (or decl start) through the last line of the decl. + removedLines := make([]lineSpan, 0, len(eliminated)) + for _, d := range eliminated { + start := d.Pos() + switch dd := d.(type) { + case *ast.GenDecl: + if dd.Doc != nil { + start = dd.Doc.Pos() + } + case *ast.FuncDecl: + if dd.Doc != nil { + start = dd.Doc.Pos() + } + } + removedLines = append(removedLines, lineSpan{ + startLine: fset.Position(start).Line, + endLine: fset.Position(d.End()).Line, + }) + } + + filtered := make([]*ast.CommentGroup, 0, len(f.Comments)) + for _, cg := range f.Comments { + cgStart := fset.Position(cg.Pos()).Line + cgEnd := fset.Position(cg.End()).Line + if !linesOverlap(cgStart, cgEnd, removedLines) { + filtered = append(filtered, cg) + } + } + f.Comments = filtered + } + var buf strings.Builder if err := printer.Fprint(&buf, fset, f); err != nil { return "", err @@ -154,6 +189,17 @@ func receiverTypeName(expr ast.Expr) string { return "" } +type lineSpan struct{ startLine, endLine int } + +func linesOverlap(cgStart, cgEnd int, spans []lineSpan) bool { + for _, s := range spans { + if cgStart >= s.startLine && cgEnd <= s.endLine { + return true + } + } + return false +} + func collectIdents(node ast.Node, idents map[string]bool) { ast.Inspect(node, func(n ast.Node) bool { if id, ok := n.(*ast.Ident); ok { diff --git a/codegen/internal/dce/dce_test.go b/codegen/internal/dce/dce_test.go new file mode 100644 index 0000000..1754a84 --- /dev/null +++ b/codegen/internal/dce/dce_test.go @@ -0,0 +1,186 @@ +package dce + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEliminateDeadCode_RemovesOrphanedComments(t *testing.T) { + src := `package gen + +func usedFunc() { + helperFunc() +} + +// --- oapi-runtime begin --- + +// helperFunc is used by usedFunc. +func helperFunc() {} + +// unusedFunc does nothing useful. +// It has a multi-line doc comment. +func unusedFunc() {} + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + assert.Contains(t, result, "helperFunc") + assert.NotContains(t, result, "unusedFunc") +} + +func TestEliminateDeadCode_NoMarkers(t *testing.T) { + src := `package gen + +func foo() {} +` + result, err := EliminateDeadCode(src) + require.NoError(t, err) + assert.Equal(t, src, result) +} + +func TestEliminateDeadCode_AllUsed(t *testing.T) { + src := `package gen + +func caller() { + a() + b() +} + +// --- oapi-runtime begin --- + +// a does something. +func a() {} + +// b does something else. +func b() {} + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + assert.Contains(t, result, "func a()") + assert.Contains(t, result, "func b()") + assert.Contains(t, result, "a does something") + assert.Contains(t, result, "b does something else") +} + +func TestEliminateDeadCode_NoneUsed(t *testing.T) { + src := `package gen + +func caller() {} + +// --- oapi-runtime begin --- + +// orphanA is unused. +func orphanA() {} + +// orphanB is also unused. +func orphanB() {} + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + assert.NotContains(t, result, "orphanA") + assert.NotContains(t, result, "orphanB") +} + +func TestEliminateDeadCode_InlineComments(t *testing.T) { + src := `package gen + +func caller() { + used() +} + +// --- oapi-runtime begin --- + +func used() {} // keep this + +func unused() {} // drop this + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + assert.Contains(t, result, "keep this") + assert.NotContains(t, result, "drop this") +} + +func TestEliminateDeadCode_TypeDecl(t *testing.T) { + src := `package gen + +// UsedType is referenced. +type UsedType struct{} + +// --- oapi-runtime begin --- + +// helperType is used by UsedType (transitively). +type helperType = UsedType + +// unusedType is not referenced. +type unusedType struct { + field string +} + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + // helperType is reachable because it references UsedType which is in roots, + // and helperType itself may or may not be reachable depending on the algorithm. + // The key assertion: unusedType and its comment should be gone. + assert.NotContains(t, result, "unusedType") + assert.NotContains(t, result, "unusedType is not referenced") +} + +func TestEliminateDeadCode_PreservesNonRuntimeComments(t *testing.T) { + src := `package gen + +// Package-level comment that should survive. + +// caller calls helper. +func caller() { + helper() +} + +// --- oapi-runtime begin --- + +// helper is needed. +func helper() {} + +// unused is not needed. +func unused() {} + +// --- oapi-runtime end --- +` + + result, err := EliminateDeadCode(src) + require.NoError(t, err) + + assert.Contains(t, result, "Package-level comment") + assert.Contains(t, result, "caller calls helper") + assert.Contains(t, result, "helper is needed") + assert.NotContains(t, result, "unused is not needed") + // The word "unused" should only not appear as a function or its comment + lines := strings.Split(result, "\n") + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.Contains(trimmed, "unused") && !strings.Contains(trimmed, "nolint") { + t.Errorf("unexpected reference to 'unused' in output: %s", trimmed) + } + } +} diff --git a/codegen/internal/runtime/params/helpers.go b/codegen/internal/runtime/params/helpers.go index 7b19419..a82fdf4 100644 --- a/codegen/internal/runtime/params/helpers.go +++ b/codegen/internal/runtime/params/helpers.go @@ -438,34 +438,3 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { } return b, nil } - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value any) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/codegen/internal/test/callbacks/output/callbacks_test.go b/codegen/internal/test/callbacks/output/callbacks_test.go index 9d16b07..1d3d60b 100644 --- a/codegen/internal/test/callbacks/output/callbacks_test.go +++ b/codegen/internal/test/callbacks/output/callbacks_test.go @@ -34,9 +34,9 @@ func TestSchemaInstantiation(t *testing.T) { // Verify callback request body type alias func TestCallbackRequestBodyAlias(t *testing.T) { - var body TreePlantedJSONRequestBody = TreePlantingResult{ + var body = TreePlantedJSONRequestBody(TreePlantingResult{ Success: true, - } + }) assert.True(t, body.Success) } diff --git a/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types.gen.go b/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types.gen.go index c185205..01e0e18 100644 --- a/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types.gen.go +++ b/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types.gen.go @@ -43,7 +43,7 @@ func (t ConfigSaveReq) AsConfigHTTP() (ConfigHTTP, error) { // FromConfigHTTP overwrites any union data inside the ConfigSaveReq as the provided ConfigHTTP. func (t *ConfigSaveReq) FromConfigHTTP(v ConfigHTTP) error { - v.ConfigType = "apache_server" + v.ConfigType = "another_server" b, err := json.Marshal(v) t.union = b return err @@ -51,7 +51,7 @@ func (t *ConfigSaveReq) FromConfigHTTP(v ConfigHTTP) error { // MergeConfigHTTP performs a merge with any union data inside the ConfigSaveReq, using the provided ConfigHTTP. func (t *ConfigSaveReq) MergeConfigHTTP(v ConfigHTTP) error { - v.ConfigType = "apache_server" + v.ConfigType = "another_server" b, err := json.Marshal(v) if err != nil { return err @@ -104,12 +104,12 @@ func (t ConfigSaveReq) ValueByDiscriminator() (any, error) { return nil, err } switch discriminator { + case "another_server": + return t.AsConfigHTTP() case "apache_server": return t.AsConfigHTTP() case "web_server": return t.AsConfigHTTP() - case "another_server": - return t.AsConfigHTTP() case "ssh_server": return t.AsConfigSSH() default: diff --git a/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types_test.go b/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types_test.go index b5e641b..10ba2c2 100644 --- a/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types_test.go +++ b/codegen/internal/test/components/one_of_discriminator_multi_mapping/output/types_test.go @@ -64,8 +64,8 @@ func TestOneOfDiscriminatorMultiMapping(t *testing.T) { } // TestFromConfigHTTPSetsDiscriminator verifies that FromConfigHTTP sets the -// discriminator value. Note: V3 codegen forces the first mapping value -// ("apache_server") regardless of what the caller sets. +// discriminator value. V3 codegen forces the lexicographically first mapping +// value ("another_server") regardless of what the caller sets. func TestFromConfigHTTPSetsDiscriminator(t *testing.T) { var saveReq ConfigSaveReq err := saveReq.FromConfigHTTP(ConfigHTTP{ @@ -77,9 +77,9 @@ func TestFromConfigHTTPSetsDiscriminator(t *testing.T) { disc, err := saveReq.Discriminator() require.NoError(t, err) - // V3 codegen forces "apache_server" — this documents the current behavior. + // V3 codegen forces "another_server" (sorted first among the HTTP mappings). // If/when this is fixed to preserve the caller's value, change this assertion. - assert.Equal(t, "apache_server", disc) + assert.Equal(t, "another_server", disc) } func TestApplyDefaults(t *testing.T) { diff --git a/codegen/internal/test/parameters/all_styles/output/types.gen.go b/codegen/internal/test/parameters/all_styles/output/types.gen.go index f170eef..4e2d2db 100644 --- a/codegen/internal/test/parameters/all_styles/output/types.gen.go +++ b/codegen/internal/test/parameters/all_styles/output/types.gen.go @@ -104,258 +104,18 @@ func GetOpenAPISpecJSON() ([]byte, error) { return openAPISpec() } -// MarshalText implements encoding.TextMarshaler for Date. - -// Format returns the date formatted according to layout. - -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - -// ParamLocation indicates where a parameter is located in an HTTP request. - -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - -// unescapeParameterString unescapes a parameter value based on its location. - -// sortedKeys returns the keys of a map in sorted order. - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - -// isByteSlice reports whether t is []byte (or equivalently []uint8). - -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - -// ParameterOptions carries OpenAPI parameter metadata to bind and style -// functions so they can handle style dispatch, explode, required, -// type-aware coercions, and location-aware escaping from a single -// uniform call site. All fields have sensible zero-value defaults. - -// OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" -// Where the parameter appears: query, path, header, cookie - -// OpenAPI type: "string", "integer", "array", "object" -// OpenAPI format: "int32", "date-time", etc. -// When true, reserved characters in query values are not percent-encoded - -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/codegen/internal/test/parameters/encoding/output/types.gen.go b/codegen/internal/test/parameters/encoding/output/types.gen.go index ded8cb9..a7a6f9f 100644 --- a/codegen/internal/test/parameters/encoding/output/types.gen.go +++ b/codegen/internal/test/parameters/encoding/output/types.gen.go @@ -100,258 +100,18 @@ func GetOpenAPISpecJSON() ([]byte, error) { return openAPISpec() } -// MarshalText implements encoding.TextMarshaler for Date. - -// Format returns the date formatted according to layout. - -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - -// ParamLocation indicates where a parameter is located in an HTTP request. - -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - -// unescapeParameterString unescapes a parameter value based on its location. - -// sortedKeys returns the keys of a map in sorted order. - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - -// isByteSlice reports whether t is []byte (or equivalently []uint8). - -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - -// ParameterOptions carries OpenAPI parameter metadata to bind and style -// functions so they can handle style dispatch, explode, required, -// type-aware coercions, and location-aware escaping from a single -// uniform call site. All fields have sensible zero-value defaults. - -// OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" -// Where the parameter appears: query, path, header, cookie - -// OpenAPI type: "string", "integer", "array", "object" -// OpenAPI format: "int32", "date-time", etc. -// When true, reserved characters in query values are not percent-encoded - -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/codegen/internal/test/parameters/precedence/output/types.gen.go b/codegen/internal/test/parameters/precedence/output/types.gen.go index e8d2067..90a4ff8 100644 --- a/codegen/internal/test/parameters/precedence/output/types.gen.go +++ b/codegen/internal/test/parameters/precedence/output/types.gen.go @@ -60,258 +60,18 @@ func GetOpenAPISpecJSON() ([]byte, error) { return openAPISpec() } -// MarshalText implements encoding.TextMarshaler for Date. - -// Format returns the date formatted according to layout. - -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - -// ParamLocation indicates where a parameter is located in an HTTP request. - -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - -// unescapeParameterString unescapes a parameter value based on its location. - -// sortedKeys returns the keys of a map in sorted order. - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - -// isByteSlice reports whether t is []byte (or equivalently []uint8). - -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - -// ParameterOptions carries OpenAPI parameter metadata to bind and style -// functions so they can handle style dispatch, explode, required, -// type-aware coercions, and location-aware escaping from a single -// uniform call site. All fields have sensible zero-value defaults. - -// OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" -// Where the parameter appears: query, path, header, cookie - -// OpenAPI type: "string", "integer", "array", "object" -// OpenAPI format: "int32", "date-time", etc. -// When true, reserved characters in query values are not percent-encoded - -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/codegen/internal/test/parameters/roundtrip/client/client.gen.go b/codegen/internal/test/parameters/roundtrip/client/client.gen.go index 017f92c..31a0b9d 100644 --- a/codegen/internal/test/parameters/roundtrip/client/client.gen.go +++ b/codegen/internal/test/parameters/roundtrip/client/client.gen.go @@ -1733,123 +1733,16 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - type UUID = uuid.UUID -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - // ParamLocation indicates where a parameter is located in an HTTP request. type ParamLocation int @@ -1861,12 +1754,6 @@ const ( ParamLocationCookie ) -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - // primitiveToString converts a primitive value to a string representation. // It handles basic Go types, time.Time, Date, and types that implement // json.Marshaler or fmt.Stringer. @@ -2004,8 +1891,6 @@ func isUnreserved(c byte) bool { c == '-' || c == '.' || c == '_' || c == '~' } -// unescapeParameterString unescapes a parameter value based on its location. - // sortedKeys returns the keys of a map in sorted order. func sortedKeys(m map[string]string) []string { keys := make([]string, 0, len(m)) @@ -2016,55 +1901,11 @@ func sortedKeys(m map[string]string) []string { return keys } -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 } -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -2440,10 +2281,3 @@ func marshalDeepObjectRecursive(in any, path []string) ([]string, error) { } return result, nil } - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/codegen/internal/test/webhooks/output/webhooks_test.go b/codegen/internal/test/webhooks/output/webhooks_test.go index 6539537..e1b2486 100644 --- a/codegen/internal/test/webhooks/output/webhooks_test.go +++ b/codegen/internal/test/webhooks/output/webhooks_test.go @@ -29,8 +29,8 @@ func TestWebhookKindEnum(t *testing.T) { // Verify type aliases for webhook request bodies func TestWebhookRequestBodyAliases(t *testing.T) { - var enterBody EnterEventJSONRequestBody = Person{Name: "Bob"} - var exitBody ExitEventJSONRequestBody = Person{Name: "Carol"} + var enterBody = EnterEventJSONRequestBody(Person{Name: "Bob"}) + var exitBody = ExitEventJSONRequestBody(Person{Name: "Carol"}) assert.Equal(t, "Bob", enterBody.Name) assert.Equal(t, "Carol", exitBody.Name) } diff --git a/examples/callback/treefarm.gen.go b/examples/callback/treefarm.gen.go index 30b9799..9c00f19 100644 --- a/examples/callback/treefarm.gen.go +++ b/examples/callback/treefarm.gen.go @@ -514,260 +514,20 @@ func TreePlantedCallbackHandler(si CallbackReceiverInterface, errHandler func(w }) } -// MarshalText implements encoding.TextMarshaler for Date. - -// Format returns the date formatted according to layout. - -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - type UUID = uuid.UUID -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - -// ParamLocation indicates where a parameter is located in an HTTP request. - -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - -// unescapeParameterString unescapes a parameter value based on its location. - -// sortedKeys returns the keys of a map in sorted order. - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - -// isByteSlice reports whether t is []byte (or equivalently []uint8). - -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - -// ParameterOptions carries OpenAPI parameter metadata to bind and style -// functions so they can handle style dispatch, explode, required, -// type-aware coercions, and location-aware escaping from a single -// uniform call site. All fields have sensible zero-value defaults. - -// OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" -// Where the parameter appears: query, path, header, cookie - -// OpenAPI type: "string", "integer", "array", "object" -// OpenAPI format: "int32", "date-time", etc. -// When true, reserved characters in query values are not percent-encoded - -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/chi/go.mod b/examples/petstore-expanded/chi/go.mod index 57dceb9..2804800 100644 --- a/examples/petstore-expanded/chi/go.mod +++ b/examples/petstore-expanded/chi/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/chi -go 1.24.0 +go 1.25.0 require ( github.com/go-chi/chi/v5 v5.2.5 diff --git a/examples/petstore-expanded/chi/server/server.gen.go b/examples/petstore-expanded/chi/server/server.gen.go index ba68158..13e7227 100644 --- a/examples/petstore-expanded/chi/server/server.gen.go +++ b/examples/petstore-expanded/chi/server/server.gen.go @@ -334,50 +334,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -646,21 +602,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -709,9 +650,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -1036,39 +974,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1081,8 +986,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1245,12 +1148,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1288,10 +1185,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1306,36 +1199,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/client/client.gen.go b/examples/petstore-expanded/client/client.gen.go index 93725ec..f992a22 100644 --- a/examples/petstore-expanded/client/client.gen.go +++ b/examples/petstore-expanded/client/client.gen.go @@ -6,11 +6,10 @@ import ( "bytes" "context" "encoding" + "encoding/base64" "encoding/json" "errors" "fmt" - "github.com/google/uuid" - petstore "github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded" "io" "net/http" "net/url" @@ -19,6 +18,9 @@ import ( "strconv" "strings" "time" + + "github.com/google/uuid" + petstore "github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded" ) type addPetJSONRequestBody = petstore.NewPet @@ -37,7 +39,7 @@ type Client struct { // The endpoint of the server conforming to this interface, with scheme, // https://api.deepmap.com for example. This can contain a path relative // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. + // paths in the OpenAPI spec will be appended to the server. Server string // Doer for performing requests, typically a *http.Client with any @@ -166,7 +168,7 @@ func (c *Client) AddPetWithBody(ctx context.Context, contentType string, body io return c.Client.Do(req) } -// AddPet makes a POST request to /pets with JSON body +// AddPet makes a POST request to /pets with application/json body func (c *Client) AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewAddPetRequest(c.Server, body) if err != nil { @@ -221,15 +223,15 @@ func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, e operationPath = "." + operationPath } - queryURL, err := serverURL.Parse(operationPath) + reqURL, err := serverURL.Parse(operationPath) if err != nil { return nil, err } if params != nil { - queryValues := queryURL.Query() + queryValues := reqURL.Query() if params.Tags != nil { - if queryFrag, err := StyleFormExplodeParam("tags", ParamLocationQuery, *params.Tags); err != nil { + if queryFrag, err := StyleParameter("tags", *params.Tags, ParameterOptions{Style: "form", ParamLocation: ParamLocationQuery, Explode: true, Required: false, Type: "array", Format: "", AllowReserved: false}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -242,7 +244,7 @@ func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, e } } if params.Limit != nil { - if queryFrag, err := StyleFormExplodeParam("limit", ParamLocationQuery, *params.Limit); err != nil { + if queryFrag, err := StyleParameter("limit", *params.Limit, ParameterOptions{Style: "form", ParamLocation: ParamLocationQuery, Explode: true, Required: false, Type: "integer", Format: "int32", AllowReserved: false}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -254,10 +256,10 @@ func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, e } } } - queryURL.RawQuery = queryValues.Encode() + reqURL.RawQuery = queryValues.Encode() } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("GET", reqURL.String(), nil) if err != nil { return nil, err } @@ -290,12 +292,12 @@ func NewAddPetRequestWithBody(server string, contentType string, body io.Reader) operationPath = "." + operationPath } - queryURL, err := serverURL.Parse(operationPath) + reqURL, err := serverURL.Parse(operationPath) if err != nil { return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("POST", reqURL.String(), body) if err != nil { return nil, err } @@ -310,7 +312,7 @@ func NewDeletePetRequest(server string, id int64) (*http.Request, error) { var err error var pathParam0 string - pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) + pathParam0, err = StyleParameter("id", id, ParameterOptions{Style: "simple", ParamLocation: ParamLocationPath, Explode: false, Required: true, Type: "integer", Format: "int64", AllowReserved: false}) if err != nil { return nil, err } @@ -325,12 +327,12 @@ func NewDeletePetRequest(server string, id int64) (*http.Request, error) { operationPath = "." + operationPath } - queryURL, err := serverURL.Parse(operationPath) + reqURL, err := serverURL.Parse(operationPath) if err != nil { return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest("DELETE", reqURL.String(), nil) if err != nil { return nil, err } @@ -343,7 +345,7 @@ func NewFindPetByIDRequest(server string, id int64) (*http.Request, error) { var err error var pathParam0 string - pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) + pathParam0, err = StyleParameter("id", id, ParameterOptions{Style: "simple", ParamLocation: ParamLocationPath, Explode: false, Required: true, Type: "integer", Format: "int64", AllowReserved: false}) if err != nil { return nil, err } @@ -358,12 +360,12 @@ func NewFindPetByIDRequest(server string, id int64) (*http.Request, error) { operationPath = "." + operationPath } - queryURL, err := serverURL.Parse(operationPath) + reqURL, err := serverURL.Parse(operationPath) if err != nil { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("GET", reqURL.String(), nil) if err != nil { return nil, err } @@ -371,7 +373,7 @@ func NewFindPetByIDRequest(server string, id int64) (*http.Request, error) { return req, nil } -// ClientHttpError represents an HTTP error response from the server. +// ClientHttpError represents an HTTP error response. // The type parameter E is the type of the parsed error body. type ClientHttpError[E any] struct { StatusCode int @@ -392,11 +394,11 @@ type SimpleClient struct { // NewSimpleClient creates a new SimpleClient which wraps a Client. func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) { - client, err := NewClient(server, opts...) + inner, err := NewClient(server, opts...) if err != nil { return nil, err } - return &SimpleClient{Client: client}, nil + return &SimpleClient{Client: inner}, nil } // FindPets makes a GET request to /pets and returns the parsed response. @@ -498,37 +500,40 @@ func (c *SimpleClient) FindPetByID(ctx context.Context, id int64, reqEditors ... } } -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int +const DateFormat = "2006-01-02" -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) +type Date struct { + time.Time +} -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error +func (d Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Format(DateFormat)) } -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" +func (d *Date) UnmarshalJSON(data []byte) error { + var dateStr string + err := json.Unmarshal(data, &dateStr) + if err != nil { + return err + } + parsed, err := time.Parse(DateFormat, dateStr) + if err != nil { + return err + } + d.Time = parsed + return nil +} -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time +func (d Date) String() string { + return d.Format(DateFormat) } -// UnmarshalText implements encoding.TextUnmarshaler for Date. func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) + parsed, err := time.Parse(DateFormat, string(data)) if err != nil { return err } - d.Time = t + d.Time = parsed return nil } @@ -542,10 +547,31 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } +type UUID = uuid.UUID + +// --------------------------------------------------------------------------- +// Deep object internals +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Exploded object binding +// --------------------------------------------------------------------------- + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + // primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement +// It handles basic Go types, time.Time, Date, and types that implement // json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { +func primitiveToString(value any) (string, error) { // Check for known types first (time, date, uuid) if res, ok := marshalKnownTypes(value); ok { return res, nil @@ -585,7 +611,7 @@ func primitiveToString(value interface{}) (string, error) { } e := json.NewDecoder(bytes.NewReader(buf)) e.UseNumber() - var i2 interface{} + var i2 any if err = e.Decode(&i2); err != nil { return "", fmt.Errorf("failed to decode JSON: %w", err) } @@ -601,7 +627,7 @@ func primitiveToString(value interface{}) (string, error) { } // marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { +func marshalKnownTypes(value any) (string, bool) { v := reflect.Indirect(reflect.ValueOf(value)) t := v.Type() @@ -626,11 +652,25 @@ func marshalKnownTypes(value interface{}) (string, bool) { return "", false } +// escapeParameterName escapes a parameter name for use in query strings and +// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) +// are properly percent-encoded per RFC 3986. +func escapeParameterName(name string, paramLocation ParamLocation) string { + // Parameter names should always be encoded regardless of allowReserved, + // which only applies to values per the OpenAPI spec. + return escapeParameterString(name, paramLocation, false) +} + // escapeParameterString escapes a parameter value based on its location. // Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { +// When allowReserved is true and the location is query, RFC 3986 reserved +// characters are left unencoded per the OpenAPI allowReserved specification. +func escapeParameterString(value string, paramLocation ParamLocation, allowReserved bool) string { switch paramLocation { case ParamLocationQuery: + if allowReserved { + return escapeQueryAllowReserved(value) + } return url.QueryEscape(value) case ParamLocationPath: return url.PathEscape(value) @@ -639,16 +679,30 @@ func escapeParameterString(value string, paramLocation ParamLocation) string { } } -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil +// escapeQueryAllowReserved percent-encodes a query parameter value while +// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as +// specified by OpenAPI's allowReserved parameter option. +func escapeQueryAllowReserved(value string) string { + const reserved = `:/?#[]@!$&'()*+,;=` + + var buf strings.Builder + for _, b := range []byte(value) { + if isUnreserved(b) || strings.IndexByte(reserved, b) >= 0 { + buf.WriteByte(b) + } else { + fmt.Fprintf(&buf, "%%%02X", b) + } } + return buf.String() +} + +// isUnreserved reports whether the byte is an RFC 3986 unreserved character: +// ALPHA / DIGIT / "-" / "." / "_" / "~" +func isUnreserved(c byte) bool { + return (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '.' || c == '_' || c == '~' } // sortedKeys returns the keys of a map in sorted order. @@ -661,293 +715,39 @@ func sortedKeys(m map[string]string) []string { return keys } -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil +// isByteSlice reports whether t is []byte (or equivalently []uint8). +func isByteSlice(t reflect.Type) bool { + return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 } -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t +// ParameterOptions carries OpenAPI parameter metadata to bind and style +// functions so they can handle style dispatch, explode, required, +// type-aware coercions, and location-aware escaping from a single +// uniform call site. All fields have sensible zero-value defaults. +type ParameterOptions struct { + Style string // OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" + ParamLocation ParamLocation // Where the parameter appears: query, path, header, cookie + Explode bool + Required bool + Type string // OpenAPI type: "string", "integer", "array", "object" + Format string // OpenAPI format: "int32", "date-time", etc. + AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) +// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. +// This is the entry point for client-side parameter serialization. The Style +// field in opts selects the serialization format. If Style is empty, "simple" +// is assumed. +func StyleParameter(paramName string, value any, opts ParameterOptions) (string, error) { + style := opts.Style + if style == "" { + style = "simple" } - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { t := reflect.TypeOf(value) v := reflect.ValueOf(value) - // Dereference pointers + // Dereference pointers; error on nil. if t.Kind() == reflect.Ptr { if v.IsNil() { return "", fmt.Errorf("value is a nil pointer") @@ -956,270 +756,342 @@ func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value t = v.Type() } - // Check for TextMarshaler (but not time.Time or Date) + // If the value implements encoding.TextMarshaler, use it — but not for + // time.Time or Date which have their own formatting logic. if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + it := reflect.Indirect(reflect.ValueOf(value)).Type() + if !it.ConvertibleTo(reflect.TypeOf(time.Time{})) && !it.ConvertibleTo(reflect.TypeOf(Date{})) { b, err := tu.MarshalText() if err != nil { return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + return stylePrimitive(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, string(b)) } } switch t.Kind() { case reflect.Slice: + if opts.Format == "byte" && isByteSlice(t) { + encoded := base64.StdEncoding.EncodeToString(v.Bytes()) + return stylePrimitive(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, encoded) + } n := v.Len() - sliceVal := make([]interface{}, n) + sliceVal := make([]any, n) for i := 0; i < n; i++ { sliceVal[i] = v.Index(i).Interface() } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + return styleSlice(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, sliceVal) case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) + return styleStruct(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, value) case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) + return styleMap(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, value) default: - return styleFormExplodePrimitive(paramName, paramLocation, value) + return stylePrimitive(style, opts.Explode, paramName, opts.ParamLocation, opts.AllowReserved, value) } } -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err +// --------------------------------------------------------------------------- +// Internal style helpers +// --------------------------------------------------------------------------- + +func styleSlice(style string, explode bool, paramName string, paramLocation ParamLocation, allowReserved bool, values []any) (string, error) { + if style == "deepObject" { + if !explode { + return "", errors.New("deepObjects must be exploded") + } + return MarshalDeepObject(values, paramName) + } + + var prefix string + var separator string + + escapedName := escapeParameterName(paramName, paramLocation) + + switch style { + case "simple": + separator = "," + case "label": + prefix = "." + if explode { + separator = "." + } else { + separator = "," + } + case "matrix": + prefix = fmt.Sprintf(";%s=", escapedName) + if explode { + separator = prefix + } else { + separator = "," + } + case "form": + prefix = fmt.Sprintf("%s=", escapedName) + if explode { + separator = "&" + prefix + } else { + separator = "," + } + case "spaceDelimited": + prefix = fmt.Sprintf("%s=", escapedName) + if explode { + separator = "&" + prefix + } else { + separator = " " + } + case "pipeDelimited": + prefix = fmt.Sprintf("%s=", escapedName) + if explode { + separator = "&" + prefix + } else { + separator = "|" + } + default: + return "", fmt.Errorf("unsupported style '%s'", style) } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) parts := make([]string, len(values)) for i, v := range values { part, err := primitiveToString(v) if err != nil { return "", fmt.Errorf("error formatting '%s': %w", paramName, err) } - parts[i] = escapeParameterString(part, paramLocation) + parts[i] = escapeParameterString(part, paramLocation, allowReserved) } - return prefix + strings.Join(parts, "&"+prefix), nil + return prefix + strings.Join(parts, separator), nil } -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first +func styleStruct(style string, explode bool, paramName string, paramLocation ParamLocation, allowReserved bool, value any) (string, error) { if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + return stylePrimitive(style, explode, paramName, paramLocation, allowReserved, timeVal) } - // Check for json.Marshaler + if style == "deepObject" { + if !explode { + return "", errors.New("deepObjects must be exploded") + } + return MarshalDeepObject(value, paramName) + } + + // If input implements json.Marshaler (e.g. objects with additional properties + // or anyOf), marshal to JSON and re-style the generic structure. if m, ok := value.(json.Marshaler); ok { buf, err := m.MarshalJSON() if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) + return "", fmt.Errorf("failed to marshal input to JSON: %w", err) } - var i2 interface{} e := json.NewDecoder(bytes.NewReader(buf)) e.UseNumber() - if err = e.Decode(&i2); err != nil { + var i2 any + err = e.Decode(&i2) + if err != nil { return "", fmt.Errorf("failed to unmarshal JSON: %w", err) } - return StyleFormExplodeParam(paramName, paramLocation, i2) + return StyleParameter(paramName, i2, ParameterOptions{ + Style: style, + ParamLocation: paramLocation, + Explode: explode, + AllowReserved: allowReserved, + }) } - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} + // Build a dictionary of the struct's fields. + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) + // Skip nil optional fields. + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) if err != nil { return "", fmt.Errorf("error formatting '%s': %w", paramName, err) } fieldDict[fieldName] = str } - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil + return processFieldDict(style, explode, paramName, paramLocation, allowReserved, fieldDict) } -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) +func styleMap(style string, explode bool, paramName string, paramLocation ParamLocation, allowReserved bool, value any) (string, error) { + if style == "deepObject" { + if !explode { + return "", errors.New("deepObjects must be exploded") + } + return MarshalDeepObject(value, paramName) + } v := reflect.ValueOf(value) - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") + fieldDict := make(map[string]string) + for _, fieldName := range v.MapKeys() { + str, err := primitiveToString(v.MapIndex(fieldName).Interface()) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) } - v = reflect.Indirect(v) - t = v.Type() + fieldDict[fieldName.String()] = str } + return processFieldDict(style, explode, paramName, paramLocation, allowReserved, fieldDict) +} - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) +func processFieldDict(style string, explode bool, paramName string, paramLocation ParamLocation, allowReserved bool, fieldDict map[string]string) (string, error) { + var parts []string + + if style != "deepObject" { + if explode { + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation, allowReserved) + parts = append(parts, k+"="+v) + } + } else { + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation, allowReserved) + parts = append(parts, k) + parts = append(parts, v) } - return escapeParameterString(string(b), paramLocation), nil } } - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() + escapedName := escapeParameterName(paramName, paramLocation) + + var prefix string + var separator string + + switch style { + case "simple": + separator = "," + case "label": + prefix = "." + if explode { + separator = prefix + } else { + separator = "," } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) + case "matrix": + if explode { + separator = ";" + prefix = ";" + } else { + separator = "," + prefix = fmt.Sprintf(";%s=", escapedName) + } + case "form": + if explode { + separator = "&" + } else { + prefix = fmt.Sprintf("%s=", escapedName) + separator = "," + } + case "deepObject": + if !explode { + return "", fmt.Errorf("deepObject parameters must be exploded") + } + for _, k := range sortedKeys(fieldDict) { + v := fieldDict[k] + part := fmt.Sprintf("%s[%s]=%s", escapedName, k, v) + parts = append(parts, part) + } + separator = "&" default: - return styleSimplePrimitive(paramLocation, value) + return "", fmt.Errorf("unsupported style '%s'", style) } + + return prefix + strings.Join(parts, separator), nil } -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { +func stylePrimitive(style string, explode bool, paramName string, paramLocation ParamLocation, allowReserved bool, value any) (string, error) { strVal, err := primitiveToString(value) if err != nil { return "", err } - return escapeParameterString(strVal, paramLocation), nil -} -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) + escapedName := escapeParameterName(paramName, paramLocation) + + var prefix string + switch style { + case "simple": + case "label": + prefix = "." + case "matrix": + prefix = fmt.Sprintf(";%s=", escapedName) + case "form": + prefix = fmt.Sprintf("%s=", escapedName) + default: + return "", fmt.Errorf("unsupported style '%s'", style) } - return strings.Join(parts, ","), nil + return prefix + escapeParameterString(strVal, paramLocation, allowReserved), nil } -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } +// --------------------------------------------------------------------------- +// Deep object marshaling +// --------------------------------------------------------------------------- - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) +// MarshalDeepObject marshals an object to deepObject style query parameters. +func MarshalDeepObject(i any, paramName string) (string, error) { + buf, err := json.Marshal(i) if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) + return "", fmt.Errorf("failed to marshal input to JSON: %w", err) } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") + var i2 any + err = json.Unmarshal(buf, &i2) + if err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str + fields, err := marshalDeepObjectRecursive(i2, nil) + if err != nil { + return "", fmt.Errorf("error traversing JSON structure: %w", err) } - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) + for idx := range fields { + fields[idx] = paramName + fields[idx] } - return strings.Join(parts, ","), nil + return strings.Join(fields, "&"), nil } -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) +func marshalDeepObjectRecursive(in any, path []string) ([]string, error) { + var result []string - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] + switch t := in.(type) { + case []any: + for i, iface := range t { + newPath := append(path, strconv.Itoa(i)) + fields, err := marshalDeepObjectRecursive(iface, newPath) + if err != nil { + return nil, fmt.Errorf("error traversing array: %w", err) } + result = append(result, fields...) } - f := v.Field(i) + case map[string]any: + keys := make([]string, 0, len(t)) + for k := range t { + keys = append(keys, k) + } + sort.Strings(keys) - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue + for _, k := range keys { + newPath := append(path, k) + fields, err := marshalDeepObjectRecursive(t[k], newPath) + if err != nil { + return nil, fmt.Errorf("error traversing map: %w", err) + } + result = append(result, fields...) } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + default: + prefix := "[" + strings.Join(path, "][") + "]" + result = []string{ + prefix + fmt.Sprintf("=%v", t), } - fieldDict[fieldName] = str } - return fieldDict, nil + return result, nil } diff --git a/examples/petstore-expanded/echo-v4/go.mod b/examples/petstore-expanded/echo-v4/go.mod index 6ffea79..cf57161 100644 --- a/examples/petstore-expanded/echo-v4/go.mod +++ b/examples/petstore-expanded/echo-v4/go.mod @@ -1,22 +1,22 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/echo-v4 -go 1.24.0 +go 1.25.0 require ( - github.com/labstack/echo/v4 v4.13.3 + github.com/labstack/echo/v4 v4.15.1 github.com/oapi-codegen/oapi-codegen-exp v0.0.0 ) require ( github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect ) replace github.com/oapi-codegen/oapi-codegen-exp => ../../../ diff --git a/examples/petstore-expanded/echo-v4/go.sum b/examples/petstore-expanded/echo-v4/go.sum index 14d2b5b..b13d4e4 100644 --- a/examples/petstore-expanded/echo-v4/go.sum +++ b/examples/petstore-expanded/echo-v4/go.sum @@ -1,14 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= -github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= +github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -17,15 +16,13 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/petstore-expanded/echo-v4/server/server.gen.go b/examples/petstore-expanded/echo-v4/server/server.gen.go index c6087ea..e8ac192 100644 --- a/examples/petstore-expanded/echo-v4/server/server.gen.go +++ b/examples/petstore-expanded/echo-v4/server/server.gen.go @@ -268,50 +268,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -580,21 +536,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -643,9 +584,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -970,39 +908,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1015,8 +920,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1179,12 +1082,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1222,10 +1119,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1240,36 +1133,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/echo/go.sum b/examples/petstore-expanded/echo/go.sum index a4cf87a..c8f0ef9 100644 --- a/examples/petstore-expanded/echo/go.sum +++ b/examples/petstore-expanded/echo/go.sum @@ -8,7 +8,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/petstore-expanded/echo/server/server.gen.go b/examples/petstore-expanded/echo/server/server.gen.go index 7279bc0..8f56add 100644 --- a/examples/petstore-expanded/echo/server/server.gen.go +++ b/examples/petstore-expanded/echo/server/server.gen.go @@ -269,50 +269,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -581,21 +537,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -644,9 +585,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -971,39 +909,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1016,8 +921,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1180,12 +1083,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1223,10 +1120,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1241,36 +1134,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/fiber/go.mod b/examples/petstore-expanded/fiber/go.mod index cf93c1b..70c7867 100644 --- a/examples/petstore-expanded/fiber/go.mod +++ b/examples/petstore-expanded/fiber/go.mod @@ -1,31 +1,29 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/fiber -go 1.24.0 +go 1.25.0 require ( - github.com/gofiber/fiber/v3 v3.0.0-beta.4 + github.com/gofiber/fiber/v3 v3.1.0 github.com/oapi-codegen/oapi-codegen-exp v0.0.0 ) require ( - github.com/andybalholm/brotli v1.1.1 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gofiber/schema v1.2.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect + github.com/gofiber/schema v1.7.0 // indirect + github.com/gofiber/utils/v2 v2.0.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/tinylib/msgp v1.2.5 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.58.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.33.0 // indirect + github.com/valyala/fasthttp v1.70.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect ) replace github.com/oapi-codegen/oapi-codegen-exp => ../../../ diff --git a/examples/petstore-expanded/fiber/go.sum b/examples/petstore-expanded/fiber/go.sum index 00a56bd..f6f5cf1 100644 --- a/examples/petstore-expanded/fiber/go.sum +++ b/examples/petstore-expanded/fiber/go.sum @@ -1,51 +1,48 @@ -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= -github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= -github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= -github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= -github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= -github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= +github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= +github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= +github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= +github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI= +github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= +github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= -github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= +github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/petstore-expanded/fiber/server/server.gen.go b/examples/petstore-expanded/fiber/server/server.gen.go index 88ac3f0..aa0a8c4 100644 --- a/examples/petstore-expanded/fiber/server/server.gen.go +++ b/examples/petstore-expanded/fiber/server/server.gen.go @@ -264,50 +264,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -576,21 +532,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -639,9 +580,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -966,39 +904,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1011,8 +916,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1175,12 +1078,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1218,10 +1115,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1236,36 +1129,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/generate.go b/examples/petstore-expanded/generate.go index 356f2a5..1f70173 100644 --- a/examples/petstore-expanded/generate.go +++ b/examples/petstore-expanded/generate.go @@ -1,4 +1,5 @@ //go:generate go run github.com/oapi-codegen/oapi-codegen-exp/cmd/oapi-codegen -config models.config.yaml petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen-exp/cmd/oapi-codegen -config client/client.config.yaml -output client/client.gen.go petstore-expanded.yaml //go:generate go run github.com/oapi-codegen/oapi-codegen-exp/cmd/oapi-codegen -config stdhttp/server/server.config.yaml -output stdhttp/server/server.gen.go petstore-expanded.yaml //go:generate go run github.com/oapi-codegen/oapi-codegen-exp/cmd/oapi-codegen -config chi/server/server.config.yaml -output chi/server/server.gen.go petstore-expanded.yaml //go:generate go run github.com/oapi-codegen/oapi-codegen-exp/cmd/oapi-codegen -config echo-v4/server/server.config.yaml -output echo-v4/server/server.gen.go petstore-expanded.yaml diff --git a/examples/petstore-expanded/gin/go.mod b/examples/petstore-expanded/gin/go.mod index 6ed1039..1fa730e 100644 --- a/examples/petstore-expanded/gin/go.mod +++ b/examples/petstore-expanded/gin/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/gin -go 1.24.0 +go 1.25.0 require ( github.com/gin-gonic/gin v1.10.0 @@ -31,7 +31,7 @@ require ( golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/text v0.36.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/petstore-expanded/gin/go.sum b/examples/petstore-expanded/gin/go.sum index e960d70..20fb4af 100644 --- a/examples/petstore-expanded/gin/go.sum +++ b/examples/petstore-expanded/gin/go.sum @@ -75,8 +75,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/examples/petstore-expanded/gin/server/server.gen.go b/examples/petstore-expanded/gin/server/server.gen.go index db90230..a2e4923 100644 --- a/examples/petstore-expanded/gin/server/server.gen.go +++ b/examples/petstore-expanded/gin/server/server.gen.go @@ -302,50 +302,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -614,21 +570,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -677,9 +618,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -1004,39 +942,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1049,8 +954,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1213,12 +1116,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1256,10 +1153,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1274,36 +1167,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/gorilla/go.mod b/examples/petstore-expanded/gorilla/go.mod index 7dcbdbe..e3f4f5f 100644 --- a/examples/petstore-expanded/gorilla/go.mod +++ b/examples/petstore-expanded/gorilla/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/gorilla -go 1.24.0 +go 1.25.0 require ( github.com/gorilla/mux v1.8.1 diff --git a/examples/petstore-expanded/gorilla/server/server.gen.go b/examples/petstore-expanded/gorilla/server/server.gen.go index 7d47e99..e5831bd 100644 --- a/examples/petstore-expanded/gorilla/server/server.gen.go +++ b/examples/petstore-expanded/gorilla/server/server.gen.go @@ -330,50 +330,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -642,21 +598,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -705,9 +646,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -1032,39 +970,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1077,8 +982,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1241,12 +1144,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1284,10 +1181,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1302,36 +1195,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/iris/go.mod b/examples/petstore-expanded/iris/go.mod index 7fef3c6..d70c72a 100644 --- a/examples/petstore-expanded/iris/go.mod +++ b/examples/petstore-expanded/iris/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/iris -go 1.24.0 +go 1.25.0 require ( github.com/kataras/iris/v12 v12.2.11 @@ -45,7 +45,7 @@ require ( golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/examples/petstore-expanded/iris/go.sum b/examples/petstore-expanded/iris/go.sum index a06439c..e49c87f 100644 --- a/examples/petstore-expanded/iris/go.sum +++ b/examples/petstore-expanded/iris/go.sum @@ -152,8 +152,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/examples/petstore-expanded/iris/server/server.gen.go b/examples/petstore-expanded/iris/server/server.gen.go index b05edf6..20379e5 100644 --- a/examples/petstore-expanded/iris/server/server.gen.go +++ b/examples/petstore-expanded/iris/server/server.gen.go @@ -268,50 +268,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -580,21 +536,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -643,9 +584,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -970,39 +908,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1015,8 +920,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1179,12 +1082,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1222,10 +1119,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1240,36 +1133,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/petstore.gen.go b/examples/petstore-expanded/petstore.gen.go index dd67943..3cdcfca 100644 --- a/examples/petstore-expanded/petstore.gen.go +++ b/examples/petstore-expanded/petstore.gen.go @@ -123,258 +123,18 @@ func GetOpenAPISpecJSON() ([]byte, error) { return openAPISpec() } -// MarshalText implements encoding.TextMarshaler for Date. - -// Format returns the date formatted according to layout. - -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - -// BindParameter binds a styled parameter from a single string value to a Go -// object. This is the entry point for path, header, and cookie parameters -// where the HTTP framework has already extracted the raw value. -// -// The Style field in opts selects how the value is split into parts (simple, -// label, matrix, form). If Style is empty, "simple" is assumed. - -// Unescape based on parameter location. - -// If the destination implements encoding.TextUnmarshaler, use it directly. - -// Primitive types need style-specific prefix stripping before binding. -// Label and matrix use splitStyledParameter for their prefix formats. -// Form style adds a "name=" prefix (e.g. "p=5") which is meaningful in -// query strings but must be stripped for cookie/header values. We use -// TrimPrefix instead of splitStyledParameter to avoid splitting on commas, -// which would break string primitives containing literal commas. - -// BindQueryParameter binds a query parameter from pre-parsed url.Values. -// The Style field in opts selects parsing behavior. If Style is empty, "form" -// is assumed. Supports form, spaceDelimited, pipeDelimited, and deepObject. - -// Destination value management for optional (pointer) parameters. - -// Exploded: each value is a separate key=value pair. -// spaceDelimited and pipeDelimited with explode=true are -// serialized identically to form explode=true. - -// Non-exploded: single value, delimiter-separated. - -// Primitive types: use the raw value as-is without splitting. - -// Some struct types (e.g. Date, time.Time) are scalar values -// that should be bound from a single string, not decomposed as -// key-value objects. - -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- -// unmarshalDeepObject is the internal implementation of deep object -// unmarshaling that supports the required parameter. - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - // --------------------------------------------------------------------------- // Exploded object binding // --------------------------------------------------------------------------- -// bindParamsToExplodedObject reflects the destination structure and pulls the -// value for each settable field from the given query parameters. Returns -// whether any fields were bound. - -// indirectBinder checks if dest implements Binder and returns reflect values. - -// ParamLocation indicates where a parameter is located in an HTTP request. - -// Binder is an interface for types that can bind themselves from a string value. - -// MissingRequiredParameterError is returned when a required parameter is not -// present in the request. Upper layers can use errors.As to detect this and -// produce an appropriate HTTP error response. - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - -// unescapeParameterString unescapes a parameter value based on its location. - -// sortedKeys returns the keys of a map in sorted order. - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. - -// Check for TextUnmarshaler - -// Check for Binder interface - -// Try JSON unmarshal as a fallback - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. - -// splitStyledParameter splits a styled parameter string value into parts based -// on the OpenAPI style. The object flag indicates whether the destination is a -// struct/map (affects matrix explode handling). - -// In the simple case, we always split on comma - -// Exploded: .a.b.c or .key=value.key=value - -// Unexploded: .a,b,c - -// Exploded: ;a;b;c or ;key=value;key=value - -// Unexploded: ;paramName=a,b,c - -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - -// isByteSlice reports whether t is []byte (or equivalently []uint8). - -// base64Decode decodes s as base64. -// -// Per OpenAPI 3.0, format: byte uses RFC 4648 Section 4 (standard alphabet, -// padded). We use padding presence to select the right decoder, rather than -// blindly cascading (which can produce corrupt output when RawStdEncoding -// silently accepts padded input and treats '=' as data). - -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - -// ParameterOptions carries OpenAPI parameter metadata to bind and style -// functions so they can handle style dispatch, explode, required, -// type-aware coercions, and location-aware escaping from a single -// uniform call site. All fields have sensible zero-value defaults. - -// OpenAPI style: "simple", "form", "label", "matrix", "deepObject", "pipeDelimited", "spaceDelimited" -// Where the parameter appears: query, path, header, cookie - -// OpenAPI type: "string", "integer", "array", "object" -// OpenAPI format: "int32", "date-time", etc. -// When true, reserved characters in query values are not percent-encoded - -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/examples/petstore-expanded/stdhttp/go.mod b/examples/petstore-expanded/stdhttp/go.mod index 9a2123c..fa77482 100644 --- a/examples/petstore-expanded/stdhttp/go.mod +++ b/examples/petstore-expanded/stdhttp/go.mod @@ -1,5 +1,5 @@ module github.com/oapi-codegen/oapi-codegen-exp/examples/petstore-expanded/stdhttp -go 1.24.0 +go 1.25.0 replace github.com/oapi-codegen/oapi-codegen-exp => ../../../ diff --git a/examples/petstore-expanded/stdhttp/server/server.gen.go b/examples/petstore-expanded/stdhttp/server/server.gen.go index c5752b6..3e104a4 100644 --- a/examples/petstore-expanded/stdhttp/server/server.gen.go +++ b/examples/petstore-expanded/stdhttp/server/server.gen.go @@ -330,50 +330,6 @@ func (d Date) Format(layout string) string { return d.Time.Format(layout) } -// ErrValidationEmail is the sentinel error returned when an email fails validation - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value - -// NewNullableWithValue creates a Nullable with the given value. - -// NewNullNullable creates a Nullable that is explicitly null. - -// Get returns the value if set, or an error if null or unspecified. - -// MustGet returns the value or panics if null or unspecified. - -// Set assigns a value. - -// SetNull marks the field as explicitly null. - -// SetUnspecified clears the field (as if it was never set). - -// IsNull returns true if the field is explicitly null. - -// IsSpecified returns true if the field was provided (either null or a value). - -// MarshalJSON implements json.Marshaler. - -// Unspecified - this shouldn't be called if omitempty is used correctly - -// UnmarshalJSON implements json.Unmarshaler. - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. - // BindParameter binds a styled parameter from a single string value to a Go // object. This is the entry point for path, header, and cookie parameters // where the HTTP framework has already extracted the raw value. @@ -642,21 +598,6 @@ func BindQueryParameter(paramName string, queryParams url.Values, dest any, opts } } -// BindRawQueryParameter works like BindQueryParameter but operates on the raw -// (undecoded) query string. This correctly handles form/explode=false -// parameters whose values contain literal commas encoded as %2C — something -// that BindQueryParameter cannot do because url.Values has already decoded -// %2C to ',' before we can split on the delimiter comma. - -// For explode, url.ParseQuery is fine — no delimiter commas to -// confuse with literal commas. - -// explode=false — use findRawQueryParam to get the still-encoded -// value, split on the style-specific delimiter, then URL-decode -// each resulting part individually. - -// Primitive types: decode as-is without splitting. - // --------------------------------------------------------------------------- // Deep object internals // --------------------------------------------------------------------------- @@ -705,9 +646,6 @@ func unmarshalDeepObject(dst any, paramName string, params url.Values, required return nil } -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a -// destination. Exported for use by generated code and tests. - type fieldOrValue struct { fields map[string]fieldOrValue value string @@ -1032,39 +970,6 @@ func (e *MissingRequiredParameterError) Error() string { return fmt.Sprintf("parameter '%s' is required", e.ParamName) } -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, Date, and types that implement -// json.Marshaler or fmt.Stringer. - -// Check for known types first (time, date, uuid) - -// Dereference pointers for optional values - -// Check if it's a UUID - -// Check if it implements json.Marshaler - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. - -// escapeParameterName escapes a parameter name for use in query strings and -// paths. This ensures characters like [] in parameter names (e.g. user_ids[]) -// are properly percent-encoded per RFC 3986. - -// Parameter names should always be encoded regardless of allowReserved, -// which only applies to values per the OpenAPI spec. - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -// When allowReserved is true and the location is query, RFC 3986 reserved -// characters are left unencoded per the OpenAPI allowReserved specification. - -// escapeQueryAllowReserved percent-encodes a query parameter value while -// leaving RFC 3986 reserved characters (:/?#[]@!$&'()*+,;=) unencoded, as -// specified by OpenAPI's allowReserved parameter option. - -// isUnreserved reports whether the byte is an RFC 3986 unreserved character: -// ALPHA / DIGIT / "-" / "." / "_" / "~" - // unescapeParameterString unescapes a parameter value based on its location. func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { switch paramLocation { @@ -1077,8 +982,6 @@ func unescapeParameterString(value string, paramLocation ParamLocation) (string, } } -// sortedKeys returns the keys of a map in sorted order. - // BindStringToObject binds a string value to a destination object. // It handles primitives, encoding.TextUnmarshaler, and the Binder interface. func BindStringToObject(src string, dst any) error { @@ -1241,12 +1144,6 @@ func splitStyledParameter(style string, explode bool, object bool, paramName str return nil, fmt.Errorf("unhandled parameter style: %s", style) } -// findRawQueryParam extracts values for a named parameter from a raw -// (undecoded) query string. The parameter key is decoded for comparison -// purposes, but the returned values remain in their original encoded form. - -// Skip malformed keys. - // isByteSlice reports whether t is []byte (or equivalently []uint8). func isByteSlice(t reflect.Type) bool { return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 @@ -1284,10 +1181,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. - -// Skip nil optional fields - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single @@ -1302,36 +1195,10 @@ type ParameterOptions struct { AllowReserved bool // When true, reserved characters in query values are not percent-encoded } -// StyleParameter serializes a Go value into an OpenAPI-styled parameter string. -// This is the entry point for client-side parameter serialization. The Style -// field in opts selects the serialization format. If Style is empty, "simple" -// is assumed. - -// Dereference pointers; error on nil. - -// If the value implements encoding.TextMarshaler, use it — but not for -// time.Time or Date which have their own formatting logic. - // --------------------------------------------------------------------------- // Internal style helpers // --------------------------------------------------------------------------- -// If input implements json.Marshaler (e.g. objects with additional properties -// or anyOf), marshal to JSON and re-style the generic structure. - -// Build a dictionary of the struct's fields. - -// Skip nil optional fields. - // --------------------------------------------------------------------------- // Deep object marshaling // --------------------------------------------------------------------------- - -// MarshalDeepObject marshals an object to deepObject style query parameters. - -// JSONMerge merges two JSON-encoded objects. Fields from patch override -// fields in base. Both arguments must be valid JSON objects (or nil/null). - -// MarshalForm marshals a struct into url.Values using the struct's json tags -// as field names. It handles nested structs, slices, pointers, and -// AdditionalProperties maps. diff --git a/go.mod b/go.mod index 8d7be4b..8a70918 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,27 @@ module github.com/oapi-codegen/oapi-codegen-exp -go 1.24.0 +go 1.25.0 require ( github.com/google/uuid v1.6.0 - github.com/pb33f/libopenapi v0.33.5 + github.com/pb33f/libopenapi v0.36.1 github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v4 v4.0.0-rc.4 // required by libopenapi for Extensions type gopkg.in/yaml.v3 v3.0.1 ) require ( - golang.org/x/text v0.33.0 - golang.org/x/tools v0.41.0 + golang.org/x/text v0.36.0 + golang.org/x/tools v0.44.0 ) require ( github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pb33f/jsonpath v0.7.1 // indirect - github.com/pb33f/ordered-map/v2 v2.3.0 // indirect + github.com/pb33f/jsonpath v0.8.2 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index 28075cb..97e56b8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -12,12 +12,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pb33f/jsonpath v0.7.1 h1:dEp6oIZuJbpDSyuHAl9m7GonoDW4M20BcD5vT0tPYRE= -github.com/pb33f/jsonpath v0.7.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= -github.com/pb33f/libopenapi v0.33.5 h1:AzILVrOzMaawLFhQENmwmn7h/TIDH2QEgUd0PfxS2xE= -github.com/pb33f/libopenapi v0.33.5/go.mod h1:e/dmd2Pf1nkjqkI0r7guFSyt9T5V0IIQKgs0L6B/3b0= -github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= -github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= +github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= +github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.36.1 h1:CNZ52e+/W9fA1kAgL8EePDQQrKPfN9+HdLR6XAxUEpw= +github.com/pb33f/libopenapi v0.36.1/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -26,14 +26,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/runtime/params/params.gen.go b/runtime/params/params.gen.go index 4c8b18d..e63b8f7 100644 --- a/runtime/params/params.gen.go +++ b/runtime/params/params.gen.go @@ -1224,37 +1224,6 @@ func base64Decode1(enc *base64.Encoding, s string) ([]byte, error) { return b, nil } -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value any) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} - // ParameterOptions carries OpenAPI parameter metadata to bind and style // functions so they can handle style dispatch, explode, required, // type-aware coercions, and location-aware escaping from a single