github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/apiform/form_test.go (about)

     1  package apiform
     2  
     3  import (
     4  	"bytes"
     5  	"github.com/anthropics/anthropic-sdk-go/packages/param"
     6  	"io"
     7  	"mime/multipart"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  )
    12  
    13  func P[T any](v T) *T { return &v }
    14  
    15  type Primitives struct {
    16  	A bool    `form:"a"`
    17  	B int     `form:"b"`
    18  	C uint    `form:"c"`
    19  	D float64 `form:"d"`
    20  	E float32 `form:"e"`
    21  	F []int   `form:"f"`
    22  }
    23  
    24  // These aliases are necessary to bypass the cache.
    25  // This only relevant during testing.
    26  type int_ int
    27  type PrimitivesBrackets struct {
    28  	F []int_ `form:"f"`
    29  }
    30  
    31  type PrimitivePointers struct {
    32  	A *bool    `form:"a"`
    33  	B *int     `form:"b"`
    34  	C *uint    `form:"c"`
    35  	D *float64 `form:"d"`
    36  	E *float32 `form:"e"`
    37  	F *[]int   `form:"f"`
    38  }
    39  
    40  type Slices struct {
    41  	Slice []Primitives `form:"slices"`
    42  }
    43  
    44  type DateTime struct {
    45  	Date     time.Time `form:"date" format:"date"`
    46  	DateTime time.Time `form:"date-time" format:"date-time"`
    47  }
    48  
    49  type AdditionalProperties struct {
    50  	A      bool           `form:"a"`
    51  	Extras map[string]any `form:"-" api:"extrafields"`
    52  }
    53  
    54  type TypedAdditionalProperties struct {
    55  	A      bool           `form:"a"`
    56  	Extras map[string]int `form:"-" api:"extrafields"`
    57  }
    58  
    59  type EmbeddedStructs struct {
    60  	AdditionalProperties
    61  	A      *int           `form:"number2"`
    62  	Extras map[string]any `form:"-" api:"extrafields"`
    63  }
    64  
    65  type Recursive struct {
    66  	Name  string     `form:"name"`
    67  	Child *Recursive `form:"child"`
    68  }
    69  
    70  type UnknownStruct struct {
    71  	Unknown any `form:"unknown"`
    72  }
    73  
    74  type UnionStruct struct {
    75  	Union Union `form:"union" format:"date"`
    76  }
    77  
    78  type Union interface {
    79  	union()
    80  }
    81  
    82  type UnionInteger int64
    83  
    84  func (UnionInteger) union() {}
    85  
    86  type UnionStructA struct {
    87  	Type string `form:"type"`
    88  	A    string `form:"a"`
    89  	B    string `form:"b"`
    90  }
    91  
    92  func (UnionStructA) union() {}
    93  
    94  type UnionStructB struct {
    95  	Type string `form:"type"`
    96  	A    string `form:"a"`
    97  }
    98  
    99  func (UnionStructB) union() {}
   100  
   101  type UnionTime time.Time
   102  
   103  func (UnionTime) union() {}
   104  
   105  type ReaderStruct struct {
   106  	File io.Reader `form:"file"`
   107  }
   108  
   109  type NamedEnum string
   110  
   111  const NamedEnumFoo NamedEnum = "foo"
   112  
   113  type StructUnionWrapper struct {
   114  	Union StructUnion `form:"union"`
   115  }
   116  
   117  type StructUnion struct {
   118  	OfInt    param.Opt[int64]     `form:",omitzero,inline"`
   119  	OfString param.Opt[string]    `form:",omitzero,inline"`
   120  	OfEnum   param.Opt[NamedEnum] `form:",omitzero,inline"`
   121  	OfA      UnionStructA         `form:",omitzero,inline"`
   122  	OfB      UnionStructB         `form:",omitzero,inline"`
   123  	param.APIUnion
   124  }
   125  
   126  type ConstantStruct struct {
   127  	Anchor  string `form:"anchor" default:"created_at"`
   128  	Seconds int    `form:"seconds"`
   129  }
   130  
   131  type MultipartMarshalerParent struct {
   132  	Middle MultipartMarshalerMiddleNext `form:"middle"`
   133  }
   134  
   135  type MultipartMarshalerMiddleNext struct {
   136  	MiddleNext MultipartMarshalerMiddle `form:"middleNext"`
   137  }
   138  
   139  type MultipartMarshalerMiddle struct {
   140  	Child int `form:"child"`
   141  }
   142  
   143  var tests = map[string]struct {
   144  	buf string
   145  	val any
   146  }{
   147  	"file": {
   148  		buf: `--xxx
   149  Content-Disposition: form-data; name="file"; filename="anonymous_file"
   150  Content-Type: application/octet-stream
   151  
   152  some file contents...
   153  --xxx--
   154  `,
   155  		val: ReaderStruct{
   156  			File: io.Reader(bytes.NewBuffer([]byte("some file contents..."))),
   157  		},
   158  	},
   159  	"map_string": {
   160  		`--xxx
   161  Content-Disposition: form-data; name="foo"
   162  
   163  bar
   164  --xxx--
   165  `,
   166  		map[string]string{"foo": "bar"},
   167  	},
   168  
   169  	"map_interface": {
   170  		`--xxx
   171  Content-Disposition: form-data; name="a"
   172  
   173  1
   174  --xxx
   175  Content-Disposition: form-data; name="b"
   176  
   177  str
   178  --xxx
   179  Content-Disposition: form-data; name="c"
   180  
   181  false
   182  --xxx--
   183  `,
   184  		map[string]any{"a": float64(1), "b": "str", "c": false},
   185  	},
   186  
   187  	"primitive_struct": {
   188  		`--xxx
   189  Content-Disposition: form-data; name="a"
   190  
   191  false
   192  --xxx
   193  Content-Disposition: form-data; name="b"
   194  
   195  237628372683
   196  --xxx
   197  Content-Disposition: form-data; name="c"
   198  
   199  654
   200  --xxx
   201  Content-Disposition: form-data; name="d"
   202  
   203  9999.43
   204  --xxx
   205  Content-Disposition: form-data; name="e"
   206  
   207  43.76
   208  --xxx
   209  Content-Disposition: form-data; name="f.0"
   210  
   211  1
   212  --xxx
   213  Content-Disposition: form-data; name="f.1"
   214  
   215  2
   216  --xxx
   217  Content-Disposition: form-data; name="f.2"
   218  
   219  3
   220  --xxx
   221  Content-Disposition: form-data; name="f.3"
   222  
   223  4
   224  --xxx--
   225  `,
   226  		Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
   227  	},
   228  	"primitive_struct,brackets": {
   229  		`--xxx
   230  Content-Disposition: form-data; name="f[]"
   231  
   232  1
   233  --xxx
   234  Content-Disposition: form-data; name="f[]"
   235  
   236  2
   237  --xxx
   238  Content-Disposition: form-data; name="f[]"
   239  
   240  3
   241  --xxx
   242  Content-Disposition: form-data; name="f[]"
   243  
   244  4
   245  --xxx--
   246  `,
   247  		PrimitivesBrackets{F: []int_{1, 2, 3, 4}},
   248  	},
   249  
   250  	"slices": {
   251  		`--xxx
   252  Content-Disposition: form-data; name="slices.0.a"
   253  
   254  false
   255  --xxx
   256  Content-Disposition: form-data; name="slices.0.b"
   257  
   258  237628372683
   259  --xxx
   260  Content-Disposition: form-data; name="slices.0.c"
   261  
   262  654
   263  --xxx
   264  Content-Disposition: form-data; name="slices.0.d"
   265  
   266  9999.43
   267  --xxx
   268  Content-Disposition: form-data; name="slices.0.e"
   269  
   270  43.76
   271  --xxx
   272  Content-Disposition: form-data; name="slices.0.f.0"
   273  
   274  1
   275  --xxx
   276  Content-Disposition: form-data; name="slices.0.f.1"
   277  
   278  2
   279  --xxx
   280  Content-Disposition: form-data; name="slices.0.f.2"
   281  
   282  3
   283  --xxx
   284  Content-Disposition: form-data; name="slices.0.f.3"
   285  
   286  4
   287  --xxx--
   288  `,
   289  		Slices{
   290  			Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
   291  		},
   292  	},
   293  	"primitive_pointer_struct": {
   294  		`--xxx
   295  Content-Disposition: form-data; name="a"
   296  
   297  false
   298  --xxx
   299  Content-Disposition: form-data; name="b"
   300  
   301  237628372683
   302  --xxx
   303  Content-Disposition: form-data; name="c"
   304  
   305  654
   306  --xxx
   307  Content-Disposition: form-data; name="d"
   308  
   309  9999.43
   310  --xxx
   311  Content-Disposition: form-data; name="e"
   312  
   313  43.76
   314  --xxx
   315  Content-Disposition: form-data; name="f.0"
   316  
   317  1
   318  --xxx
   319  Content-Disposition: form-data; name="f.1"
   320  
   321  2
   322  --xxx
   323  Content-Disposition: form-data; name="f.2"
   324  
   325  3
   326  --xxx
   327  Content-Disposition: form-data; name="f.3"
   328  
   329  4
   330  --xxx
   331  Content-Disposition: form-data; name="f.4"
   332  
   333  5
   334  --xxx--
   335  `,
   336  		PrimitivePointers{
   337  			A: P(false),
   338  			B: P(237628372683),
   339  			C: P(uint(654)),
   340  			D: P(9999.43),
   341  			E: P(float32(43.76)),
   342  			F: &[]int{1, 2, 3, 4, 5},
   343  		},
   344  	},
   345  
   346  	"datetime_struct": {
   347  		`--xxx
   348  Content-Disposition: form-data; name="date"
   349  
   350  2006-01-02
   351  --xxx
   352  Content-Disposition: form-data; name="date-time"
   353  
   354  2006-01-02T15:04:05Z
   355  --xxx--
   356  `,
   357  		DateTime{
   358  			Date:     time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
   359  			DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
   360  		},
   361  	},
   362  
   363  	"additional_properties": {
   364  		`--xxx
   365  Content-Disposition: form-data; name="a"
   366  
   367  true
   368  --xxx
   369  Content-Disposition: form-data; name="bar"
   370  
   371  value
   372  --xxx
   373  Content-Disposition: form-data; name="foo"
   374  
   375  true
   376  --xxx--
   377  `,
   378  		AdditionalProperties{
   379  			A: true,
   380  			Extras: map[string]any{
   381  				"bar": "value",
   382  				"foo": true,
   383  			},
   384  		},
   385  	},
   386  	"recursive_struct,brackets": {
   387  		`--xxx
   388  Content-Disposition: form-data; name="child[name]"
   389  
   390  Alex
   391  --xxx
   392  Content-Disposition: form-data; name="name"
   393  
   394  Robert
   395  --xxx--
   396  `,
   397  		Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
   398  	},
   399  
   400  	"recursive_struct": {
   401  		`--xxx
   402  Content-Disposition: form-data; name="child.name"
   403  
   404  Alex
   405  --xxx
   406  Content-Disposition: form-data; name="name"
   407  
   408  Robert
   409  --xxx--
   410  `,
   411  		Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
   412  	},
   413  
   414  	"unknown_struct_number": {
   415  		`--xxx
   416  Content-Disposition: form-data; name="unknown"
   417  
   418  12
   419  --xxx--
   420  `,
   421  		UnknownStruct{
   422  			Unknown: 12.,
   423  		},
   424  	},
   425  
   426  	"unknown_struct_map": {
   427  		`--xxx
   428  Content-Disposition: form-data; name="unknown.foo"
   429  
   430  bar
   431  --xxx--
   432  `,
   433  		UnknownStruct{
   434  			Unknown: map[string]any{
   435  				"foo": "bar",
   436  			},
   437  		},
   438  	},
   439  
   440  	"struct_union_integer": {
   441  		`--xxx
   442  Content-Disposition: form-data; name="union"
   443  
   444  12
   445  --xxx--
   446  `,
   447  		StructUnionWrapper{
   448  			Union: StructUnion{OfInt: param.NewOpt[int64](12)},
   449  		},
   450  	},
   451  
   452  	"union_integer": {
   453  		`--xxx
   454  Content-Disposition: form-data; name="union"
   455  
   456  12
   457  --xxx--
   458  `,
   459  		UnionStruct{
   460  			Union: UnionInteger(12),
   461  		},
   462  	},
   463  
   464  	"struct_union_struct_discriminated_a": {
   465  		`--xxx
   466  Content-Disposition: form-data; name="union.a"
   467  
   468  foo
   469  --xxx
   470  Content-Disposition: form-data; name="union.b"
   471  
   472  bar
   473  --xxx
   474  Content-Disposition: form-data; name="union.type"
   475  
   476  typeA
   477  --xxx--
   478  `,
   479  		StructUnionWrapper{
   480  			Union: StructUnion{OfA: UnionStructA{
   481  				Type: "typeA",
   482  				A:    "foo",
   483  				B:    "bar",
   484  			}},
   485  		},
   486  	},
   487  
   488  	"union_struct_discriminated_a": {
   489  		`--xxx
   490  Content-Disposition: form-data; name="union.a"
   491  
   492  foo
   493  --xxx
   494  Content-Disposition: form-data; name="union.b"
   495  
   496  bar
   497  --xxx
   498  Content-Disposition: form-data; name="union.type"
   499  
   500  typeA
   501  --xxx--
   502  `,
   503  
   504  		UnionStruct{
   505  			Union: UnionStructA{
   506  				Type: "typeA",
   507  				A:    "foo",
   508  				B:    "bar",
   509  			},
   510  		},
   511  	},
   512  
   513  	"struct_union_struct_discriminated_b": {
   514  		`--xxx
   515  Content-Disposition: form-data; name="union.a"
   516  
   517  foo
   518  --xxx
   519  Content-Disposition: form-data; name="union.type"
   520  
   521  typeB
   522  --xxx--
   523  `,
   524  		StructUnionWrapper{
   525  			Union: StructUnion{OfB: UnionStructB{
   526  				Type: "typeB",
   527  				A:    "foo",
   528  			}},
   529  		},
   530  	},
   531  
   532  	"union_struct_discriminated_b": {
   533  		`--xxx
   534  Content-Disposition: form-data; name="union.a"
   535  
   536  foo
   537  --xxx
   538  Content-Disposition: form-data; name="union.type"
   539  
   540  typeB
   541  --xxx--
   542  `,
   543  		UnionStruct{
   544  			Union: UnionStructB{
   545  				Type: "typeB",
   546  				A:    "foo",
   547  			},
   548  		},
   549  	},
   550  
   551  	"union_struct_time": {
   552  		`--xxx
   553  Content-Disposition: form-data; name="union"
   554  
   555  2010-05-23
   556  --xxx--
   557  `,
   558  		UnionStruct{
   559  			Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
   560  		},
   561  	},
   562  	"constant_zero_value": {
   563  		`--xxx
   564  Content-Disposition: form-data; name="anchor"
   565  
   566  created_at
   567  --xxx
   568  Content-Disposition: form-data; name="seconds"
   569  
   570  3600
   571  --xxx--
   572  `,
   573  		ConstantStruct{
   574  			Seconds: 3600,
   575  		},
   576  	},
   577  	"constant_explicit_value": {
   578  		`--xxx
   579  Content-Disposition: form-data; name="anchor"
   580  
   581  created_at_override
   582  --xxx
   583  Content-Disposition: form-data; name="seconds"
   584  
   585  3600
   586  --xxx--
   587  `,
   588  		ConstantStruct{
   589  			Anchor:  "created_at_override",
   590  			Seconds: 3600,
   591  		},
   592  	},
   593  	"deeply-nested-struct,brackets": {
   594  		`--xxx
   595  Content-Disposition: form-data; name="middle[middleNext][child]"
   596  
   597  10
   598  --xxx--
   599  `,
   600  		MultipartMarshalerParent{
   601  			Middle: MultipartMarshalerMiddleNext{
   602  				MiddleNext: MultipartMarshalerMiddle{
   603  					Child: 10,
   604  				},
   605  			},
   606  		},
   607  	},
   608  	"deeply-nested-map,brackets": {
   609  		`--xxx
   610  Content-Disposition: form-data; name="middle[middleNext][child]"
   611  
   612  10
   613  --xxx--
   614  `,
   615  		map[string]any{"middle": map[string]any{"middleNext": map[string]any{"child": 10}}},
   616  	},
   617  }
   618  
   619  func TestEncode(t *testing.T) {
   620  	for name, test := range tests {
   621  		t.Run(name, func(t *testing.T) {
   622  			buf := bytes.NewBuffer(nil)
   623  			writer := multipart.NewWriter(buf)
   624  			err := writer.SetBoundary("xxx")
   625  			if err != nil {
   626  				t.Errorf("setting boundary for %v failed with error %v", test.val, err)
   627  			}
   628  
   629  			arrayFmt := "indices:dots"
   630  			if tags := strings.Split(name, ","); len(tags) > 1 {
   631  				arrayFmt = tags[1]
   632  			}
   633  
   634  			err = MarshalWithSettings(test.val, writer, arrayFmt)
   635  			if err != nil {
   636  				t.Errorf("serialization of %v failed with error %v", test.val, err)
   637  			}
   638  			err = writer.Close()
   639  			if err != nil {
   640  				t.Errorf("serialization of %v failed with error %v", test.val, err)
   641  			}
   642  			raw := buf.Bytes()
   643  			if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
   644  				t.Errorf("expected %+#v to serialize to '%s' but got '%s' (with format %s)", test.val, test.buf, string(raw), arrayFmt)
   645  			}
   646  		})
   647  	}
   648  }