@@ -5,28 +5,38 @@ package params
55import (
66 "encoding"
77 "fmt"
8+ "net/url"
89 "reflect"
910 "strings"
11+ "time"
12+
13+ "github.com/oapi-codegen/oapi-codegen-exp/codegen/internal/runtime/types"
1014)
1115
12- // BindFormParam binds a form-style parameter without explode to a destination.
13- // Form style is the default for query and cookie parameters.
14- // This function handles a single query parameter value (not url.Values).
15- // Arrays: a,b,c -> []string{"a", "b", "c"}
16- // Objects: key1,value1,key2,value2 -> struct{Key1, Key2}
17- func BindFormParam (paramName string , paramLocation ParamLocation , value string , dest any ) error {
16+ // BindFormParam binds a form-style parameter from a single string value.
17+ // Used for path, header, and cookie parameters where the value has already
18+ // been extracted from the request.
19+ //
20+ // Non-explode (default for form):
21+ //
22+ // Arrays: a,b,c -> []string{"a", "b", "c"}
23+ // Objects: key1,value1,key2,value2 -> struct{Key1, Key2}
24+ //
25+ // Explode:
26+ //
27+ // Primitives and arrays: same comma-separated format
28+ // Objects: key1=value1,key2=value2 -> struct{Key1, Key2}
29+ func BindFormParam (paramName string , value string , dest any , opts ParameterOptions ) error {
1830 if value == "" {
1931 return fmt .Errorf ("parameter '%s' is empty, can't bind its value" , paramName )
2032 }
2133
22- // Unescape based on location
2334 var err error
24- value , err = unescapeParameterString (value , paramLocation )
35+ value , err = unescapeParameterString (value , opts . ParamLocation )
2536 if err != nil {
2637 return fmt .Errorf ("error unescaping parameter '%s': %w" , paramName , err )
2738 }
2839
29- // Check for TextUnmarshaler
3040 if tu , ok := dest .(encoding.TextUnmarshaler ); ok {
3141 return tu .UnmarshalText ([]byte (value ))
3242 }
@@ -37,11 +47,168 @@ func BindFormParam(paramName string, paramLocation ParamLocation, value string,
3747 switch t .Kind () {
3848 case reflect .Struct :
3949 parts := strings .Split (value , "," )
40- return bindSplitPartsToDestinationStruct (paramName , parts , false , dest )
50+ return bindSplitPartsToDestinationStruct (paramName , parts , opts . Explode , dest )
4151 case reflect .Slice :
4252 parts := strings .Split (value , "," )
4353 return bindSplitPartsToDestinationArray (parts , dest )
4454 default :
4555 return BindStringToObject (value , dest )
4656 }
4757}
58+
59+ // BindFormQueryParam binds a form-style query parameter from url.Values.
60+ // The function looks up the parameter by name and handles both exploded
61+ // and non-exploded formats.
62+ //
63+ // Non-explode: ?param=a,b,c (single query key, comma-separated value)
64+ // Explode: ?param=a¶m=b¶m=c (multiple query keys)
65+ func BindFormQueryParam (paramName string , queryParams url.Values , dest any , opts ParameterOptions ) error {
66+ if opts .Explode {
67+ return bindFormExplodeQuery (paramName , queryParams , dest , opts )
68+ }
69+ // Non-explode: single value, comma-separated
70+ value := queryParams .Get (paramName )
71+ if value == "" {
72+ if opts .Required {
73+ return fmt .Errorf ("query parameter '%s' is required" , paramName )
74+ }
75+ return nil
76+ }
77+ return BindFormParam (paramName , value , dest , opts )
78+ }
79+
80+ // bindFormExplodeQuery handles the exploded form-style query parameter case.
81+ func bindFormExplodeQuery (paramName string , queryParams url.Values , dest any , opts ParameterOptions ) error {
82+ dv := reflect .Indirect (reflect .ValueOf (dest ))
83+ v := dv
84+ var output any
85+
86+ if opts .Required {
87+ output = dest
88+ } else {
89+ if v .IsNil () {
90+ t := v .Type ()
91+ newValue := reflect .New (t .Elem ())
92+ output = newValue .Interface ()
93+ } else {
94+ output = v .Interface ()
95+ }
96+ v = reflect .Indirect (reflect .ValueOf (output ))
97+ }
98+
99+ t := v .Type ()
100+ k := t .Kind ()
101+
102+ values , found := queryParams [paramName ]
103+
104+ switch k {
105+ case reflect .Slice :
106+ if ! found {
107+ if opts .Required {
108+ return fmt .Errorf ("query parameter '%s' is required" , paramName )
109+ }
110+ return nil
111+ }
112+ err := bindSplitPartsToDestinationArray (values , output )
113+ if err != nil {
114+ return err
115+ }
116+ case reflect .Struct :
117+ fieldsPresent , err := bindParamsToExplodedObject (paramName , queryParams , output )
118+ if err != nil {
119+ return err
120+ }
121+ if ! fieldsPresent {
122+ return nil
123+ }
124+ default :
125+ if len (values ) == 0 {
126+ if opts .Required {
127+ return fmt .Errorf ("query parameter '%s' is required" , paramName )
128+ }
129+ return nil
130+ }
131+ if len (values ) != 1 {
132+ return fmt .Errorf ("multiple values for single value parameter '%s'" , paramName )
133+ }
134+ if ! found {
135+ if opts .Required {
136+ return fmt .Errorf ("query parameter '%s' is required" , paramName )
137+ }
138+ return nil
139+ }
140+ err := BindStringToObject (values [0 ], output )
141+ if err != nil {
142+ return err
143+ }
144+ }
145+
146+ if ! opts .Required {
147+ dv .Set (reflect .ValueOf (output ))
148+ }
149+ return nil
150+ }
151+
152+ // bindParamsToExplodedObject binds query params to struct fields for exploded objects.
153+ func bindParamsToExplodedObject (paramName string , values url.Values , dest any ) (bool , error ) {
154+ binder , v , t := indirectBinder (dest )
155+ if binder != nil {
156+ _ , found := values [paramName ]
157+ if ! found {
158+ return false , nil
159+ }
160+ return true , BindStringToObject (values .Get (paramName ), dest )
161+ }
162+ if t .Kind () != reflect .Struct {
163+ return false , fmt .Errorf ("unmarshaling query arg '%s' into wrong type" , paramName )
164+ }
165+
166+ fieldsPresent := false
167+ for i := 0 ; i < t .NumField (); i ++ {
168+ fieldT := t .Field (i )
169+ if ! v .Field (i ).CanSet () {
170+ continue
171+ }
172+
173+ tag := fieldT .Tag .Get ("json" )
174+ fieldName := fieldT .Name
175+ if tag != "" {
176+ tagParts := strings .Split (tag , "," )
177+ if tagParts [0 ] != "" {
178+ fieldName = tagParts [0 ]
179+ }
180+ }
181+
182+ fieldVal , found := values [fieldName ]
183+ if found {
184+ if len (fieldVal ) != 1 {
185+ return false , fmt .Errorf ("field '%s' specified multiple times for param '%s'" , fieldName , paramName )
186+ }
187+ err := BindStringToObject (fieldVal [0 ], v .Field (i ).Addr ().Interface ())
188+ if err != nil {
189+ return false , fmt .Errorf ("could not bind query arg '%s': %w" , paramName , err )
190+ }
191+ fieldsPresent = true
192+ }
193+ }
194+ return fieldsPresent , nil
195+ }
196+
197+ // indirectBinder checks if dest implements Binder and returns reflect values.
198+ func indirectBinder (dest any ) (any , reflect.Value , reflect.Type ) {
199+ v := reflect .ValueOf (dest )
200+ if v .Type ().NumMethod () > 0 && v .CanInterface () {
201+ if u , ok := v .Interface ().(Binder ); ok {
202+ return u , reflect.Value {}, nil
203+ }
204+ }
205+ v = reflect .Indirect (v )
206+ t := v .Type ()
207+ if t .ConvertibleTo (reflect .TypeOf (time.Time {})) {
208+ return dest , reflect.Value {}, nil
209+ }
210+ if t .ConvertibleTo (reflect .TypeOf (types.Date {})) {
211+ return dest , reflect.Value {}, nil
212+ }
213+ return nil , v , t
214+ }
0 commit comments