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 }