Last active
November 10, 2024 16:32
-
-
Save hasenj/95dbc2321dc584093537b30289bc5a58 to your computer and use it in GitHub Desktop.
TSBridge: a system to auto generate typescript bindings for Go server side functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This code is shared as-is. Use at your own risk. | |
It is not meant to be depended upon as a module. Instead, copy it to your own code base and make changes as needed. | |
CC0/MIT-0/Unlicense | |
It generates type definitions for structs and enums in Go to Typescript. | |
It's only meant to be executed locally; during development. | |
Example usage: | |
var s2t tsbridge.Bridge | |
// Add those in a loop or something | |
// pass instances of `reflect.Type` | |
// Only pass struct types | |
s2t.QueueType(someReflectedType) | |
// call this once after queueing all the types you explicitly care about | |
// this will process each type one by one, and queue any other types that | |
// are referenced until there's nothing further to process | |
s2t.Process() | |
// Now we're ready to write out the typescript type definitions | |
// create a file or otherwise make any object that implements the | |
// io.Writer interface so you can write to it | |
var f, _ = os.Create("server_types.ts") | |
tsbridge.WriteStructTSBinding(&s2t, f) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tsbridge | |
import ( | |
"fmt" | |
"go/ast" | |
"go/build" | |
"go/parser" | |
"go/token" | |
) | |
func (b *Bridge) ProcessPackage(pkgPath string) { | |
pkgInfo, err := build.Import(pkgPath, "", build.FindOnly) | |
if err != nil { | |
fmt.Println("ImportDir error:", err) | |
return | |
} | |
fset := token.NewFileSet() | |
pkgs, error := parser.ParseDir(fset, pkgInfo.Dir, nil, 0) | |
if error != nil { | |
fmt.Println("ParseDir error:", error) | |
return | |
} | |
var enumTypesMap = make(map[string]*EnumInfo) | |
for idx := range b.Enums { | |
e := &b.Enums[idx] | |
enumTypesMap[e.Name] = e | |
} | |
for _, pkg := range pkgs { | |
for _, file := range pkg.Files { | |
// We can get the values of const names from the scope objects | |
// list, but they will be out of order. | |
// | |
// We can get the order of declarations from the file declarations | |
// list, but we won't know the values for the types | |
// | |
// So we grab the values first but store them for retrival for when | |
// we iterate the declaration blocks (to get the declarations in | |
// the declaration order) | |
var nameValues = make(map[string]any) | |
for _, object := range file.Scope.Objects { | |
if object.Kind == ast.Con { | |
nameValues[object.Name] = object.Data | |
} | |
} | |
for _, decl := range file.Decls { | |
genDecl, ok := decl.(*ast.GenDecl) | |
if !ok { | |
continue | |
} | |
if genDecl.Tok != token.CONST { | |
continue | |
} | |
var enumInfo *EnumInfo | |
for _, spec := range genDecl.Specs { | |
valSpec := spec.(*ast.ValueSpec) | |
if typeIdent, ok := valSpec.Type.(*ast.Ident); ok { | |
enumInfo = enumTypesMap[typeIdent.String()] | |
} | |
if enumInfo != nil { | |
for _, nameIdent := range valSpec.Names { | |
name := nameIdent.String() | |
if name == "_" { | |
continue | |
} | |
value := nameValues[name] | |
enumInfo.Consts = append(enumInfo.Consts, ConstValue{ | |
Name: name, | |
Value: value, | |
}) | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tsbridge | |
// Supporting structs in typescript | |
import ( | |
"encoding/json" | |
"fmt" | |
"io" | |
"reflect" | |
"strings" | |
) | |
type Bridge struct { | |
Structs []StructInfo | |
Enums []EnumInfo | |
// types we want to process (structs/enums) | |
ProcessedTypes []reflect.Type | |
QueuedTypes []reflect.Type | |
// packages we want to process consts for | |
QueuedPackages []string | |
ProcessedPackages []string | |
} | |
type StructInfo struct { | |
Name string | |
Fields []StructField | |
} | |
type StructField struct { | |
Name string | |
TypeName string | |
TypeCustom bool | |
} | |
type EnumInfo struct { | |
Name string | |
TypeName string | |
Consts []ConstValue | |
} | |
type ConstValue struct { | |
Name string | |
Value any // int or string | |
} | |
func (b *Bridge) QueueObject(x interface{}) { | |
b.QueueType(reflect.TypeOf(x)) | |
} | |
func (b *Bridge) QueueType(t reflect.Type) { | |
// don't care about native types | |
if t.Name() == t.Kind().String() { | |
return | |
} | |
// make sure we don't already have it! | |
for _, pt := range b.QueuedTypes { | |
if t == pt { | |
return | |
} | |
} | |
for _, pt := range b.ProcessedTypes { | |
if t == pt { | |
return | |
} | |
} | |
// fmt.Println("Adding to queue:", t) | |
b.QueuedTypes = append(b.QueuedTypes, t) | |
} | |
func (b *Bridge) QueuePackage(pkgPath string) { | |
for _, pp := range b.QueuedPackages { | |
if pp == pkgPath { | |
return | |
} | |
} | |
for _, pp := range b.ProcessedPackages { | |
if pp == pkgPath { | |
return | |
} | |
} | |
b.QueuedPackages = append(b.QueuedPackages, pkgPath) | |
} | |
func (b *Bridge) Process() { | |
for len(b.QueuedTypes) > 0 { | |
var t = b.QueuedTypes[0] | |
b.processType(t) | |
b.QueuedTypes = b.QueuedTypes[1:] | |
} | |
for _, pkgPath := range b.QueuedPackages { | |
b.ProcessPackage(pkgPath) | |
} | |
} | |
func (b *Bridge) processType(t reflect.Type) { | |
// fmt.Println("Processing: ", t) | |
var kind = t.Kind() | |
if kind == reflect.Struct { | |
var sinfo StructInfo | |
sinfo.Name = t.Name() | |
b.AddStructFields(&sinfo, t) | |
b.Structs = append(b.Structs, sinfo) | |
b.ProcessedTypes = append(b.ProcessedTypes, t) | |
} else { | |
b.QueuePackage(t.PkgPath()) | |
var einfo EnumInfo | |
einfo.Name = t.Name() | |
einfo.TypeName = DecideEnumTypeName(t) | |
b.Enums = append(b.Enums, einfo) | |
b.ProcessedTypes = append(b.ProcessedTypes, t) | |
} | |
} | |
func (b *Bridge) AddStructFields(sinfo *StructInfo, t reflect.Type) { | |
var numFields = t.NumField() | |
for index := 0; index < numFields; index++ { | |
var field = t.Field(index) | |
if field.Anonymous { | |
b.AddStructFields(sinfo, field.Type) | |
continue | |
} | |
var sField StructField // our data | |
sField.Name = field.Name | |
sField.TypeName = field.Type.Name() | |
var jsonTag = field.Tag.Get("json") | |
if jsonTag != "" { | |
var parts = strings.Split(jsonTag, ",") | |
if parts[0] != "" { | |
if parts[0] == "-" { | |
continue | |
} | |
sField.Name = parts[0] | |
} | |
} | |
var tsTag = field.Tag.Get("ts") | |
if tsTag != "" { | |
var parts = strings.Split(tsTag, ",") | |
if parts[0] != "" { | |
sField.TypeName = parts[0] | |
sField.TypeCustom = true | |
} | |
} | |
if !sField.TypeCustom { | |
sField.TypeName = b.DecideTypescriptTypeName(field.Type) | |
} | |
sinfo.Fields = append(sinfo.Fields, sField) | |
} | |
} | |
func DecideEnumTypeName(t reflect.Type) string { | |
switch t.Kind() { | |
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |
return "number" | |
case reflect.String: | |
return "string" | |
default: | |
return "void" | |
} | |
} | |
func (b *Bridge) DecideTypescriptTypeName(t reflect.Type) string { | |
var kind = t.Kind() | |
// fmt.Println("Type:", t, "Kind:", kind) | |
switch kind { | |
case reflect.Struct: | |
b.QueueType(t) | |
return t.Name() | |
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: | |
if t.String() != kind.String() { | |
// potentially an enum! | |
// fmt.Println(" Maybe an enum!") | |
b.QueueType(t) | |
return t.Name() | |
} | |
return "number" | |
case reflect.String: | |
if t.String() != kind.String() { | |
// potentially an enum! | |
// fmt.Println(" Maybe an enum!") | |
b.QueueType(t) | |
return t.Name() | |
} | |
return "string" | |
case reflect.Bool: | |
return "boolean" | |
case reflect.Slice, reflect.Array: | |
var elementType = b.DecideTypescriptTypeName(t.Elem()) | |
if elementType == "" { | |
return "" | |
} | |
return elementType + "[] | null" | |
case reflect.Map: | |
var elementType = b.DecideTypescriptTypeName(t.Elem()) | |
if elementType == "" { | |
return "" | |
} | |
var keyType = b.DecideTypescriptTypeName(t.Key()) | |
if keyType == "" { | |
return "" | |
} | |
return fmt.Sprintf("Record<%s, %s>", keyType, elementType) | |
case reflect.Ptr: | |
var elementType = b.DecideTypescriptTypeName(t.Elem()) | |
if elementType == "" { | |
return "" | |
} | |
return elementType + " | null" | |
case reflect.Interface: | |
// TODO: should we create a matching TS interface type?! | |
return "any" | |
default: | |
return "" | |
} | |
} | |
func WriteStructTSBinding(b *Bridge, w io.Writer) { | |
for index := range b.Enums { | |
var einfo = &b.Enums[index] | |
fmt.Fprintf(w, "export type %s = %s;\n", einfo.Name, einfo.TypeName) | |
for _, c := range einfo.Consts { | |
b, err := json.Marshal(c.Value) | |
if err != nil { | |
fmt.Printf("Could not print out the constant value for %s: got error: %v\n", c.Name, err) | |
} | |
fmt.Fprintf(w, "export const %s: %s = %s;\n", c.Name, einfo.Name, b) | |
} | |
fmt.Fprintln(w) | |
} | |
for index := range b.Structs { | |
var sinfo = &b.Structs[index] | |
fmt.Fprintf(w, "export interface %s {\n", sinfo.Name) | |
for findex := range sinfo.Fields { | |
var field = &sinfo.Fields[findex] | |
fmt.Fprintf(w, " %s: %s\n", field.Name, field.TypeName) | |
} | |
fmt.Fprintf(w, "}\n\n") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment