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 }