mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-06 17:34:58 +01:00
ci: new publish method (#1451)
This commit is contained in:
341
packages/sdk/go/internal/apiquery/encoder.go
Normal file
341
packages/sdk/go/internal/apiquery/encoder.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package apiquery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
)
|
||||
|
||||
var encoders sync.Map // map[reflect.Type]encoderFunc
|
||||
|
||||
type encoder struct {
|
||||
dateFormat string
|
||||
root bool
|
||||
settings QuerySettings
|
||||
}
|
||||
|
||||
type encoderFunc func(key string, value reflect.Value) []Pair
|
||||
|
||||
type encoderField struct {
|
||||
tag parsedStructTag
|
||||
fn encoderFunc
|
||||
idx []int
|
||||
}
|
||||
|
||||
type encoderEntry struct {
|
||||
reflect.Type
|
||||
dateFormat string
|
||||
root bool
|
||||
settings QuerySettings
|
||||
}
|
||||
|
||||
type Pair struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
|
||||
entry := encoderEntry{
|
||||
Type: t,
|
||||
dateFormat: e.dateFormat,
|
||||
root: e.root,
|
||||
settings: e.settings,
|
||||
}
|
||||
|
||||
if fi, ok := encoders.Load(entry); ok {
|
||||
return fi.(encoderFunc)
|
||||
}
|
||||
|
||||
// To deal with recursive types, populate the map with an
|
||||
// indirect func before we build it. This type waits on the
|
||||
// real func (f) to be ready and then calls it. This indirect
|
||||
// func is only used for recursive types.
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
f encoderFunc
|
||||
)
|
||||
wg.Add(1)
|
||||
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair {
|
||||
wg.Wait()
|
||||
return f(key, v)
|
||||
}))
|
||||
if loaded {
|
||||
return fi.(encoderFunc)
|
||||
}
|
||||
|
||||
// Compute the real encoder and replace the indirect func with it.
|
||||
f = e.newTypeEncoder(t)
|
||||
wg.Done()
|
||||
encoders.Store(entry, f)
|
||||
return f
|
||||
}
|
||||
|
||||
func marshalerEncoder(key string, value reflect.Value) []Pair {
|
||||
s, _ := value.Interface().(json.Marshaler).MarshalJSON()
|
||||
return []Pair{{key, string(s)}}
|
||||
}
|
||||
|
||||
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
|
||||
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||
return e.newTimeTypeEncoder(t)
|
||||
}
|
||||
if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
|
||||
return marshalerEncoder
|
||||
}
|
||||
e.root = false
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
encoder := e.typeEncoder(t.Elem())
|
||||
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||
if !value.IsValid() || value.IsNil() {
|
||||
return
|
||||
}
|
||||
pairs = encoder(key, value.Elem())
|
||||
return
|
||||
}
|
||||
case reflect.Struct:
|
||||
return e.newStructTypeEncoder(t)
|
||||
case reflect.Array:
|
||||
fallthrough
|
||||
case reflect.Slice:
|
||||
return e.newArrayTypeEncoder(t)
|
||||
case reflect.Map:
|
||||
return e.newMapEncoder(t)
|
||||
case reflect.Interface:
|
||||
return e.newInterfaceEncoder()
|
||||
default:
|
||||
return e.newPrimitiveTypeEncoder(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
|
||||
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
|
||||
return e.newFieldTypeEncoder(t)
|
||||
}
|
||||
|
||||
encoderFields := []encoderField{}
|
||||
|
||||
// This helper allows us to recursively collect field encoders into a flat
|
||||
// array. The parameter `index` keeps track of the access patterns necessary
|
||||
// to get to some field.
|
||||
var collectEncoderFields func(r reflect.Type, index []int)
|
||||
collectEncoderFields = func(r reflect.Type, index []int) {
|
||||
for i := 0; i < r.NumField(); i++ {
|
||||
idx := append(index, i)
|
||||
field := t.FieldByIndex(idx)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
// If this is an embedded struct, traverse one level deeper to extract
|
||||
// the field and get their encoders as well.
|
||||
if field.Anonymous {
|
||||
collectEncoderFields(field.Type, idx)
|
||||
continue
|
||||
}
|
||||
// If query tag is not present, then we skip, which is intentionally
|
||||
// different behavior from the stdlib.
|
||||
ptag, ok := parseQueryStructTag(field)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if ptag.name == "-" && !ptag.inline {
|
||||
continue
|
||||
}
|
||||
|
||||
dateFormat, ok := parseFormatStructTag(field)
|
||||
oldFormat := e.dateFormat
|
||||
if ok {
|
||||
switch dateFormat {
|
||||
case "date-time":
|
||||
e.dateFormat = time.RFC3339
|
||||
case "date":
|
||||
e.dateFormat = "2006-01-02"
|
||||
}
|
||||
}
|
||||
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
|
||||
e.dateFormat = oldFormat
|
||||
}
|
||||
}
|
||||
collectEncoderFields(t, []int{})
|
||||
|
||||
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||
for _, ef := range encoderFields {
|
||||
var subkey string = e.renderKeyPath(key, ef.tag.name)
|
||||
if ef.tag.inline {
|
||||
subkey = key
|
||||
}
|
||||
|
||||
field := value.FieldByIndex(ef.idx)
|
||||
pairs = append(pairs, ef.fn(subkey, field)...)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
|
||||
keyEncoder := e.typeEncoder(t.Key())
|
||||
elementEncoder := e.typeEncoder(t.Elem())
|
||||
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||
iter := value.MapRange()
|
||||
for iter.Next() {
|
||||
encodedKey := keyEncoder("", iter.Key())
|
||||
if len(encodedKey) != 1 {
|
||||
panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?")
|
||||
}
|
||||
subkey := encodedKey[0].value
|
||||
keyPath := e.renderKeyPath(key, subkey)
|
||||
pairs = append(pairs, elementEncoder(keyPath, iter.Value())...)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) renderKeyPath(key string, subkey string) string {
|
||||
if len(key) == 0 {
|
||||
return subkey
|
||||
}
|
||||
if e.settings.NestedFormat == NestedQueryFormatDots {
|
||||
return fmt.Sprintf("%s.%s", key, subkey)
|
||||
}
|
||||
return fmt.Sprintf("%s[%s]", key, subkey)
|
||||
}
|
||||
|
||||
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
|
||||
switch e.settings.ArrayFormat {
|
||||
case ArrayQueryFormatComma:
|
||||
innerEncoder := e.typeEncoder(t.Elem())
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
elements := []string{}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
for _, pair := range innerEncoder("", v.Index(i)) {
|
||||
elements = append(elements, pair.value)
|
||||
}
|
||||
}
|
||||
if len(elements) == 0 {
|
||||
return []Pair{}
|
||||
}
|
||||
return []Pair{{key, strings.Join(elements, ",")}}
|
||||
}
|
||||
case ArrayQueryFormatRepeat:
|
||||
innerEncoder := e.typeEncoder(t.Elem())
|
||||
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
pairs = append(pairs, innerEncoder(key, value.Index(i))...)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
case ArrayQueryFormatIndices:
|
||||
panic("The array indices format is not supported yet")
|
||||
case ArrayQueryFormatBrackets:
|
||||
innerEncoder := e.typeEncoder(t.Elem())
|
||||
return func(key string, value reflect.Value) []Pair {
|
||||
pairs := []Pair{}
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat))
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
inner := t.Elem()
|
||||
|
||||
innerEncoder := e.newPrimitiveTypeEncoder(inner)
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
if !v.IsValid() || v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
return innerEncoder(key, v.Elem())
|
||||
}
|
||||
case reflect.String:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return []Pair{{key, v.String()}}
|
||||
}
|
||||
case reflect.Bool:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
if v.Bool() {
|
||||
return []Pair{{key, "true"}}
|
||||
}
|
||||
return []Pair{{key, "false"}}
|
||||
}
|
||||
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return []Pair{{key, strconv.FormatInt(v.Int(), 10)}}
|
||||
}
|
||||
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}}
|
||||
}
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
bitSize := 64
|
||||
if t.Kind() == reflect.Complex128 {
|
||||
bitSize = 128
|
||||
}
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}}
|
||||
}
|
||||
default:
|
||||
return func(key string, v reflect.Value) []Pair {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
|
||||
f, _ := t.FieldByName("Value")
|
||||
enc := e.typeEncoder(f.Type)
|
||||
|
||||
return func(key string, value reflect.Value) []Pair {
|
||||
present := value.FieldByName("Present")
|
||||
if !present.Bool() {
|
||||
return nil
|
||||
}
|
||||
null := value.FieldByName("Null")
|
||||
if null.Bool() {
|
||||
// TODO: Error?
|
||||
return nil
|
||||
}
|
||||
raw := value.FieldByName("Raw")
|
||||
if !raw.IsNil() {
|
||||
return e.typeEncoder(raw.Type())(key, raw)
|
||||
}
|
||||
return enc(key, value.FieldByName("Value"))
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc {
|
||||
format := e.dateFormat
|
||||
return func(key string, value reflect.Value) []Pair {
|
||||
return []Pair{{
|
||||
key,
|
||||
value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format),
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) newInterfaceEncoder() encoderFunc {
|
||||
return func(key string, value reflect.Value) []Pair {
|
||||
value = value.Elem()
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return e.typeEncoder(value.Type())(key, value)
|
||||
}
|
||||
|
||||
}
|
||||
50
packages/sdk/go/internal/apiquery/query.go
Normal file
50
packages/sdk/go/internal/apiquery/query.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package apiquery
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values {
|
||||
e := encoder{time.RFC3339, true, settings}
|
||||
kv := url.Values{}
|
||||
val := reflect.ValueOf(value)
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
typ := val.Type()
|
||||
for _, pair := range e.typeEncoder(typ)("", val) {
|
||||
kv.Add(pair.key, pair.value)
|
||||
}
|
||||
return kv
|
||||
}
|
||||
|
||||
func Marshal(value interface{}) url.Values {
|
||||
return MarshalWithSettings(value, QuerySettings{})
|
||||
}
|
||||
|
||||
type Queryer interface {
|
||||
URLQuery() url.Values
|
||||
}
|
||||
|
||||
type QuerySettings struct {
|
||||
NestedFormat NestedQueryFormat
|
||||
ArrayFormat ArrayQueryFormat
|
||||
}
|
||||
|
||||
type NestedQueryFormat int
|
||||
|
||||
const (
|
||||
NestedQueryFormatBrackets NestedQueryFormat = iota
|
||||
NestedQueryFormatDots
|
||||
)
|
||||
|
||||
type ArrayQueryFormat int
|
||||
|
||||
const (
|
||||
ArrayQueryFormatComma ArrayQueryFormat = iota
|
||||
ArrayQueryFormatRepeat
|
||||
ArrayQueryFormatIndices
|
||||
ArrayQueryFormatBrackets
|
||||
)
|
||||
335
packages/sdk/go/internal/apiquery/query_test.go
Normal file
335
packages/sdk/go/internal/apiquery/query_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package apiquery
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func P[T any](v T) *T { return &v }
|
||||
|
||||
type Primitives struct {
|
||||
A bool `query:"a"`
|
||||
B int `query:"b"`
|
||||
C uint `query:"c"`
|
||||
D float64 `query:"d"`
|
||||
E float32 `query:"e"`
|
||||
F []int `query:"f"`
|
||||
}
|
||||
|
||||
type PrimitivePointers struct {
|
||||
A *bool `query:"a"`
|
||||
B *int `query:"b"`
|
||||
C *uint `query:"c"`
|
||||
D *float64 `query:"d"`
|
||||
E *float32 `query:"e"`
|
||||
F *[]int `query:"f"`
|
||||
}
|
||||
|
||||
type Slices struct {
|
||||
Slice []Primitives `query:"slices"`
|
||||
Mixed []interface{} `query:"mixed"`
|
||||
}
|
||||
|
||||
type DateTime struct {
|
||||
Date time.Time `query:"date" format:"date"`
|
||||
DateTime time.Time `query:"date-time" format:"date-time"`
|
||||
}
|
||||
|
||||
type AdditionalProperties struct {
|
||||
A bool `query:"a"`
|
||||
Extras map[string]interface{} `query:"-,inline"`
|
||||
}
|
||||
|
||||
type Recursive struct {
|
||||
Name string `query:"name"`
|
||||
Child *Recursive `query:"child"`
|
||||
}
|
||||
|
||||
type UnknownStruct struct {
|
||||
Unknown interface{} `query:"unknown"`
|
||||
}
|
||||
|
||||
type UnionStruct struct {
|
||||
Union Union `query:"union" format:"date"`
|
||||
}
|
||||
|
||||
type Union interface {
|
||||
union()
|
||||
}
|
||||
|
||||
type UnionInteger int64
|
||||
|
||||
func (UnionInteger) union() {}
|
||||
|
||||
type UnionString string
|
||||
|
||||
func (UnionString) union() {}
|
||||
|
||||
type UnionStructA struct {
|
||||
Type string `query:"type"`
|
||||
A string `query:"a"`
|
||||
B string `query:"b"`
|
||||
}
|
||||
|
||||
func (UnionStructA) union() {}
|
||||
|
||||
type UnionStructB struct {
|
||||
Type string `query:"type"`
|
||||
A string `query:"a"`
|
||||
}
|
||||
|
||||
func (UnionStructB) union() {}
|
||||
|
||||
type UnionTime time.Time
|
||||
|
||||
func (UnionTime) union() {}
|
||||
|
||||
type DeeplyNested struct {
|
||||
A DeeplyNested1 `query:"a"`
|
||||
}
|
||||
|
||||
type DeeplyNested1 struct {
|
||||
B DeeplyNested2 `query:"b"`
|
||||
}
|
||||
|
||||
type DeeplyNested2 struct {
|
||||
C DeeplyNested3 `query:"c"`
|
||||
}
|
||||
|
||||
type DeeplyNested3 struct {
|
||||
D *string `query:"d"`
|
||||
}
|
||||
|
||||
var tests = map[string]struct {
|
||||
enc string
|
||||
val interface{}
|
||||
settings QuerySettings
|
||||
}{
|
||||
"primitives": {
|
||||
"a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4",
|
||||
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"slices_brackets": {
|
||||
`mixed[]=1&mixed[]=2.3&mixed[]=hello&slices[][a]=false&slices[][a]=false&slices[][b]=237628372683&slices[][b]=237628372683&slices[][c]=654&slices[][c]=654&slices[][d]=9999.43&slices[][d]=9999.43&slices[][e]=43.7599983215332&slices[][e]=43.7599983215332&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4`,
|
||||
Slices{
|
||||
Slice: []Primitives{
|
||||
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||
},
|
||||
Mixed: []interface{}{1, 2.3, "hello"},
|
||||
},
|
||||
QuerySettings{ArrayFormat: ArrayQueryFormatBrackets},
|
||||
},
|
||||
|
||||
"slices_comma": {
|
||||
`mixed=1,2.3,hello`,
|
||||
Slices{
|
||||
Mixed: []interface{}{1, 2.3, "hello"},
|
||||
},
|
||||
QuerySettings{ArrayFormat: ArrayQueryFormatComma},
|
||||
},
|
||||
|
||||
"slices_repeat": {
|
||||
`mixed=1&mixed=2.3&mixed=hello&slices[a]=false&slices[a]=false&slices[b]=237628372683&slices[b]=237628372683&slices[c]=654&slices[c]=654&slices[d]=9999.43&slices[d]=9999.43&slices[e]=43.7599983215332&slices[e]=43.7599983215332&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4`,
|
||||
Slices{
|
||||
Slice: []Primitives{
|
||||
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||
},
|
||||
Mixed: []interface{}{1, 2.3, "hello"},
|
||||
},
|
||||
QuerySettings{ArrayFormat: ArrayQueryFormatRepeat},
|
||||
},
|
||||
|
||||
"primitive_pointer_struct": {
|
||||
"a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5",
|
||||
PrimitivePointers{
|
||||
A: P(false),
|
||||
B: P(237628372683),
|
||||
C: P(uint(654)),
|
||||
D: P(9999.43),
|
||||
E: P(float32(43.76)),
|
||||
F: &[]int{1, 2, 3, 4, 5},
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"datetime_struct": {
|
||||
`date=2006-01-02&date-time=2006-01-02T15:04:05Z`,
|
||||
DateTime{
|
||||
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
|
||||
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"additional_properties": {
|
||||
`a=true&bar=value&foo=true`,
|
||||
AdditionalProperties{
|
||||
A: true,
|
||||
Extras: map[string]interface{}{
|
||||
"bar": "value",
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"recursive_struct_brackets": {
|
||||
`child[name]=Alex&name=Robert`,
|
||||
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||
},
|
||||
|
||||
"recursive_struct_dots": {
|
||||
`child.name=Alex&name=Robert`,
|
||||
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||
},
|
||||
|
||||
"unknown_struct_number": {
|
||||
`unknown=12`,
|
||||
UnknownStruct{
|
||||
Unknown: 12.,
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"unknown_struct_map_brackets": {
|
||||
`unknown[foo]=bar`,
|
||||
UnknownStruct{
|
||||
Unknown: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||
},
|
||||
|
||||
"unknown_struct_map_dots": {
|
||||
`unknown.foo=bar`,
|
||||
UnknownStruct{
|
||||
Unknown: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||
},
|
||||
|
||||
"union_string": {
|
||||
`union=hello`,
|
||||
UnionStruct{
|
||||
Union: UnionString("hello"),
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"union_integer": {
|
||||
`union=12`,
|
||||
UnionStruct{
|
||||
Union: UnionInteger(12),
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"union_struct_discriminated_a": {
|
||||
`union[a]=foo&union[b]=bar&union[type]=typeA`,
|
||||
UnionStruct{
|
||||
Union: UnionStructA{
|
||||
Type: "typeA",
|
||||
A: "foo",
|
||||
B: "bar",
|
||||
},
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"union_struct_discriminated_b": {
|
||||
`union[a]=foo&union[type]=typeB`,
|
||||
UnionStruct{
|
||||
Union: UnionStructB{
|
||||
Type: "typeB",
|
||||
A: "foo",
|
||||
},
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"union_struct_time": {
|
||||
`union=2010-05-23`,
|
||||
UnionStruct{
|
||||
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
QuerySettings{},
|
||||
},
|
||||
|
||||
"deeply_nested_brackets": {
|
||||
`a[b][c][d]=hello`,
|
||||
DeeplyNested{
|
||||
A: DeeplyNested1{
|
||||
B: DeeplyNested2{
|
||||
C: DeeplyNested3{
|
||||
D: P("hello"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||
},
|
||||
|
||||
"deeply_nested_dots": {
|
||||
`a.b.c.d=hello`,
|
||||
DeeplyNested{
|
||||
A: DeeplyNested1{
|
||||
B: DeeplyNested2{
|
||||
C: DeeplyNested3{
|
||||
D: P("hello"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||
},
|
||||
|
||||
"deeply_nested_brackets_empty": {
|
||||
``,
|
||||
DeeplyNested{
|
||||
A: DeeplyNested1{
|
||||
B: DeeplyNested2{
|
||||
C: DeeplyNested3{
|
||||
D: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||
},
|
||||
|
||||
"deeply_nested_dots_empty": {
|
||||
``,
|
||||
DeeplyNested{
|
||||
A: DeeplyNested1{
|
||||
B: DeeplyNested2{
|
||||
C: DeeplyNested3{
|
||||
D: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
values := MarshalWithSettings(test.val, test.settings)
|
||||
str, _ := url.QueryUnescape(values.Encode())
|
||||
if str != test.enc {
|
||||
t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
packages/sdk/go/internal/apiquery/tag.go
Normal file
41
packages/sdk/go/internal/apiquery/tag.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package apiquery
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const queryStructTag = "query"
|
||||
const formatStructTag = "format"
|
||||
|
||||
type parsedStructTag struct {
|
||||
name string
|
||||
omitempty bool
|
||||
inline bool
|
||||
}
|
||||
|
||||
func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
|
||||
raw, ok := field.Tag.Lookup(queryStructTag)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
if len(parts) == 0 {
|
||||
return tag, false
|
||||
}
|
||||
tag.name = parts[0]
|
||||
for _, part := range parts[1:] {
|
||||
switch part {
|
||||
case "omitempty":
|
||||
tag.omitempty = true
|
||||
case "inline":
|
||||
tag.inline = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
|
||||
format, ok = field.Tag.Lookup(formatStructTag)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user