summaryrefslogtreecommitdiffhomepage
path: root/packages/sdk/go/internal/apiform
diff options
context:
space:
mode:
authorDax <[email protected]>2025-07-31 01:00:29 -0400
committerGitHub <[email protected]>2025-07-31 01:00:29 -0400
commit33cef075d228e80aefb44671ec68e1989c2855a8 (patch)
treed43a5c1bcc40d4d938eacccfd923c80301706cf1 /packages/sdk/go/internal/apiform
parentb09ebf464552f3899120b22c7a8572669000a554 (diff)
downloadopencode-33cef075d228e80aefb44671ec68e1989c2855a8.tar.gz
opencode-33cef075d228e80aefb44671ec68e1989c2855a8.zip
ci: new publish method (#1451)
Diffstat (limited to 'packages/sdk/go/internal/apiform')
-rw-r--r--packages/sdk/go/internal/apiform/encoder.go383
-rw-r--r--packages/sdk/go/internal/apiform/form.go5
-rw-r--r--packages/sdk/go/internal/apiform/form_test.go440
-rw-r--r--packages/sdk/go/internal/apiform/tag.go48
4 files changed, 876 insertions, 0 deletions
diff --git a/packages/sdk/go/internal/apiform/encoder.go b/packages/sdk/go/internal/apiform/encoder.go
new file mode 100644
index 000000000..243a1a123
--- /dev/null
+++ b/packages/sdk/go/internal/apiform/encoder.go
@@ -0,0 +1,383 @@
+package apiform
+
+import (
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/textproto"
+ "path"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+var encoders sync.Map // map[encoderEntry]encoderFunc
+
+func Marshal(value interface{}, writer *multipart.Writer) error {
+ e := &encoder{dateFormat: time.RFC3339}
+ return e.marshal(value, writer)
+}
+
+func MarshalRoot(value interface{}, writer *multipart.Writer) error {
+ e := &encoder{root: true, dateFormat: time.RFC3339}
+ return e.marshal(value, writer)
+}
+
+type encoder struct {
+ dateFormat string
+ root bool
+}
+
+type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error
+
+type encoderField struct {
+ tag parsedStructTag
+ fn encoderFunc
+ idx []int
+}
+
+type encoderEntry struct {
+ reflect.Type
+ dateFormat string
+ root bool
+}
+
+func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error {
+ val := reflect.ValueOf(value)
+ if !val.IsValid() {
+ return nil
+ }
+ typ := val.Type()
+ enc := e.typeEncoder(typ)
+ return enc("", val, writer)
+}
+
+func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
+ entry := encoderEntry{
+ Type: t,
+ dateFormat: e.dateFormat,
+ root: e.root,
+ }
+
+ 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, writer *multipart.Writer) error {
+ wg.Wait()
+ return f(key, v, writer)
+ }))
+ 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 (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
+ if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
+ return e.newTimeTypeEncoder()
+ }
+ if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
+ return e.newReaderTypeEncoder()
+ }
+ e.root = false
+ switch t.Kind() {
+ case reflect.Pointer:
+ inner := t.Elem()
+
+ innerEncoder := e.typeEncoder(inner)
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if !v.IsValid() || v.IsNil() {
+ return nil
+ }
+ return innerEncoder(key, v.Elem(), writer)
+ }
+ case reflect.Struct:
+ return e.newStructTypeEncoder(t)
+ case reflect.Slice, reflect.Array:
+ 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) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
+ switch t.Kind() {
+ // Note that we could use `gjson` to encode these types but it would complicate our
+ // code more and this current code shouldn't cause any issues
+ case reflect.String:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, v.String())
+ }
+ case reflect.Bool:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if v.Bool() {
+ return writer.WriteField(key, "true")
+ }
+ return writer.WriteField(key, "false")
+ }
+ case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatInt(v.Int(), 10))
+ }
+ case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10))
+ }
+ case reflect.Float32:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32))
+ }
+ case reflect.Float64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
+ }
+ default:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
+ }
+ }
+}
+
+func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
+ itemEncoder := e.typeEncoder(t.Elem())
+
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if key != "" {
+ key = key + "."
+ }
+ for i := 0; i < v.Len(); i++ {
+ err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
+ if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
+ return e.newFieldTypeEncoder(t)
+ }
+
+ encoderFields := []encoderField{}
+ extraEncoder := (*encoderField)(nil)
+
+ // 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 json tag is not present, then we skip, which is intentionally
+ // different behavior from the stdlib.
+ ptag, ok := parseFormStructTag(field)
+ if !ok {
+ continue
+ }
+ // We only want to support unexported field if they're tagged with
+ // `extras` because that field shouldn't be part of the public API. We
+ // also want to only keep the top level extras
+ if ptag.extras && len(index) == 0 {
+ extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
+ continue
+ }
+ if ptag.name == "-" {
+ 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{})
+
+ // Ensure deterministic output by sorting by lexicographic order
+ sort.Slice(encoderFields, func(i, j int) bool {
+ return encoderFields[i].tag.name < encoderFields[j].tag.name
+ })
+
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ if key != "" {
+ key = key + "."
+ }
+
+ for _, ef := range encoderFields {
+ field := value.FieldByIndex(ef.idx)
+ err := ef.fn(key+ef.tag.name, field, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ if extraEncoder != nil {
+ err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ 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, writer *multipart.Writer) error {
+ present := value.FieldByName("Present")
+ if !present.Bool() {
+ return nil
+ }
+ null := value.FieldByName("Null")
+ if null.Bool() {
+ return nil
+ }
+ raw := value.FieldByName("Raw")
+ if !raw.IsNil() {
+ return e.typeEncoder(raw.Type())(key, raw, writer)
+ }
+ return enc(key, value.FieldByName("Value"), writer)
+ }
+}
+
+func (e *encoder) newTimeTypeEncoder() encoderFunc {
+ format := e.dateFormat
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format))
+ }
+}
+
+func (e encoder) newInterfaceEncoder() encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ value = value.Elem()
+ if !value.IsValid() {
+ return nil
+ }
+ return e.typeEncoder(value.Type())(key, value, writer)
+ }
+}
+
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
+func escapeQuotes(s string) string {
+ return quoteEscaper.Replace(s)
+}
+
+func (e *encoder) newReaderTypeEncoder() encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
+ filename := "anonymous_file"
+ contentType := "application/octet-stream"
+ if named, ok := reader.(interface{ Filename() string }); ok {
+ filename = named.Filename()
+ } else if named, ok := reader.(interface{ Name() string }); ok {
+ filename = path.Base(named.Name())
+ }
+ if typed, ok := reader.(interface{ ContentType() string }); ok {
+ contentType = typed.ContentType()
+ }
+
+ // Below is taken almost 1-for-1 from [multipart.CreateFormFile]
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename)))
+ h.Set("Content-Type", contentType)
+ filewriter, err := writer.CreatePart(h)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(filewriter, reader)
+ return err
+ }
+}
+
+// Given a []byte of json (may either be an empty object or an object that already contains entries)
+// encode all of the entries in the map to the json byte array.
+func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
+ type mapPair struct {
+ key string
+ value reflect.Value
+ }
+
+ if key != "" {
+ key = key + "."
+ }
+
+ pairs := []mapPair{}
+
+ iter := v.MapRange()
+ for iter.Next() {
+ if iter.Key().Type().Kind() == reflect.String {
+ pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
+ } else {
+ return fmt.Errorf("cannot encode a map with a non string key")
+ }
+ }
+
+ // Ensure deterministic output
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].key < pairs[j].key
+ })
+
+ elementEncoder := e.typeEncoder(v.Type().Elem())
+ for _, p := range pairs {
+ err := elementEncoder(key+string(p.key), p.value, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ return e.encodeMapEntries(key, value, writer)
+ }
+}
diff --git a/packages/sdk/go/internal/apiform/form.go b/packages/sdk/go/internal/apiform/form.go
new file mode 100644
index 000000000..5445116e9
--- /dev/null
+++ b/packages/sdk/go/internal/apiform/form.go
@@ -0,0 +1,5 @@
+package apiform
+
+type Marshaler interface {
+ MarshalMultipart() ([]byte, string, error)
+}
diff --git a/packages/sdk/go/internal/apiform/form_test.go b/packages/sdk/go/internal/apiform/form_test.go
new file mode 100644
index 000000000..39d1460c0
--- /dev/null
+++ b/packages/sdk/go/internal/apiform/form_test.go
@@ -0,0 +1,440 @@
+package apiform
+
+import (
+ "bytes"
+ "mime/multipart"
+ "strings"
+ "testing"
+ "time"
+)
+
+func P[T any](v T) *T { return &v }
+
+type Primitives struct {
+ A bool `form:"a"`
+ B int `form:"b"`
+ C uint `form:"c"`
+ D float64 `form:"d"`
+ E float32 `form:"e"`
+ F []int `form:"f"`
+}
+
+type PrimitivePointers struct {
+ A *bool `form:"a"`
+ B *int `form:"b"`
+ C *uint `form:"c"`
+ D *float64 `form:"d"`
+ E *float32 `form:"e"`
+ F *[]int `form:"f"`
+}
+
+type Slices struct {
+ Slice []Primitives `form:"slices"`
+}
+
+type DateTime struct {
+ Date time.Time `form:"date" format:"date"`
+ DateTime time.Time `form:"date-time" format:"date-time"`
+}
+
+type AdditionalProperties struct {
+ A bool `form:"a"`
+ Extras map[string]interface{} `form:"-,extras"`
+}
+
+type TypedAdditionalProperties struct {
+ A bool `form:"a"`
+ Extras map[string]int `form:"-,extras"`
+}
+
+type EmbeddedStructs struct {
+ AdditionalProperties
+ A *int `form:"number2"`
+ Extras map[string]interface{} `form:"-,extras"`
+}
+
+type Recursive struct {
+ Name string `form:"name"`
+ Child *Recursive `form:"child"`
+}
+
+type UnknownStruct struct {
+ Unknown interface{} `form:"unknown"`
+}
+
+type UnionStruct struct {
+ Union Union `form:"union" format:"date"`
+}
+
+type Union interface {
+ union()
+}
+
+type UnionInteger int64
+
+func (UnionInteger) union() {}
+
+type UnionStructA struct {
+ Type string `form:"type"`
+ A string `form:"a"`
+ B string `form:"b"`
+}
+
+func (UnionStructA) union() {}
+
+type UnionStructB struct {
+ Type string `form:"type"`
+ A string `form:"a"`
+}
+
+func (UnionStructB) union() {}
+
+type UnionTime time.Time
+
+func (UnionTime) union() {}
+
+type ReaderStruct struct {
+}
+
+var tests = map[string]struct {
+ buf string
+ val interface{}
+}{
+ "map_string": {
+ `--xxx
+Content-Disposition: form-data; name="foo"
+
+bar
+--xxx--
+`,
+ map[string]string{"foo": "bar"},
+ },
+
+ "map_interface": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+1
+--xxx
+Content-Disposition: form-data; name="b"
+
+str
+--xxx
+Content-Disposition: form-data; name="c"
+
+false
+--xxx--
+`,
+ map[string]interface{}{"a": float64(1), "b": "str", "c": false},
+ },
+
+ "primitive_struct": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+false
+--xxx
+Content-Disposition: form-data; name="b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="c"
+
+654
+--xxx
+Content-Disposition: form-data; name="d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="f.3"
+
+4
+--xxx--
+`,
+ Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ },
+
+ "slices": {
+ `--xxx
+Content-Disposition: form-data; name="slices.0.a"
+
+false
+--xxx
+Content-Disposition: form-data; name="slices.0.b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="slices.0.c"
+
+654
+--xxx
+Content-Disposition: form-data; name="slices.0.d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="slices.0.e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="slices.0.f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="slices.0.f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="slices.0.f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="slices.0.f.3"
+
+4
+--xxx--
+`,
+ Slices{
+ Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
+ },
+ },
+
+ "primitive_pointer_struct": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+false
+--xxx
+Content-Disposition: form-data; name="b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="c"
+
+654
+--xxx
+Content-Disposition: form-data; name="d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="f.3"
+
+4
+--xxx
+Content-Disposition: form-data; name="f.4"
+
+5
+--xxx--
+`,
+ 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},
+ },
+ },
+
+ "datetime_struct": {
+ `--xxx
+Content-Disposition: form-data; name="date"
+
+2006-01-02
+--xxx
+Content-Disposition: form-data; name="date-time"
+
+2006-01-02T15:04:05Z
+--xxx--
+`,
+ 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),
+ },
+ },
+
+ "additional_properties": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+true
+--xxx
+Content-Disposition: form-data; name="bar"
+
+value
+--xxx
+Content-Disposition: form-data; name="foo"
+
+true
+--xxx--
+`,
+ AdditionalProperties{
+ A: true,
+ Extras: map[string]interface{}{
+ "bar": "value",
+ "foo": true,
+ },
+ },
+ },
+
+ "recursive_struct": {
+ `--xxx
+Content-Disposition: form-data; name="child.name"
+
+Alex
+--xxx
+Content-Disposition: form-data; name="name"
+
+Robert
+--xxx--
+`,
+ Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
+ },
+
+ "unknown_struct_number": {
+ `--xxx
+Content-Disposition: form-data; name="unknown"
+
+12
+--xxx--
+`,
+ UnknownStruct{
+ Unknown: 12.,
+ },
+ },
+
+ "unknown_struct_map": {
+ `--xxx
+Content-Disposition: form-data; name="unknown.foo"
+
+bar
+--xxx--
+`,
+ UnknownStruct{
+ Unknown: map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+
+ "union_integer": {
+ `--xxx
+Content-Disposition: form-data; name="union"
+
+12
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionInteger(12),
+ },
+ },
+
+ "union_struct_discriminated_a": {
+ `--xxx
+Content-Disposition: form-data; name="union.a"
+
+foo
+--xxx
+Content-Disposition: form-data; name="union.b"
+
+bar
+--xxx
+Content-Disposition: form-data; name="union.type"
+
+typeA
+--xxx--
+`,
+
+ UnionStruct{
+ Union: UnionStructA{
+ Type: "typeA",
+ A: "foo",
+ B: "bar",
+ },
+ },
+ },
+
+ "union_struct_discriminated_b": {
+ `--xxx
+Content-Disposition: form-data; name="union.a"
+
+foo
+--xxx
+Content-Disposition: form-data; name="union.type"
+
+typeB
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionStructB{
+ Type: "typeB",
+ A: "foo",
+ },
+ },
+ },
+
+ "union_struct_time": {
+ `--xxx
+Content-Disposition: form-data; name="union"
+
+2010-05-23
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
+ },
+ },
+}
+
+func TestEncode(t *testing.T) {
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ buf := bytes.NewBuffer(nil)
+ writer := multipart.NewWriter(buf)
+ writer.SetBoundary("xxx")
+ err := Marshal(test.val, writer)
+ if err != nil {
+ t.Errorf("serialization of %v failed with error %v", test.val, err)
+ }
+ err = writer.Close()
+ if err != nil {
+ t.Errorf("serialization of %v failed with error %v", test.val, err)
+ }
+ raw := buf.Bytes()
+ if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
+ t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
+ }
+ })
+ }
+}
diff --git a/packages/sdk/go/internal/apiform/tag.go b/packages/sdk/go/internal/apiform/tag.go
new file mode 100644
index 000000000..b22e054fc
--- /dev/null
+++ b/packages/sdk/go/internal/apiform/tag.go
@@ -0,0 +1,48 @@
+package apiform
+
+import (
+ "reflect"
+ "strings"
+)
+
+const jsonStructTag = "json"
+const formStructTag = "form"
+const formatStructTag = "format"
+
+type parsedStructTag struct {
+ name string
+ required bool
+ extras bool
+ metadata bool
+}
+
+func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
+ raw, ok := field.Tag.Lookup(formStructTag)
+ if !ok {
+ raw, ok = field.Tag.Lookup(jsonStructTag)
+ }
+ 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 "required":
+ tag.required = true
+ case "extras":
+ tag.extras = true
+ case "metadata":
+ tag.metadata = true
+ }
+ }
+ return
+}
+
+func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
+ format, ok = field.Tag.Lookup(formatStructTag)
+ return
+}