| 1 | package hostrpc |
| 2 | |
| 3 | import ( |
| 4 | "encoding" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "reflect" |
| 8 | "strings" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // Kind is the JSON shape a TypeRef describes. |
| 13 | type Kind string |
| 14 | |
| 15 | const ( |
| 16 | KindString Kind = "string" |
| 17 | KindNumber Kind = "number" |
| 18 | KindInteger Kind = "integer" |
| 19 | KindBoolean Kind = "boolean" |
| 20 | KindAny Kind = "any" |
| 21 | KindArray Kind = "array" |
| 22 | KindMap Kind = "map" |
| 23 | KindObject Kind = "object" |
| 24 | KindNullable Kind = "nullable" |
| 25 | ) |
| 26 | |
| 27 | // TypeRef describes one JSON value shape. Elem is the element of an array, |
| 28 | // map or nullable; Key is a map's key shape; Ref names an entry of |
| 29 | // Contract.Types for objects. |
| 30 | type TypeRef struct { |
| 31 | Kind Kind `json:"kind"` |
| 32 | Elem *TypeRef `json:"elem,omitempty"` |
| 33 | Key *TypeRef `json:"key,omitempty"` |
| 34 | Ref string `json:"ref,omitempty"` |
| 35 | } |
| 36 | |
| 37 | // Field is one member of an ObjectType. Optional marks members the encoder |
| 38 | // may omit (omitempty, omitzero) or emit as null (pointers). |
| 39 | type Field struct { |
| 40 | Name string `json:"name"` |
| 41 | Type TypeRef `json:"type"` |
| 42 | Optional bool `json:"optional,omitempty"` |
| 43 | } |
| 44 | |
| 45 | // ObjectType is a Go struct as encoding/json writes it. |
| 46 | type ObjectType struct { |
| 47 | Fields []Field `json:"fields"` |
| 48 | } |
| 49 | |
| 50 | var ( |
| 51 | timeType = reflect.TypeFor[time.Time]() |
| 52 | rawMessageType = reflect.TypeFor[json.RawMessage]() |
| 53 | textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]() |
| 54 | jsonMarshalerType = reflect.TypeFor[json.Marshaler]() |
| 55 | errorType = reflect.TypeFor[error]() |
| 56 | ) |
| 57 | |
| 58 | // typeCollector builds TypeRefs and gathers every named struct it crosses, |
| 59 | // keyed by pkg.Name, so a contract lists each DTO exactly once. |
| 60 | type typeCollector struct { |
| 61 | types map[string]ObjectType |
| 62 | named map[reflect.Type]string |
| 63 | } |
| 64 | |
| 65 | func newTypeCollector() *typeCollector { |
| 66 | return &typeCollector{types: map[string]ObjectType{}, named: map[reflect.Type]string{}} |
| 67 | } |
| 68 | |
| 69 | // ref describes t. hint names an anonymous struct reached through t. |
| 70 | func (c *typeCollector) ref(t reflect.Type, hint string) (TypeRef, error) { |
| 71 | switch { |
| 72 | case t == timeType: |
| 73 | return TypeRef{Kind: KindString}, nil |
| 74 | case t == rawMessageType: |
| 75 | return TypeRef{Kind: KindAny}, nil |
| 76 | case !implements(t, jsonMarshalerType) && implements(t, textMarshalerType): |
| 77 | return TypeRef{Kind: KindString}, nil |
| 78 | } |
| 79 | switch t.Kind() { |
| 80 | case reflect.Interface: |
| 81 | return TypeRef{Kind: KindAny}, nil |
| 82 | case reflect.Pointer: |
| 83 | return c.wrap(KindNullable, t.Elem(), hint) |
| 84 | case reflect.String: |
| 85 | return TypeRef{Kind: KindString}, nil |
| 86 | case reflect.Bool: |
| 87 | return TypeRef{Kind: KindBoolean}, nil |
| 88 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, |
| 89 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
| 90 | return TypeRef{Kind: KindInteger}, nil |
| 91 | case reflect.Float32, reflect.Float64: |
| 92 | return TypeRef{Kind: KindNumber}, nil |
| 93 | case reflect.Slice: |
| 94 | if t.Elem().Kind() == reflect.Uint8 && !implements(t.Elem(), jsonMarshalerType) { |
| 95 | return TypeRef{Kind: KindString}, nil |
| 96 | } |
| 97 | return c.wrap(KindArray, t.Elem(), hint) |
| 98 | case reflect.Array: |
| 99 | return c.wrap(KindArray, t.Elem(), hint) |
| 100 | case reflect.Map: |
| 101 | return c.mapRef(t, hint) |
| 102 | case reflect.Struct: |
| 103 | return c.structRef(t, hint) |
| 104 | } |
| 105 | return TypeRef{}, fmt.Errorf("%s is not JSON-serialisable", t) |
| 106 | } |
| 107 | |
| 108 | func (c *typeCollector) wrap(kind Kind, elem reflect.Type, hint string) (TypeRef, error) { |
| 109 | ref, err := c.ref(elem, hint) |
| 110 | if err != nil { |
| 111 | return TypeRef{}, err |
| 112 | } |
| 113 | return TypeRef{Kind: kind, Elem: &ref}, nil |
| 114 | } |
| 115 | |
| 116 | func (c *typeCollector) mapRef(t reflect.Type, hint string) (TypeRef, error) { |
| 117 | var key TypeRef |
| 118 | switch kt := t.Key(); { |
| 119 | case kt.Kind() == reflect.String || implements(kt, textMarshalerType): |
| 120 | key = TypeRef{Kind: KindString} |
| 121 | case isIntegerKind(kt.Kind()): |
| 122 | key = TypeRef{Kind: KindInteger} |
| 123 | default: |
| 124 | return TypeRef{}, fmt.Errorf("%s: map key %s is not JSON-serialisable", t, kt) |
| 125 | } |
| 126 | elem, err := c.ref(t.Elem(), hint) |
| 127 | if err != nil { |
| 128 | return TypeRef{}, err |
| 129 | } |
| 130 | return TypeRef{Kind: KindMap, Key: &key, Elem: &elem}, nil |
| 131 | } |
| 132 | |
| 133 | func (c *typeCollector) structRef(t reflect.Type, hint string) (TypeRef, error) { |
| 134 | name, seen := c.named[t] |
| 135 | if seen { |
| 136 | return TypeRef{Kind: KindObject, Ref: name}, nil |
| 137 | } |
| 138 | // Type.String uses the package name, so "main.TabMeta" is the same key in |
| 139 | // the shipped binary and in the test binary that compiles package main |
| 140 | // under its import path; PkgPath would split the digest between them. |
| 141 | name = hint |
| 142 | if t.Name() != "" { |
| 143 | name = t.String() |
| 144 | } |
| 145 | if _, taken := c.types[name]; taken { |
| 146 | return TypeRef{}, fmt.Errorf("%s: type name %q is already used by another type", t, name) |
| 147 | } |
| 148 | c.named[t] = name |
| 149 | c.types[name] = ObjectType{} |
| 150 | fields, err := c.fields(t, name) |
| 151 | if err != nil { |
| 152 | return TypeRef{}, fmt.Errorf("%s: %w", name, err) |
| 153 | } |
| 154 | c.types[name] = ObjectType{Fields: fields} |
| 155 | return TypeRef{Kind: KindObject, Ref: name}, nil |
| 156 | } |
| 157 | |
| 158 | type fieldCandidate struct { |
| 159 | field Field |
| 160 | depth int |
| 161 | tagged bool |
| 162 | } |
| 163 | |
| 164 | func (c *typeCollector) fields(t reflect.Type, owner string) ([]Field, error) { |
| 165 | var found []fieldCandidate |
| 166 | if err := c.walkFields(t, owner, 0, &found); err != nil { |
| 167 | return nil, err |
| 168 | } |
| 169 | return dominantFields(found), nil |
| 170 | } |
| 171 | |
| 172 | // walkFields mirrors encoding/json: embedded structs without a tag name are |
| 173 | // flattened, tagged or non-struct embeds become members named after the type. |
| 174 | func (c *typeCollector) walkFields(t reflect.Type, owner string, depth int, out *[]fieldCandidate) error { |
| 175 | for i := range t.NumField() { |
| 176 | f := t.Field(i) |
| 177 | tag := f.Tag.Get("json") |
| 178 | if tag == "-" { |
| 179 | continue |
| 180 | } |
| 181 | name, opts, _ := strings.Cut(tag, ",") |
| 182 | if f.Anonymous && name == "" { |
| 183 | et := f.Type |
| 184 | if et.Kind() == reflect.Pointer { |
| 185 | et = et.Elem() |
| 186 | } |
| 187 | if et.Kind() == reflect.Struct { |
| 188 | if err := c.walkFields(et, owner, depth+1, out); err != nil { |
| 189 | return err |
| 190 | } |
| 191 | continue |
| 192 | } |
| 193 | } |
| 194 | if !f.IsExported() { |
| 195 | continue |
| 196 | } |
| 197 | if name == "" { |
| 198 | name = f.Name |
| 199 | } |
| 200 | ref, err := c.ref(f.Type, owner+"."+f.Name) |
| 201 | if err != nil { |
| 202 | return fmt.Errorf("field %s: %w", f.Name, err) |
| 203 | } |
| 204 | if hasOption(opts, "string") && quotable(f.Type) { |
| 205 | ref = TypeRef{Kind: KindString} |
| 206 | } |
| 207 | optional := hasOption(opts, "omitempty") || hasOption(opts, "omitzero") || f.Type.Kind() == reflect.Pointer |
| 208 | *out = append(*out, fieldCandidate{ |
| 209 | field: Field{Name: name, Type: ref, Optional: optional}, |
| 210 | depth: depth, |
| 211 | tagged: tag != "", |
| 212 | }) |
| 213 | } |
| 214 | return nil |
| 215 | } |
| 216 | |
| 217 | // dominantFields applies encoding/json's shadowing: the shallowest member |
| 218 | // wins, a tagged one breaks a tie, an unresolved tie drops the name. |
| 219 | func dominantFields(found []fieldCandidate) []Field { |
| 220 | byName := map[string][]fieldCandidate{} |
| 221 | var order []string |
| 222 | for _, cand := range found { |
| 223 | if _, ok := byName[cand.field.Name]; !ok { |
| 224 | order = append(order, cand.field.Name) |
| 225 | } |
| 226 | byName[cand.field.Name] = append(byName[cand.field.Name], cand) |
| 227 | } |
| 228 | out := make([]Field, 0, len(order)) |
| 229 | for _, name := range order { |
| 230 | if f, ok := dominant(byName[name]); ok { |
| 231 | out = append(out, f) |
| 232 | } |
| 233 | } |
| 234 | return out |
| 235 | } |
| 236 | |
| 237 | func dominant(cands []fieldCandidate) (Field, bool) { |
| 238 | minDepth := cands[0].depth |
| 239 | for _, cand := range cands[1:] { |
| 240 | minDepth = min(minDepth, cand.depth) |
| 241 | } |
| 242 | var shallow, tagged []fieldCandidate |
| 243 | for _, cand := range cands { |
| 244 | if cand.depth != minDepth { |
| 245 | continue |
| 246 | } |
| 247 | shallow = append(shallow, cand) |
| 248 | if cand.tagged { |
| 249 | tagged = append(tagged, cand) |
| 250 | } |
| 251 | } |
| 252 | switch { |
| 253 | case len(shallow) == 1: |
| 254 | return shallow[0].field, true |
| 255 | case len(tagged) == 1: |
| 256 | return tagged[0].field, true |
| 257 | } |
| 258 | return Field{}, false |
| 259 | } |
| 260 | |
| 261 | func hasOption(opts, want string) bool { |
| 262 | for opts != "" { |
| 263 | var opt string |
| 264 | opt, opts, _ = strings.Cut(opts, ",") |
| 265 | if opt == want { |
| 266 | return true |
| 267 | } |
| 268 | } |
| 269 | return false |
| 270 | } |
| 271 | |
| 272 | func quotable(t reflect.Type) bool { |
| 273 | if t.Kind() == reflect.Pointer { |
| 274 | t = t.Elem() |
| 275 | } |
| 276 | switch k := t.Kind(); { |
| 277 | case k == reflect.String, k == reflect.Bool, isIntegerKind(k), k == reflect.Float32, k == reflect.Float64: |
| 278 | return true |
| 279 | } |
| 280 | return false |
| 281 | } |
| 282 | |
| 283 | func isIntegerKind(k reflect.Kind) bool { |
| 284 | return k >= reflect.Int && k <= reflect.Uintptr |
| 285 | } |
| 286 | |
| 287 | func implements(t, iface reflect.Type) bool { |
| 288 | return t.Implements(iface) || reflect.PointerTo(t).Implements(iface) |
| 289 | } |
| 290 |