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

     1  // Package compat tests the hand-written parts of the SDK against the generated
     2  // types, so that a field or variant added by codegen fails a test here instead
     3  // of going unhandled.
     4  //
     5  // This file synthesizes response JSON from those types: one fully populated
     6  // value per type, plus one variation per field (empty list, zero, null, and
     7  // each union variant), using the matching param struct to decide which fields a
     8  // request can carry. It lives in this package because reflecting over the
     9  // As<Variant>() accessors would defeat dead-code elimination in a package users
    10  // link.
    11  package compat
    12  
    13  import (
    14  	"encoding/json"
    15  	"maps"
    16  	"reflect"
    17  	"slices"
    18  	"strings"
    19  	"testing"
    20  )
    21  
    22  // A variant is one member of a response union.
    23  type variant struct {
    24  	// Accessor is the As* method that returns the variant.
    25  	Accessor string
    26  	// Type is what the accessor returns: a struct, or a slice for a list member.
    27  	Type reflect.Type
    28  }
    29  
    30  // unionVariants returns the members of a response union in accessor order, or
    31  // nothing for a type that is not a union.
    32  func unionVariants(union reflect.Type) []variant {
    33  	var out []variant
    34  	for i := 0; i < union.NumMethod(); i++ {
    35  		m := union.Method(i)
    36  		if !strings.HasPrefix(m.Name, "As") || m.Type.NumIn() != 1 || m.Type.NumOut() != 1 {
    37  			continue
    38  		}
    39  		// AsAny returns the interface the variants share, not a variant.
    40  		if typ := m.Type.Out(0); typ.Kind() != reflect.Interface {
    41  			out = append(out, variant{m.Name, typ})
    42  		}
    43  	}
    44  	return out
    45  }
    46  
    47  // wireType returns the discriminator a variant carries on the wire.
    48  func wireType(t *testing.T, v variant) string {
    49  	t.Helper()
    50  	field, ok := v.Type.FieldByName("Type")
    51  	if !ok {
    52  		t.Fatalf("%s has no Type field", v.Type)
    53  	}
    54  	return field.Tag.Get("default")
    55  }
    56  
    57  // A shape is one synthetic wire value for a response type, ready for
    58  // json.Marshal.
    59  type shape struct {
    60  	// Name is "populated" or the path of the one difference from it, such as
    61  	// "content=AsResponseWebFetchToolResultError/error_code=empty".
    62  	Name  string
    63  	Value any
    64  }
    65  
    66  // requestJSON marshals a param and normalizes it with comparableJSON.
    67  func requestJSON(t *testing.T, param any) []byte {
    68  	t.Helper()
    69  	data, err := json.Marshal(param)
    70  	if err != nil {
    71  		t.Fatalf("Failed to marshal param: %v", err)
    72  	}
    73  	return comparableJSON(t, data)
    74  }
    75  
    76  // comparableJSON re-encodes JSON with sorted keys so that two documents compare
    77  // with bytes.Equal. It drops nulls, which a request may omit, but keeps empty
    78  // lists and zero values, since losing those is what the tests catch.
    79  func comparableJSON(t *testing.T, data []byte) []byte {
    80  	t.Helper()
    81  	var v any
    82  	if err := json.Unmarshal(data, &v); err != nil {
    83  		t.Fatalf("Failed to parse JSON %s: %v", data, err)
    84  	}
    85  	out, err := json.Marshal(withoutNulls(v))
    86  	if err != nil {
    87  		t.Fatalf("Failed to re-encode JSON: %v", err)
    88  	}
    89  	return out
    90  }
    91  
    92  func withoutNulls(v any) any {
    93  	switch v := v.(type) {
    94  	case map[string]any:
    95  		for key, member := range v {
    96  			if member == nil {
    97  				delete(v, key)
    98  				continue
    99  			}
   100  			v[key] = withoutNulls(member)
   101  		}
   102  	case []any:
   103  		for i := range v {
   104  			v[i] = withoutNulls(v[i])
   105  		}
   106  	}
   107  	return v
   108  }
   109  
   110  // maxDepth exceeds any real nesting, so reaching it means the walk found a
   111  // cycle.
   112  const maxDepth = 8
   113  
   114  // A synthesizer generates response shapes for one API surface, consulting its
   115  // param structs for what a request can carry.
   116  type synthesizer struct {
   117  	t *testing.T
   118  	// params maps each wire "type" to the param structs that send it, which may
   119  	// be several.
   120  	params map[string][]reflect.Type
   121  	// skipped records each response field with no request counterpart as
   122  	// "Struct.key".
   123  	skipped map[string]bool
   124  }
   125  
   126  // newSynthesizer indexes every param struct reachable from requestUnion.
   127  func newSynthesizer(t *testing.T, requestUnion reflect.Type) *synthesizer {
   128  	s := &synthesizer{t: t, params: map[string][]reflect.Type{}, skipped: map[string]bool{}}
   129  	s.indexParams(requestUnion, map[reflect.Type]bool{})
   130  	return s
   131  }
   132  
   133  func (s *synthesizer) indexParams(typ reflect.Type, seen map[reflect.Type]bool) {
   134  	for typ.Kind() == reflect.Pointer || typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map {
   135  		typ = typ.Elem()
   136  	}
   137  	if typ.Kind() != reflect.Struct || seen[typ] {
   138  		return
   139  	}
   140  	seen[typ] = true
   141  	if wire := discriminator(typ); wire != "" {
   142  		s.params[wire] = append(s.params[wire], typ)
   143  	}
   144  	for i := 0; i < typ.NumField(); i++ {
   145  		s.indexParams(typ.Field(i).Type, seen)
   146  	}
   147  }
   148  
   149  // discriminator returns a struct's constant wire "type", or "" if it has none.
   150  func discriminator(typ reflect.Type) string {
   151  	if field, ok := typ.FieldByName("Type"); ok {
   152  		return field.Tag.Get("default")
   153  	}
   154  	return ""
   155  }
   156  
   157  // responseShapes returns the populated shape of typ followed by its variations.
   158  func (s *synthesizer) responseShapes(typ reflect.Type) []shape {
   159  	s.t.Helper()
   160  	return s.shapes(typ, nil, 0)
   161  }
   162  
   163  // shapes generates typ, with param naming the request-side field that carries
   164  // it when known.
   165  func (s *synthesizer) shapes(typ reflect.Type, param *reflect.StructField, depth int) []shape {
   166  	if depth > maxDepth {
   167  		s.t.Fatalf("%s nests deeper than %d levels", typ, maxDepth)
   168  	}
   169  	if typ == reflect.TypeFor[json.RawMessage]() {
   170  		return []shape{{"populated", map[string]any{"key": "value"}}, {"empty object", map[string]any{}}}
   171  	}
   172  	// constant.* discriminators report their only value through Default.
   173  	if def := reflect.Zero(typ).MethodByName("Default"); def.IsValid() {
   174  		return []shape{{"populated", def.Call(nil)[0].Interface()}}
   175  	}
   176  	if variants := unionVariants(typ); len(variants) > 0 {
   177  		return s.unionShapes(variants, depth)
   178  	}
   179  	switch typ.Kind() {
   180  	case reflect.Pointer:
   181  		return s.shapes(typ.Elem(), param, depth)
   182  	case reflect.Struct:
   183  		return s.structShapes(typ, param, depth)
   184  	case reflect.Slice:
   185  		return s.sliceShapes(typ, depth)
   186  	case reflect.String:
   187  		if typ.PkgPath() != "" {
   188  			// Named string types are enums, which are never empty.
   189  			return []shape{{"populated", "value"}}
   190  		}
   191  		return []shape{{"populated", "value"}, {"empty", ""}}
   192  	case reflect.Bool:
   193  		return []shape{{"populated", true}, {"false", false}}
   194  	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   195  		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
   196  		return []shape{{"populated", 7}, {"zero", 0}}
   197  	case reflect.Float32, reflect.Float64:
   198  		return []shape{{"populated", 7.5}, {"zero", 0}}
   199  	case reflect.Interface, reflect.Map:
   200  		return []shape{{"populated", map[string]any{"key": "value"}}}
   201  	}
   202  	s.t.Fatalf("Don't know how to generate a %s", typ)
   203  	return nil
   204  }
   205  
   206  func (s *synthesizer) unionShapes(variants []variant, depth int) []shape {
   207  	var out []shape
   208  	for _, v := range variants {
   209  		for _, sh := range s.shapes(v.Type, nil, depth+1) {
   210  			out = append(out, shape{v.Accessor + "/" + sh.Name, sh.Value})
   211  		}
   212  	}
   213  	return out
   214  }
   215  
   216  func (s *synthesizer) sliceShapes(typ reflect.Type, depth int) []shape {
   217  	items := s.shapes(typ.Elem(), nil, depth+1)
   218  	out := []shape{{"populated", []any{items[0].Value}}, {"empty", []any{}}}
   219  	for _, item := range items[1:] {
   220  		out = append(out, shape{"item " + item.Name, []any{item.Value}})
   221  	}
   222  	return out
   223  }
   224  
   225  // structShapes populates every field the request can carry, then varies one at
   226  // a time while the rest stay populated.
   227  func (s *synthesizer) structShapes(typ reflect.Type, param *reflect.StructField, depth int) []shape {
   228  	type variation struct {
   229  		key string
   230  		shape
   231  	}
   232  	paramStruct := s.paramStruct(typ, param)
   233  	populated := map[string]any{}
   234  	var variations []variation
   235  	for i := 0; i < typ.NumField(); i++ {
   236  		key := wireKey(typ.Field(i))
   237  		if key == "" {
   238  			continue
   239  		}
   240  		paramField, ok := requestField(paramStruct, key)
   241  		if paramStruct != nil && !ok {
   242  			s.skipped[typ.Name()+"."+key] = true
   243  			continue
   244  		}
   245  		fieldShapes := s.fieldShapes(typ.Field(i).Type, paramField, depth)
   246  		populated[key] = fieldShapes[0].Value
   247  		for _, sh := range fieldShapes[1:] {
   248  			variations = append(variations, variation{key, sh})
   249  		}
   250  	}
   251  	out := []shape{{"populated", populated}}
   252  	for _, v := range variations {
   253  		obj := maps.Clone(populated)
   254  		obj[v.key] = v.Value
   255  		out = append(out, shape{v.key + "=" + v.Name, obj})
   256  	}
   257  	return out
   258  }
   259  
   260  // paramStruct returns param's type when it is a struct, and otherwise the param
   261  // struct with typ's wire type that shares the most fields.
   262  func (s *synthesizer) paramStruct(typ reflect.Type, param *reflect.StructField) reflect.Type {
   263  	if param != nil && param.Type.Kind() == reflect.Struct {
   264  		return param.Type
   265  	}
   266  	var best reflect.Type
   267  	bestShared := -1
   268  	for _, candidate := range s.params[discriminator(typ)] {
   269  		if shared := sharedFields(typ, candidate); shared > bestShared {
   270  			best, bestShared = candidate, shared
   271  		}
   272  	}
   273  	return best
   274  }
   275  
   276  func sharedFields(response, param reflect.Type) int {
   277  	shared := 0
   278  	for i := 0; i < response.NumField(); i++ {
   279  		if key := wireKey(response.Field(i)); key != "" {
   280  			if _, ok := requestField(param, key); ok {
   281  				shared++
   282  			}
   283  		}
   284  	}
   285  	return shared
   286  }
   287  
   288  // fieldShapes generates one field, adding a null shape for a param.Opt
   289  // counterpart and dropping the zero shape for a scalar omitzero one.
   290  func (s *synthesizer) fieldShapes(typ reflect.Type, param *reflect.StructField, depth int) []shape {
   291  	out := s.shapes(typ, param, depth+1)
   292  	switch {
   293  	case param == nil:
   294  	case isOpt(param.Type):
   295  		out = append(out, shape{"null", nil})
   296  	case strings.Contains(param.Tag.Get("json"), ",omitzero") && isScalar(param.Type):
   297  		out = slices.DeleteFunc(out, func(sh shape) bool { return reflect.ValueOf(sh.Value).IsZero() })
   298  	}
   299  	return out
   300  }
   301  
   302  // requestField looks up the field of a param struct by wire key.
   303  func requestField(paramStruct reflect.Type, key string) (*reflect.StructField, bool) {
   304  	if paramStruct == nil {
   305  		return nil, false
   306  	}
   307  	for i := 0; i < paramStruct.NumField(); i++ {
   308  		field := paramStruct.Field(i)
   309  		if wireKey(field) == key {
   310  			return &field, true
   311  		}
   312  	}
   313  	return nil, false
   314  }
   315  
   316  func isOpt(typ reflect.Type) bool {
   317  	return strings.HasSuffix(typ.PkgPath(), "/packages/param") && strings.HasPrefix(typ.Name(), "Opt[")
   318  }
   319  
   320  func isScalar(typ reflect.Type) bool {
   321  	switch typ.Kind() {
   322  	case reflect.String, reflect.Bool,
   323  		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   324  		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   325  		reflect.Float32, reflect.Float64:
   326  		return true
   327  	}
   328  	return false
   329  }
   330  
   331  // wireKey returns a field's JSON key, or "" for a field not on the wire such as
   332  // the JSON metadata struct.
   333  func wireKey(field reflect.StructField) string {
   334  	if !field.IsExported() || field.Anonymous {
   335  		return ""
   336  	}
   337  	key, _, _ := strings.Cut(field.Tag.Get("json"), ",")
   338  	if key == "-" {
   339  		return ""
   340  	}
   341  	return key
   342  }