Skip to content

Commit a9c8b76

Browse files
committed
query: support struct field name casing options
Add support for converting struct field names to common casing conventions (snake_case, camelCase, kebab-case, PascalCase) via Options passed to Values or ValuesWithOptions, as well as per-field struct tag options (,snake, ,camel, ,kebab, ,pascal). Fixes #86 Signed-off-by: AdamMagued <adamismailmageud@gmail.com>
1 parent 965d79f commit a9c8b76

2 files changed

Lines changed: 431 additions & 5 deletions

File tree

‎query/encode.go‎

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"strconv"
2828
"strings"
2929
"time"
30+
"unicode"
3031
)
3132

3233
var timeType = reflect.TypeOf(time.Time{})
@@ -39,6 +40,36 @@ type Encoder interface {
3940
EncodeValues(key string, v *url.Values) error
4041
}
4142

43+
// Case defines the struct field name casing format used when encoding query parameters.
44+
type Case int
45+
46+
const (
47+
// CaseDefault uses the struct field's name as defined in the struct (usually PascalCase).
48+
CaseDefault Case = iota
49+
// CaseCamel converts struct field names to camelCase (e.g. "firstName", "userId").
50+
CaseCamel
51+
// CaseSnake converts struct field names to snake_case (e.g. "first_name", "user_id").
52+
CaseSnake
53+
// CasePascal converts struct field names to PascalCase (e.g. "FirstName", "UserId").
54+
CasePascal
55+
// CaseKebab converts struct field names to kebab-case (e.g. "first-name", "user-id").
56+
CaseKebab
57+
)
58+
59+
// Uppercase constants for compatibility with Issue #86 specification.
60+
const (
61+
CASE_DEFAULT = CaseDefault
62+
CASE_CAMEL = CaseCamel
63+
CASE_SNAKE = CaseSnake
64+
CASE_PASCAL = CasePascal
65+
CASE_KEBAB = CaseKebab
66+
)
67+
68+
// Options specifies optional configuration parameters for encoding query values.
69+
type Options struct {
70+
EncodingCase Case
71+
}
72+
4273
// Values returns the url.Values encoding of v.
4374
//
4475
// Values expects to be passed a struct, and traverses it recursively using the
@@ -122,7 +153,16 @@ type Encoder interface {
122153
//
123154
// Multiple fields that encode to the same URL parameter name will be included
124155
// as multiple URL values of the same name.
125-
func Values(v interface{}) (url.Values, error) {
156+
func Values(v interface{}, opts ...Options) (url.Values, error) {
157+
var opt Options
158+
if len(opts) > 0 {
159+
opt = opts[0]
160+
}
161+
return ValuesWithOptions(v, opt)
162+
}
163+
164+
// ValuesWithOptions returns the url.Values encoding of v with the specified Options.
165+
func ValuesWithOptions(v interface{}, opts Options) (url.Values, error) {
126166
values := make(url.Values)
127167

128168
if v == nil {
@@ -141,14 +181,14 @@ func Values(v interface{}) (url.Values, error) {
141181
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
142182
}
143183

144-
err := reflectValue(values, val, "")
184+
err := reflectValue(values, val, "", opts)
145185
return values, err
146186
}
147187

148188
// reflectValue populates the values parameter from the struct fields in val.
149189
// Embedded structs are followed recursively (using the rules defined in the
150190
// Values function documentation) breadth-first.
151-
func reflectValue(values url.Values, val reflect.Value, scope string) error {
191+
func reflectValue(values url.Values, val reflect.Value, scope string, options Options) error {
152192
var embedded []reflect.Value
153193

154194
typ := val.Type()
@@ -176,6 +216,17 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error {
176216
}
177217

178218
name = sf.Name
219+
if opts.Contains("snake") {
220+
name = applyCase(name, CaseSnake)
221+
} else if opts.Contains("camel") {
222+
name = applyCase(name, CaseCamel)
223+
} else if opts.Contains("kebab") {
224+
name = applyCase(name, CaseKebab)
225+
} else if opts.Contains("pascal") {
226+
name = applyCase(name, CasePascal)
227+
} else if options.EncodingCase != CaseDefault {
228+
name = applyCase(name, options.EncodingCase)
229+
}
179230
}
180231

181232
if scope != "" {
@@ -262,7 +313,7 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error {
262313
}
263314

264315
if sv.Kind() == reflect.Struct {
265-
if err := reflectValue(values, sv, name); err != nil {
316+
if err := reflectValue(values, sv, name, options); err != nil {
266317
return err
267318
}
268319
continue
@@ -272,7 +323,7 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error {
272323
}
273324

274325
for _, f := range embedded {
275-
if err := reflectValue(values, f, scope); err != nil {
326+
if err := reflectValue(values, f, scope, options); err != nil {
276327
return err
277328
}
278329
}
@@ -365,3 +416,96 @@ func (o tagOptions) Contains(option string) bool {
365416
}
366417
return false
367418
}
419+
420+
// applyCase converts name to the requested Case format.
421+
func applyCase(name string, c Case) string {
422+
if c == CaseDefault || name == "" {
423+
return name
424+
}
425+
return formatWords(splitWords(name), c)
426+
}
427+
428+
func splitWords(s string) []string {
429+
var words []string
430+
var word []rune
431+
runes := []rune(s)
432+
433+
for i := 0; i < len(runes); i++ {
434+
r := runes[i]
435+
if r == '_' || r == '-' || r == ' ' {
436+
if len(word) > 0 {
437+
words = append(words, string(word))
438+
word = word[:0]
439+
}
440+
continue
441+
}
442+
443+
if i > 0 {
444+
prev := runes[i-1]
445+
if unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsDigit(prev)) {
446+
if len(word) > 0 {
447+
words = append(words, string(word))
448+
word = word[:0]
449+
}
450+
} else if unicode.IsUpper(prev) && unicode.IsUpper(r) && i+1 < len(runes) && unicode.IsLower(runes[i+1]) {
451+
if len(word) > 0 {
452+
words = append(words, string(word))
453+
word = word[:0]
454+
}
455+
}
456+
}
457+
458+
word = append(word, r)
459+
}
460+
461+
if len(word) > 0 {
462+
words = append(words, string(word))
463+
}
464+
return words
465+
}
466+
467+
func formatWords(words []string, c Case) string {
468+
if len(words) == 0 {
469+
return ""
470+
}
471+
switch c {
472+
case CaseSnake:
473+
parts := make([]string, len(words))
474+
for i, w := range words {
475+
parts[i] = strings.ToLower(w)
476+
}
477+
return strings.Join(parts, "_")
478+
case CaseKebab:
479+
parts := make([]string, len(words))
480+
for i, w := range words {
481+
parts[i] = strings.ToLower(w)
482+
}
483+
return strings.Join(parts, "-")
484+
case CaseCamel:
485+
var b strings.Builder
486+
for i, w := range words {
487+
runes := []rune(w)
488+
if i == 0 {
489+
b.WriteString(strings.ToLower(w))
490+
} else {
491+
b.WriteString(string(unicode.ToUpper(runes[0])))
492+
if len(runes) > 1 {
493+
b.WriteString(strings.ToLower(string(runes[1:])))
494+
}
495+
}
496+
}
497+
return b.String()
498+
case CasePascal:
499+
var b strings.Builder
500+
for _, w := range words {
501+
runes := []rune(w)
502+
b.WriteString(string(unicode.ToUpper(runes[0])))
503+
if len(runes) > 1 {
504+
b.WriteString(strings.ToLower(string(runes[1:])))
505+
}
506+
}
507+
return b.String()
508+
default:
509+
return strings.Join(words, "")
510+
}
511+
}

0 commit comments

Comments
 (0)