diff options
Diffstat (limited to 'packages/sdk/go/internal/apiform')
| -rw-r--r-- | packages/sdk/go/internal/apiform/encoder.go | 383 | ||||
| -rw-r--r-- | packages/sdk/go/internal/apiform/form.go | 5 | ||||
| -rw-r--r-- | packages/sdk/go/internal/apiform/form_test.go | 440 | ||||
| -rw-r--r-- | packages/sdk/go/internal/apiform/tag.go | 48 |
4 files changed, 0 insertions, 876 deletions
diff --git a/packages/sdk/go/internal/apiform/encoder.go b/packages/sdk/go/internal/apiform/encoder.go deleted file mode 100644 index 243a1a123..000000000 --- a/packages/sdk/go/internal/apiform/encoder.go +++ /dev/null @@ -1,383 +0,0 @@ -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 deleted file mode 100644 index 5445116e9..000000000 --- a/packages/sdk/go/internal/apiform/form.go +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 39d1460c0..000000000 --- a/packages/sdk/go/internal/apiform/form_test.go +++ /dev/null @@ -1,440 +0,0 @@ -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 deleted file mode 100644 index b22e054fc..000000000 --- a/packages/sdk/go/internal/apiform/tag.go +++ /dev/null @@ -1,48 +0,0 @@ -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 -} |
