Skip to content

Commit 39db7ef

Browse files
committed
1 parent 04189e5 commit 39db7ef

7 files changed

Lines changed: 540 additions & 116 deletions

helpers/parameter_utilities.go

Lines changed: 176 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -174,18 +174,83 @@ func cast(v string) any {
174174
return v
175175
}
176176

177+
// getPropertySchema looks up a property's schema from an object schema's Properties map.
178+
// Returns nil if objectSchema is nil, has no Properties, or the property is not found.
179+
func getPropertySchema(objectSchema *base.Schema, propertyName string) *base.Schema {
180+
if objectSchema == nil || objectSchema.Properties == nil {
181+
return nil
182+
}
183+
proxy := objectSchema.Properties.GetOrZero(propertyName)
184+
if proxy == nil {
185+
return nil
186+
}
187+
return proxy.Schema()
188+
}
189+
190+
// castWithSchema casts a string value consulting the schema for the property's declared type.
191+
// If the schema says the property is a string, the value is returned as-is (no numeric/bool guessing).
192+
// For other declared types, it falls back to cast() which produces correct results for integer,
193+
// number, and boolean values. The explicit string check prevents the most common miscast: numeric-
194+
// looking strings like "10" being converted to int64 when the schema declares type: string.
195+
func castWithSchema(v string, objectSchema *base.Schema, propertyName string) any {
196+
propSchema := getPropertySchema(objectSchema, propertyName)
197+
if propSchema != nil {
198+
for _, t := range propSchema.Type {
199+
if t == String {
200+
return v
201+
}
202+
}
203+
}
204+
return cast(v)
205+
}
206+
207+
// constructKVFromDelimited is the shared implementation for constructing key=value maps
208+
// from delimited strings (comma, period, semicolon). The delimiter determines how to split
209+
// entries, and each entry is further split on '=' to extract key-value pairs.
210+
func constructKVFromDelimited(values string, delimiter string, sch *base.Schema) map[string]interface{} {
211+
props := make(map[string]interface{})
212+
exploded := strings.Split(values, delimiter)
213+
for i := range exploded {
214+
obK := strings.Split(exploded[i], Equals)
215+
if len(obK) == 2 {
216+
props[obK[0]] = castWithSchema(obK[1], sch, obK[0])
217+
}
218+
}
219+
return props
220+
}
221+
222+
// constructParamMapFromDelimitedEncoding is the shared implementation for constructing
223+
// parameter maps from pipe-delimited or space-delimited query parameter values.
224+
// Entries alternate between keys and values (key|value|key|value or key value key value).
225+
func constructParamMapFromDelimitedEncoding(values []*QueryParam, delimiter string, sch *base.Schema) map[string]interface{} {
226+
decoded := make(map[string]interface{})
227+
for _, v := range values {
228+
props := make(map[string]interface{})
229+
exploded := strings.Split(v.Values[0], delimiter)
230+
for i := range exploded {
231+
if i%2 == 0 && i+1 < len(exploded) {
232+
props[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i])
233+
}
234+
}
235+
decoded[v.Key] = props
236+
}
237+
return decoded
238+
}
239+
177240
// ConstructParamMapFromDeepObjectEncoding will construct a map from the query parameters that are encoded as
178241
// deep objects. It's kind of a crazy way to do things, but hey, each to their own.
179242
func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam, sch *base.Schema) map[string]interface{} {
180-
// deepObject encoding is a technique used to encode objects into query parameters. Kinda nuts.
181243
decoded := make(map[string]interface{})
182244
for _, v := range values {
183-
if decoded[v.Key] == nil {
245+
castForProp := func(val string) any {
246+
return castWithSchema(val, sch, v.Property)
247+
}
184248

249+
if decoded[v.Key] == nil {
185250
props := make(map[string]interface{})
186251
rawValues := make([]interface{}, len(v.Values))
187252
for i := range v.Values {
188-
rawValues[i] = cast(v.Values[i])
253+
rawValues[i] = castForProp(v.Values[i])
189254
}
190255
// check if the schema for the param is an array
191256
if sch != nil && slices.Contains(sch.Type, Array) {
@@ -202,15 +267,14 @@ func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam, sch *base.Sch
202267
}
203268

204269
if len(props) == 0 {
205-
props[v.Property] = cast(v.Values[0])
270+
props[v.Property] = castForProp(v.Values[0])
206271
}
207272
decoded[v.Key] = props
208273
} else {
209-
210274
added := false
211275
rawValues := make([]interface{}, len(v.Values))
212276
for i := range v.Values {
213-
rawValues[i] = cast(v.Values[i])
277+
rawValues[i] = castForProp(v.Values[i])
214278
}
215279
// check if the schema for the param is an array
216280
if sch != nil && slices.Contains(sch.Type, Array) {
@@ -225,138 +289,70 @@ func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam, sch *base.Sch
225289
added = true
226290
}
227291
if !added {
228-
decoded[v.Key].(map[string]interface{})[v.Property] = cast(v.Values[0])
292+
decoded[v.Key].(map[string]interface{})[v.Property] = castForProp(v.Values[0])
229293
}
230-
231294
}
232295
}
233296
return decoded
234297
}
235298

236299
// ConstructParamMapFromQueryParamInput will construct a param map from an existing map of *QueryParam slices.
300+
//
301+
// Deprecated: use ConstructParamMapFromQueryParamInputWithSchema instead.
237302
func ConstructParamMapFromQueryParamInput(values map[string][]*QueryParam) map[string]interface{} {
238-
decoded := make(map[string]interface{})
239-
for _, q := range values {
240-
for _, v := range q {
241-
decoded[v.Key] = cast(v.Values[0])
242-
}
243-
}
244-
return decoded
303+
return ConstructParamMapFromQueryParamInputWithSchema(values, nil)
245304
}
246305

247306
// ConstructParamMapFromPipeEncoding will construct a map from the query parameters that are encoded as
248-
// pipe separated values. Perhaps the most sane way to delimit/encode properties.
307+
// pipe separated values.
308+
//
309+
// Deprecated: use ConstructParamMapFromPipeEncodingWithSchema instead.
249310
func ConstructParamMapFromPipeEncoding(values []*QueryParam) map[string]interface{} {
250-
// Pipes are always a good alternative to commas, personally I think they're better, if I were encoding, I would
251-
// use pipes instead of commas, so much can go wrong with a comma, but a pipe? hardly ever.
252-
decoded := make(map[string]interface{})
253-
for _, v := range values {
254-
props := make(map[string]interface{})
255-
// explode PSV into array
256-
exploded := strings.Split(v.Values[0], Pipe)
257-
for i := range exploded {
258-
if i%2 == 0 {
259-
props[exploded[i]] = cast(exploded[i+1])
260-
}
261-
}
262-
decoded[v.Key] = props
263-
}
264-
return decoded
311+
return ConstructParamMapFromPipeEncodingWithSchema(values, nil)
265312
}
266313

267314
// ConstructParamMapFromSpaceEncoding will construct a map from the query parameters that are encoded as
268-
// space delimited values. This is perhaps the worst way to delimit anything other than a paragraph of text.
315+
// space delimited values.
316+
//
317+
// Deprecated: use ConstructParamMapFromSpaceEncodingWithSchema instead.
269318
func ConstructParamMapFromSpaceEncoding(values []*QueryParam) map[string]interface{} {
270-
// Don't use spaces to delimit anything unless you really know what the hell you're doing. Perhaps the
271-
// easiest way to blow something up, unless you're tokenizing strings... don't do this.
272-
decoded := make(map[string]interface{})
273-
for _, v := range values {
274-
props := make(map[string]interface{})
275-
// explode SSV into array
276-
exploded := strings.Split(v.Values[0], Space)
277-
for i := range exploded {
278-
if i%2 == 0 {
279-
props[exploded[i]] = cast(exploded[i+1])
280-
}
281-
}
282-
decoded[v.Key] = props
283-
}
284-
return decoded
319+
return ConstructParamMapFromSpaceEncodingWithSchema(values, nil)
285320
}
286321

287322
// ConstructMapFromCSV will construct a map from a comma separated value string.
323+
//
324+
// Deprecated: use ConstructMapFromCSVWithSchema instead.
288325
func ConstructMapFromCSV(csv string) map[string]interface{} {
289-
decoded := make(map[string]interface{})
290-
// explode SSV into array
291-
exploded := strings.Split(csv, Comma)
292-
for i := range exploded {
293-
if i%2 == 0 {
294-
if len(exploded) == i+1 {
295-
break
296-
}
297-
decoded[exploded[i]] = cast(exploded[i+1])
298-
}
299-
}
300-
return decoded
326+
return ConstructMapFromCSVWithSchema(csv, nil)
301327
}
302328

303329
// ConstructKVFromCSV will construct a map from a comma separated value string that denotes key value pairs.
330+
//
331+
// Deprecated: use ConstructKVFromCSVWithSchema instead.
304332
func ConstructKVFromCSV(values string) map[string]interface{} {
305-
props := make(map[string]interface{})
306-
exploded := strings.Split(values, Comma)
307-
for i := range exploded {
308-
obK := strings.Split(exploded[i], Equals)
309-
if len(obK) == 2 {
310-
props[obK[0]] = cast(obK[1])
311-
}
312-
}
313-
return props
333+
return ConstructKVFromCSVWithSchema(values, nil)
314334
}
315335

316-
// ConstructKVFromLabelEncoding will construct a map from a comma separated value string that denotes key value pairs.
336+
// ConstructKVFromLabelEncoding will construct a map from a period separated value string that denotes key value pairs.
337+
//
338+
// Deprecated: use ConstructKVFromLabelEncodingWithSchema instead.
317339
func ConstructKVFromLabelEncoding(values string) map[string]interface{} {
318-
props := make(map[string]interface{})
319-
exploded := strings.Split(values, Period)
320-
for i := range exploded {
321-
obK := strings.Split(exploded[i], Equals)
322-
if len(obK) == 2 {
323-
props[obK[0]] = cast(obK[1])
324-
}
325-
}
326-
return props
340+
return ConstructKVFromLabelEncodingWithSchema(values, nil)
327341
}
328342

329-
// ConstructKVFromMatrixCSV will construct a map from a comma separated value string that denotes key value pairs.
343+
// ConstructKVFromMatrixCSV will construct a map from a semicolon separated value string that denotes key value pairs.
344+
//
345+
// Deprecated: use ConstructKVFromMatrixCSVWithSchema instead.
330346
func ConstructKVFromMatrixCSV(values string) map[string]interface{} {
331-
props := make(map[string]interface{})
332-
exploded := strings.Split(values, SemiColon)
333-
for i := range exploded {
334-
obK := strings.Split(exploded[i], Equals)
335-
if len(obK) == 2 {
336-
props[obK[0]] = cast(obK[1])
337-
}
338-
}
339-
return props
347+
return ConstructKVFromMatrixCSVWithSchema(values, nil)
340348
}
341349

342350
// ConstructParamMapFromFormEncodingArray will construct a map from the query parameters that are encoded as
343351
// form encoded values.
352+
//
353+
// Deprecated: use ConstructParamMapFromFormEncodingArrayWithSchema instead.
344354
func ConstructParamMapFromFormEncodingArray(values []*QueryParam) map[string]interface{} {
345-
decoded := make(map[string]interface{})
346-
for _, v := range values {
347-
props := make(map[string]interface{})
348-
// explode SSV into array
349-
exploded := strings.Split(v.Values[0], Comma)
350-
for i := range exploded {
351-
if i%2 == 0 {
352-
if len(exploded) > i+1 {
353-
props[exploded[i]] = cast(exploded[i+1])
354-
}
355-
}
356-
}
357-
decoded[v.Key] = props
358-
}
359-
return decoded
355+
return ConstructParamMapFromFormEncodingArrayWithSchema(values, nil)
360356
}
361357

362358
// DoesFormParamContainDelimiter will determine if a form parameter contains a delimiter.
@@ -391,3 +387,80 @@ func CollapseCSVIntoSpaceDelimitedStyle(key string, values []string) string {
391387
func CollapseCSVIntoPipeDelimitedStyle(key string, values []string) string {
392388
return fmt.Sprintf("%s=%s", key, strings.Join(values, Pipe))
393389
}
390+
391+
// ConstructParamMapFromQueryParamInputWithSchema constructs a param map from an existing map of
392+
// *QueryParam slices, using the object schema to determine property types before casting.
393+
func ConstructParamMapFromQueryParamInputWithSchema(values map[string][]*QueryParam, sch *base.Schema) map[string]interface{} {
394+
decoded := make(map[string]interface{})
395+
for _, q := range values {
396+
for _, v := range q {
397+
decoded[v.Key] = castWithSchema(v.Values[0], sch, v.Key)
398+
}
399+
}
400+
return decoded
401+
}
402+
403+
// ConstructParamMapFromPipeEncodingWithSchema constructs a map from pipe-delimited query parameters,
404+
// using the object schema to determine property types before casting.
405+
func ConstructParamMapFromPipeEncodingWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} {
406+
return constructParamMapFromDelimitedEncoding(values, Pipe, sch)
407+
}
408+
409+
// ConstructParamMapFromSpaceEncodingWithSchema constructs a map from space-delimited query parameters,
410+
// using the object schema to determine property types before casting.
411+
func ConstructParamMapFromSpaceEncodingWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} {
412+
return constructParamMapFromDelimitedEncoding(values, Space, sch)
413+
}
414+
415+
// ConstructMapFromCSVWithSchema constructs a map from a comma separated value string,
416+
// using the object schema to determine property types before casting.
417+
func ConstructMapFromCSVWithSchema(csv string, sch *base.Schema) map[string]interface{} {
418+
decoded := make(map[string]interface{})
419+
exploded := strings.Split(csv, Comma)
420+
for i := range exploded {
421+
if i%2 == 0 {
422+
if len(exploded) == i+1 {
423+
break
424+
}
425+
decoded[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i])
426+
}
427+
}
428+
return decoded
429+
}
430+
431+
// ConstructKVFromCSVWithSchema constructs a map from a comma-separated key=value string,
432+
// using the object schema to determine property types before casting.
433+
func ConstructKVFromCSVWithSchema(values string, sch *base.Schema) map[string]interface{} {
434+
return constructKVFromDelimited(values, Comma, sch)
435+
}
436+
437+
// ConstructKVFromLabelEncodingWithSchema constructs a map from a period-separated key=value string,
438+
// using the object schema to determine property types before casting.
439+
func ConstructKVFromLabelEncodingWithSchema(values string, sch *base.Schema) map[string]interface{} {
440+
return constructKVFromDelimited(values, Period, sch)
441+
}
442+
443+
// ConstructKVFromMatrixCSVWithSchema constructs a map from a semicolon-separated key=value string,
444+
// using the object schema to determine property types before casting.
445+
func ConstructKVFromMatrixCSVWithSchema(values string, sch *base.Schema) map[string]interface{} {
446+
return constructKVFromDelimited(values, SemiColon, sch)
447+
}
448+
449+
// ConstructParamMapFromFormEncodingArrayWithSchema constructs a map from form-encoded query parameters,
450+
// using the object schema to determine property types before casting.
451+
func ConstructParamMapFromFormEncodingArrayWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} {
452+
decoded := make(map[string]interface{})
453+
for _, v := range values {
454+
props := make(map[string]interface{})
455+
exploded := strings.Split(v.Values[0], Comma)
456+
for i := range exploded {
457+
if i%2 == 0 {
458+
if len(exploded) > i+1 {
459+
props[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i])
460+
}
461+
}
462+
}
463+
decoded[v.Key] = props
464+
}
465+
return decoded
466+
}

0 commit comments

Comments
 (0)