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  }