github.com/anthropics/anthropic-sdk-go@v1.70.1/packages/param/encoder_test.go (about) 1 package param_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "reflect" 7 "testing" 8 "time" 9 10 shimjson "github.com/anthropics/anthropic-sdk-go/internal/encoding/json" 11 "github.com/anthropics/anthropic-sdk-go/packages/param" 12 ) 13 14 type Struct struct { 15 A string `json:"a"` 16 B int64 `json:"b"` 17 param.APIObject 18 } 19 20 func (r Struct) MarshalJSON() (data []byte, err error) { 21 type shadow Struct 22 return param.MarshalObject(r, (*shadow)(&r)) 23 } 24 25 // Note that the order of fields affects the JSON 26 // key order. Changing the order of the fields in this struct 27 // will fail tests unnecessarily. 28 type FieldStruct struct { 29 A param.Opt[string] `json:"a,omitzero"` 30 B param.Opt[int64] `json:"b,omitzero"` 31 C Struct `json:"c,omitzero"` 32 D time.Time `json:"d,omitzero" format:"date"` 33 E time.Time `json:"e,omitzero"` 34 F param.Opt[time.Time] `json:"f,omitzero" format:"date"` 35 G param.Opt[time.Time] `json:"g,omitzero"` 36 H param.Opt[time.Time] `json:"h,omitzero" format:"date-time"` 37 param.APIObject 38 } 39 40 func (r FieldStruct) MarshalJSON() (data []byte, err error) { 41 type shadow FieldStruct 42 return param.MarshalObject(r, (*shadow)(&r)) 43 } 44 45 type StructWithAdditionalProperties struct { 46 First string `json:"first"` 47 Second int `json:"second"` 48 ExtraFields map[string]any `json:"-"` 49 param.APIObject 50 } 51 52 func (s StructWithAdditionalProperties) MarshalJSON() ([]byte, error) { 53 type shadow StructWithAdditionalProperties 54 return param.MarshalWithExtras(s, (*shadow)(&s), s.ExtraFields) 55 } 56 57 func TestIsNullish(t *testing.T) { 58 nullTests := map[string]param.ParamNullable{ 59 "null_string": param.Null[string](), 60 "null_int64": param.Null[int64](), 61 "null_time": param.Null[time.Time](), 62 "null_struct": param.NullStruct[Struct](), 63 } 64 65 for name, test := range nullTests { 66 t.Run(name, func(t *testing.T) { 67 if !param.IsNull(test) { 68 t.Fatalf("expected %s to be null", name) 69 } 70 if param.IsOmitted(test) { 71 t.Fatalf("expected %s to not be omitted", name) 72 } 73 }) 74 } 75 76 omitTests := map[string]param.ParamNullable{ 77 "omit_string": param.Opt[string]{}, 78 "omit_int64": param.Opt[int64]{}, 79 "omit_time": param.Opt[time.Time]{}, 80 "omit_struct": Struct{}, 81 } 82 83 for name, test := range omitTests { 84 t.Run(name, func(t *testing.T) { 85 if param.IsNull(test) { 86 t.Fatalf("expected %s to be null", name) 87 } 88 if !param.IsOmitted(test) { 89 t.Fatalf("expected %s to not be omitted", name) 90 } 91 }) 92 } 93 } 94 95 func TestFieldMarshal(t *testing.T) { 96 tests := map[string]struct { 97 value any 98 expected string 99 }{ 100 "null_string": {param.Null[string](), "null"}, 101 "null_int64": {param.Null[int64](), "null"}, 102 "null_time": {param.Null[time.Time](), "null"}, 103 "null_struct": {param.NullStruct[Struct](), "null"}, 104 105 "float_zero": {param.NewOpt(float64(0.0)), "0"}, 106 "string_zero": {param.NewOpt(""), `""`}, 107 "time_zero": {param.NewOpt(time.Time{}), `"0001-01-01T00:00:00Z"`}, 108 109 "string": {param.Opt[string]{Value: "string"}, `"string"`}, 110 "int": {param.Opt[int64]{Value: 123}, "123"}, 111 "int64": {param.Opt[int64]{Value: int64(123456789123456789)}, "123456789123456789"}, 112 "struct": {Struct{A: "yo", B: 123}, `{"a":"yo","b":123}`}, 113 "datetime": { 114 param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 115 `"2023-03-18T14:47:38Z"`, 116 }, 117 "optional_date": { 118 FieldStruct{ 119 F: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 120 }, 121 `{"f":"2023-03-18"}`, 122 }, 123 "optional_time": { 124 FieldStruct{ 125 G: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 126 }, 127 `{"g":"2023-03-18T14:47:38Z"}`, 128 }, 129 "optional_datetime_explicit_format": { 130 FieldStruct{ 131 H: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 132 }, 133 `{"h":"2023-03-18T14:47:38Z"}`, 134 }, 135 "param_struct": { 136 FieldStruct{ 137 A: param.Opt[string]{Value: "hello"}, 138 B: param.Opt[int64]{Value: int64(12)}, 139 D: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC), 140 E: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC), 141 F: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 142 G: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 143 H: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 144 }, 145 `{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z","f":"2023-03-18","g":"2023-03-18T14:47:38Z","h":"2023-03-18T14:47:38Z"}`, 146 }, 147 } 148 149 for name, test := range tests { 150 t.Run(name, func(t *testing.T) { 151 b, err := json.Marshal(test.value) 152 if err != nil { 153 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 154 } 155 if string(b) != test.expected { 156 t.Fatalf("expected %s, received %s", test.expected, string(b)) 157 } 158 }) 159 } 160 } 161 162 func TestAdditionalProperties(t *testing.T) { 163 s := StructWithAdditionalProperties{ 164 First: "hello", 165 Second: 14, 166 ExtraFields: map[string]any{ 167 "hi": "there", 168 }, 169 } 170 exp := `{"first":"hello","second":14,"hi":"there"}` 171 172 bytes, err := json.Marshal(s) 173 if err != nil { 174 t.Fatalf("failed to marshal: %v", err) 175 } 176 177 if string(bytes) != exp { 178 t.Fatalf("expected %s, got %s", exp, string(bytes)) 179 } 180 } 181 182 func TestExtraFields(t *testing.T) { 183 v := Struct{ 184 A: "hello", 185 B: 123, 186 } 187 v.SetExtraFields(map[string]any{ 188 "extra": Struct{A: "recursive"}, 189 "b": nil, 190 }) 191 bytes, err := json.Marshal(v) 192 if err != nil { 193 t.Fatalf("failed to marshal: %v", err) 194 } 195 if string(bytes) != `{"a":"hello","b":null,"extra":{"a":"recursive","b":0}}` { 196 t.Fatalf("failed to marshal: got %v", string(bytes)) 197 } 198 if v.B != 123 { 199 t.Fatalf("marshal modified field B: got %v", v.B) 200 } 201 } 202 203 func TestExtraFieldsForceOmitted(t *testing.T) { 204 v := Struct{ 205 // Testing with the zero value. 206 // A: "", 207 // B: 0, 208 } 209 v.SetExtraFields(map[string]any{ 210 "b": param.Omit, 211 }) 212 bytes, err := json.Marshal(v) 213 if err != nil { 214 t.Fatalf("failed to marshal: %v", err) 215 } 216 if string(bytes) != `{"a":""}` { 217 t.Fatalf("failed to marshal: got %v", string(bytes)) 218 } 219 } 220 221 type UnionWithDates struct { 222 OfDate param.Opt[time.Time] 223 OfTime param.Opt[time.Time] 224 param.APIUnion 225 } 226 227 func (r UnionWithDates) MarshalJSON() (data []byte, err error) { 228 return param.MarshalUnion(r, param.EncodedAsDate(r.OfDate), r.OfTime) 229 } 230 231 func TestUnionDateMarshal(t *testing.T) { 232 tests := map[string]struct { 233 value UnionWithDates 234 expected string 235 }{ 236 "date_only": { 237 UnionWithDates{ 238 OfDate: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 0, 0, 0, 0, time.UTC)}, 239 }, 240 `"2023-03-18"`, 241 }, 242 "datetime_only": { 243 UnionWithDates{ 244 OfTime: param.Opt[time.Time]{Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 245 }, 246 `"2023-03-18T14:47:38Z"`, 247 }, 248 } 249 250 for name, test := range tests { 251 t.Run(name, func(t *testing.T) { 252 b, err := json.Marshal(test.value) 253 if err != nil { 254 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 255 } 256 if string(b) != test.expected { 257 t.Fatalf("expected %s, received %s", test.expected, string(b)) 258 } 259 }) 260 } 261 } 262 263 func TestOverride(t *testing.T) { 264 tests := map[string]struct { 265 value param.ParamStruct 266 expected string 267 }{ 268 "param_struct": { 269 param.Override[FieldStruct](map[string]any{ 270 "a": "hello", 271 "b": 12, 272 "c": nil, 273 }), 274 `{"a":"hello","b":12,"c":null}`, 275 }, 276 "param_struct_primitive": { 277 param.Override[FieldStruct](12), 278 `12`, 279 }, 280 "param_struct_null": { 281 param.Override[FieldStruct](nil), 282 `null`, 283 }, 284 } 285 286 f := FieldStruct{} 287 288 f.SetExtraFields(map[string]any{ 289 "z": "ok", 290 }) 291 292 for name, test := range tests { 293 t.Run(name, func(t *testing.T) { 294 b, err := json.Marshal(test.value) 295 if err != nil { 296 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 297 } 298 if string(b) != test.expected { 299 t.Fatalf("expected %s, received %s", test.expected, string(b)) 300 } 301 if _, ok := test.value.Overrides(); !ok { 302 t.Fatalf("expected to be overridden") 303 } 304 }) 305 } 306 } 307 308 // Despite implementing the interface, this struct is not an param.Optional 309 // since it was defined in a different package. 310 type almostOpt struct{} 311 312 func (almostOpt) Valid() bool { return true } 313 func (almostOpt) Null() bool { return false } 314 func (almostOpt) isZero() bool { return false } 315 316 func (almostOpt) implOpt() {} 317 318 func TestOptionalInterfaceAssignability(t *testing.T) { 319 optInt := param.Opt[int]{} 320 if _, ok := any(optInt).(param.Optional); !ok { 321 t.Fatalf("failed to assign") 322 } 323 324 notOpt := almostOpt{} 325 if _, ok := any(notOpt).(param.Optional); ok { 326 t.Fatalf("unexpected successful assignment") 327 } 328 329 notOpt.implOpt() // silence the warning 330 } 331 332 type PrimitiveUnion struct { 333 OfString param.Opt[string] 334 OfInt param.Opt[int] 335 param.APIUnion 336 } 337 338 func (p PrimitiveUnion) MarshalJSON() (data []byte, err error) { 339 return param.MarshalUnion(p, p.OfString, p.OfInt) 340 } 341 342 func TestOverriddenUnion(t *testing.T) { 343 tests := map[string]struct { 344 value PrimitiveUnion 345 expected string 346 }{ 347 "string": { 348 param.Override[PrimitiveUnion](json.RawMessage(`"hello"`)), 349 `"hello"`, 350 }, 351 "int": { 352 param.Override[PrimitiveUnion](json.RawMessage(`42`)), 353 `42`, 354 }, 355 } 356 357 for name, test := range tests { 358 t.Run(name, func(t *testing.T) { 359 b, err := json.Marshal(test.value) 360 if err != nil { 361 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 362 } 363 if string(b) != test.expected { 364 t.Fatalf("expected %s, received %s", test.expected, string(b)) 365 } 366 }) 367 } 368 } 369 370 func TestNullStructUnion(t *testing.T) { 371 nullUnion := param.NullStruct[PrimitiveUnion]() 372 373 b, err := json.Marshal(nullUnion) 374 if err != nil { 375 t.Fatalf("didn't expect error %v", err) 376 } 377 if string(b) != "null" { 378 t.Fatalf("expected null, received %s", string(b)) 379 } 380 } 381 382 // 383 // Compaction optimization 384 // 385 386 type NonCompactedDoubleParent struct { 387 Prop string `json:"prop"` 388 Parent NonCompactedParent `json:"parent"` 389 390 param.APIObject 391 } 392 393 type NonCompactedParent struct { 394 BadChild NonCompacted `json:"bad_child"` 395 396 param.APIObject 397 } 398 399 type NonCompacted struct { 400 Raw string 401 402 param.APIObject 403 } 404 405 func (a NonCompactedDoubleParent) MarshalJSON() ([]byte, error) { 406 type shadow NonCompactedDoubleParent 407 return param.MarshalObject(a, (*shadow)(&a)) 408 } 409 410 func (a NonCompactedParent) MarshalJSON() ([]byte, error) { 411 type shadow NonCompactedParent 412 return param.MarshalObject(a, (*shadow)(&a)) 413 } 414 415 func (a NonCompacted) MarshalJSON() ([]byte, error) { 416 if a.Raw == "" { 417 a.Raw = nonCompactedRaw 418 } 419 return []byte(a.Raw), nil 420 } 421 422 var nonCompactedRaw string = ` { "foo": "bar" } ` 423 424 func TestAppendCompactBroken(t *testing.T) { 425 tests := map[string]struct { 426 value json.Marshaler 427 }{ 428 "red/illegal-json": { 429 NonCompacted{Raw: `{ "broken": "json" `}, 430 }, 431 "red/nested-with-illegal-json": { 432 NonCompactedParent{BadChild: NonCompacted{ 433 Raw: `{ "broken": "json" `, 434 }}, 435 }, 436 } 437 438 for name, test := range tests { 439 t.Run(name, func(t *testing.T) { 440 v, err := json.Marshal(test.value) 441 if err == nil { 442 t.Fatal("expected error got", v) 443 } 444 }) 445 } 446 } 447 448 // TestAppendCompact validates an optimization for internal SDK types to 449 // avoid O(keys^2) iteration over each JSON object. 450 // 451 // It's possible to intentionally trigger this behavior as both a user and 452 // SDK developer. However, the edge case is quite pathological and requires 453 // calling [json.Marshaler.MarshalJSON] rather than [json.Marshal]. 454 func TestAppendCompact(t *testing.T) { 455 456 tests := map[string]struct { 457 value json.Marshaler 458 expected string 459 }{ 460 // 461 // Non-compacted cases 462 // 463 // Note this is how to exploit the compacter to fail, you must call [json.Marshaler.MarshalJSON] rather than [json.Marshal]. 464 // The type must also embed [param.APIObject] and return non-compacted JSON. 465 // 466 467 "no-compact/fails-compaction": { 468 NonCompacted{Raw: nonCompactedRaw}, 469 nonCompactedRaw, 470 }, 471 "no-compact/nested-with-bad-child": { 472 NonCompactedParent{BadChild: NonCompacted{ 473 Raw: nonCompactedRaw, 474 }}, 475 `{"bad_child":` + nonCompactedRaw + `}`, 476 }, 477 "no-compact/double-nested-with-bad-child": { 478 NonCompactedDoubleParent{Prop: "1", Parent: NonCompactedParent{BadChild: NonCompacted{ 479 Raw: nonCompactedRaw, 480 }}}, 481 `{"prop":"1","parent":{"bad_child":` + nonCompactedRaw + `}}`, 482 }, 483 484 // 485 // Compacted cases 486 // 487 488 "override/spaces-within": { 489 param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com": "pact"}`)), 490 `{"com":"pact"}`, 491 }, 492 "override/spaces-after": { 493 param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com":"pact"} `)), 494 `{"com":"pact"}`, 495 }, 496 "override/spaces-before": { 497 param.Override[NonCompactedDoubleParent](json.RawMessage(` {"com":"pact"}`)), 498 `{"com":"pact"}`, 499 }, 500 "override/spaces-around": { 501 param.Override[NonCompactedDoubleParent](json.RawMessage(` { "com": "pact" }`)), 502 `{"com":"pact"}`, 503 }, 504 "override/override-with-nested": { 505 param.Override[NonCompactedDoubleParent](NonCompactedParent{}), 506 `{"bad_child":{"foo":"bar"}}`, 507 }, 508 "override/override-with-non-compacted": { 509 param.Override[NonCompactedDoubleParent](NonCompacted{}), 510 `{"foo":"bar"}`, 511 }, 512 } 513 514 for name, test := range tests { 515 t.Run(name+"/marshal-json", func(t *testing.T) { 516 b, err := test.value.MarshalJSON() 517 if err != nil { 518 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 519 } 520 if string(b) != test.expected { 521 t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) 522 } 523 }) 524 525 t.Run(name+"/json-marshal", func(t *testing.T) { 526 b, err := json.Marshal(test.value) 527 if err != nil { 528 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 529 } 530 531 // expected output of JSON Marshal should always be compacted 532 var compactedExpected bytes.Buffer 533 err = json.Compact(&compactedExpected, []byte(test.expected)) 534 if err != nil { 535 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 536 } 537 538 if string(b) != compactedExpected.String() { 539 t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) 540 } 541 }) 542 543 t.Run(name+"/shimjson-marshal", func(t *testing.T) { 544 b, err := shimjson.Marshal(test.value) 545 if err != nil { 546 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 547 } 548 if string(b) != test.expected { 549 t.Logf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) 550 } 551 }) 552 } 553 } 554 555 // Caller-supplied json.RawMessage is not SDK-authored: unlike SDK MarshalJSON 556 // output, it must still be compacted, HTML-escaped, and validated rather than 557 // written verbatim. 558 559 type RawInput struct { 560 Input any `json:"input,omitzero"` 561 562 param.APIObject 563 } 564 565 func (r RawInput) MarshalJSON() ([]byte, error) { 566 type shadow RawInput 567 return param.MarshalObject(r, (*shadow)(&r)) 568 } 569 570 // namedRawPtr has an empty method set, so it must take the regular pointer 571 // encoder path rather than the RawMessage one. 572 type namedRawPtr *json.RawMessage 573 574 func TestRawMessageNormalized(t *testing.T) { 575 messy := json.RawMessage("{\n \"q\": \"a<b&c\u2028\" }") 576 compacted := `{"input":{"q":"a\u003cb\u0026c\u2028"}}` 577 deref := json.RawMessage(`{"a":1}`) 578 579 for name, test := range map[string]struct { 580 input any 581 expected string 582 }{ 583 "value": {messy, compacted}, 584 "pointer": {&messy, compacted}, 585 "named-pointer": {namedRawPtr(&deref), `{"input":{"a":1}}`}, 586 } { 587 t.Run(name, func(t *testing.T) { 588 b, err := RawInput{Input: test.input}.MarshalJSON() 589 if err != nil { 590 t.Fatalf("didn't expect error %v, expected %s", err, test.expected) 591 } 592 if string(b) != test.expected { 593 t.Fatalf("expected %s, received %s", test.expected, string(b)) 594 } 595 }) 596 } 597 598 for _, raw := range []string{`{"a":}`, ``} { 599 if b, err := (RawInput{Input: json.RawMessage(raw)}).MarshalJSON(); err == nil { 600 t.Errorf("raw %q: expected error, received %s", raw, b) 601 } 602 } 603 } 604 605 // Extra fields are inserted by sjson, which does not HTML-escape; the values 606 // must be escaped at insertion, since parents write nested MarshalJSON output 607 // verbatim. 608 func TestExtraFieldValuesEscaped(t *testing.T) { 609 s := StructWithAdditionalProperties{ 610 First: "f", Second: 1, 611 ExtraFields: map[string]any{"x_note": "a<b&c"}, 612 } 613 expected := `{"first":"f","second":1,"x_note":"a\u003cb\u0026c"}` 614 b, err := s.MarshalJSON() 615 if err != nil { 616 t.Fatal(err) 617 } 618 if string(b) != expected { 619 t.Fatalf("expected %s, received %s", expected, b) 620 } 621 }