@@ -16,6 +16,7 @@ import (
1616 "fmt"
1717 "net/url"
1818 "sort"
19+ "strconv"
1920 "strings"
2021)
2122
@@ -69,21 +70,173 @@ func EncodeFormFields(data any, encoding map[string]FieldEncoding) (string, erro
6970 return values .Encode (), nil
7071}
7172
72- // ConvertFormFields converts a raw form-encoded response body to JSON format.
73- // It parses the form-encoded data, converts it to a map, and then marshals it to JSON.
73+ // ConvertFormFields converts a raw form-encoded request body to JSON format.
74+ // It handles deepObject encoding (e.g., "obj[key][nested]=value") and converts
75+ // string values to appropriate types (bool, number, etc.).
7476func ConvertFormFields (resp []byte ) ([]byte , error ) {
7577 values , err := url .ParseQuery (string (resp ))
7678 if err != nil {
7779 return nil , fmt .Errorf ("error parsing form-encoded body: %w" , err )
7880 }
7981
80- data := make (map [string ]any , len (values ))
81- for key := range values {
82- data [key ] = values .Get (key )
82+ data := decodeFormData (values )
83+ return json .Marshal (data )
84+ }
85+
86+ // decodeFormData decodes URL-encoded form data into a nested map structure.
87+ // It handles deepObject encoding (e.g., "obj[key][nested]=value") and converts
88+ // string values to appropriate types (bool, number, etc.).
89+ func decodeFormData (values url.Values ) map [string ]any {
90+ result := make (map [string ]any )
91+
92+ for key , vals := range values {
93+ // Check if this is a deepObject encoded key (contains brackets)
94+ if strings .Contains (key , "[" ) {
95+ setNestedValue (result , key , vals )
96+ } else {
97+ // Simple key-value pair
98+ if len (vals ) == 1 {
99+ result [key ] = convertFormStringValue (vals [0 ])
100+ } else {
101+ // Multiple values for the same key (array)
102+ converted := make ([]any , len (vals ))
103+ for i , v := range vals {
104+ converted [i ] = convertFormStringValue (v )
105+ }
106+ result [key ] = converted
107+ }
108+ }
83109 }
84110
85- // Convert back to JSON
86- return json .Marshal (data )
111+ return result
112+ }
113+
114+ // setNestedValue sets a value in a nested map structure based on a deepObject encoded key.
115+ // Example: "obj[key][0][nested]=value" sets result["obj"]["key"][0]["nested"] = value
116+ func setNestedValue (result map [string ]any , key string , values []string ) {
117+ // Parse the key to extract the path
118+ // Example: "flow_data[subscription_update_confirm][items][0][id]"
119+ // becomes ["flow_data", "subscription_update_confirm", "items", "0", "id"]
120+ parts := parseDeepObjectKey (key )
121+ if len (parts ) == 0 {
122+ return
123+ }
124+
125+ // Special case: if only 2 parts and second is numeric, it's a simple array
126+ // Example: "expand[0]" should become expand: ["value"]
127+ if len (parts ) == 2 && isFormNumeric (parts [1 ]) {
128+ arr , ok := result [parts [0 ]].([]any )
129+ if ! ok {
130+ arr = make ([]any , 0 )
131+ }
132+
133+ idx := mustFormAtoi (parts [1 ])
134+ arr = ensureArraySize (arr , idx , false )
135+ arr [idx ] = convertFormValues (values )
136+ result [parts [0 ]] = arr
137+ return
138+ }
139+
140+ // Navigate/create the nested structure
141+ current := result
142+ for i := 0 ; i < len (parts )- 1 ; i ++ {
143+ part := parts [i ]
144+ nextPart := parts [i + 1 ]
145+
146+ // Check if next part is a number (array index)
147+ isNextArray := isFormNumeric (nextPart )
148+
149+ if isNextArray {
150+ // Current should be an array
151+ arr , ok := current [part ].([]any )
152+ if ! ok {
153+ arr = make ([]any , 0 )
154+ current [part ] = arr
155+ }
156+
157+ // Get the index
158+ idx := mustFormAtoi (nextPart )
159+
160+ // Check if this is the last level (nextPart is the last part)
161+ if i + 2 == len (parts ) {
162+ // This is the final value - just set it in the array
163+ arr = ensureArraySize (arr , idx , false )
164+ arr [idx ] = convertFormValues (values )
165+ current [part ] = arr
166+ return
167+ }
168+
169+ // Not the final value - need to navigate deeper
170+ arr = ensureArraySize (arr , idx , true )
171+ current [part ] = arr
172+
173+ // Move to the array element
174+ elem , ok := arr [idx ].(map [string ]any )
175+ if ! ok {
176+ elem = make (map [string ]any )
177+ arr [idx ] = elem
178+ }
179+ current = elem
180+ i ++ // Skip the index part
181+ } else {
182+ // Current should be an object
183+ next , ok := current [part ].(map [string ]any )
184+ if ! ok {
185+ next = make (map [string ]any )
186+ current [part ] = next
187+ }
188+ current = next
189+ }
190+ }
191+
192+ // Set the final value
193+ lastPart := parts [len (parts )- 1 ]
194+ current [lastPart ] = convertFormValues (values )
195+ }
196+
197+ // ensureArraySize grows the array to accommodate the given index.
198+ // If useMap is true, fills new slots with fresh map[string]any instances.
199+ // Otherwise, fills with nil.
200+ func ensureArraySize (arr []any , idx int , useMap bool ) []any {
201+ for len (arr ) <= idx {
202+ if useMap {
203+ arr = append (arr , make (map [string ]any ))
204+ } else {
205+ arr = append (arr , nil )
206+ }
207+ }
208+ return arr
209+ }
210+
211+ // convertFormValues converts form values to appropriate types.
212+ // Returns a single value if there's only one, or a slice if there are multiple.
213+ func convertFormValues (values []string ) any {
214+ if len (values ) == 1 {
215+ return convertFormStringValue (values [0 ])
216+ }
217+ converted := make ([]any , len (values ))
218+ for i , v := range values {
219+ converted [i ] = convertFormStringValue (v )
220+ }
221+ return converted
222+ }
223+
224+ // parseDeepObjectKey parses a deepObject encoded key into parts.
225+ // Example: "flow_data[subscription_update_confirm][items][0][id]"
226+ // Returns: ["flow_data", "subscription_update_confirm", "items", "0", "id"]
227+ func parseDeepObjectKey (key string ) []string {
228+ var parts []string
229+
230+ // Split by '[' and clean up ']'
231+ segments := strings .Split (key , "[" )
232+ for _ , seg := range segments {
233+ cleaned := strings .TrimSuffix (seg , "]" )
234+ if cleaned != "" {
235+ parts = append (parts , cleaned )
236+ }
237+ }
238+
239+ return parts
87240}
88241
89242func encodeForm (prefix string , value any , values url.Values , explode bool ) {
@@ -134,3 +287,55 @@ func encodeDeepObject(prefix string, value any, values url.Values) {
134287 values .Set (prefix , fmt .Sprintf ("%v" , v ))
135288 }
136289}
290+
291+ // convertFormStringValue attempts to convert a string value to its appropriate type.
292+ // Handles: bool, int, float, or keeps as string.
293+ // Note: We're conservative with conversions to avoid misinterpreting strings like phone numbers.
294+ func convertFormStringValue (s string ) any {
295+ // Try boolean
296+ if s == "true" {
297+ return true
298+ }
299+ if s == "false" {
300+ return false
301+ }
302+
303+ // Don't convert strings that start with + (likely phone numbers)
304+ if strings .HasPrefix (s , "+" ) {
305+ return s
306+ }
307+
308+ // Don't convert strings that contain spaces or parentheses (likely formatted values)
309+ if strings .ContainsAny (s , " ()" ) {
310+ return s
311+ }
312+
313+ // Try integer (only if it doesn't have leading zeros, which would indicate a string like "001")
314+ if len (s ) > 0 && (s [0 ] != '0' || s == "0" ) {
315+ if i , err := strconv .ParseInt (s , 10 , 64 ); err == nil {
316+ return i
317+ }
318+ }
319+
320+ // Try float (only if it contains a decimal point)
321+ if strings .Contains (s , "." ) {
322+ if f , err := strconv .ParseFloat (s , 64 ); err == nil {
323+ return f
324+ }
325+ }
326+
327+ // Keep as string
328+ return s
329+ }
330+
331+ // isFormNumeric checks if a string represents a number.
332+ func isFormNumeric (s string ) bool {
333+ _ , err := strconv .Atoi (s )
334+ return err == nil
335+ }
336+
337+ // mustFormAtoi converts a string to int (should only be called after isFormNumeric check).
338+ func mustFormAtoi (s string ) int {
339+ i , _ := strconv .Atoi (s )
340+ return i
341+ }
0 commit comments