fix: generate typescript types for healthcheck pkg (#8846)

This commit is contained in:
Colin Adler
2023-08-03 16:20:46 -05:00
committed by GitHub
parent 72780c8031
commit 8ee500c59d
21 changed files with 333 additions and 204 deletions
+76 -33
View File
@@ -15,6 +15,8 @@ import (
"text/template"
"github.com/fatih/structtag"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"golang.org/x/tools/go/packages"
"golang.org/x/xerrors"
@@ -23,21 +25,25 @@ import (
"github.com/coder/coder/coderd/util/slice"
)
const (
baseDir = "./codersdk"
indent = " "
var (
baseDirs = [...]string{"./codersdk", "./coderd/healthcheck"}
indent = " "
)
func main() {
ctx := context.Background()
log := slog.Make(sloghuman.Sink(os.Stderr))
output, err := Generate(baseDir)
if err != nil {
log.Fatal(ctx, err.Error())
}
_, _ = fmt.Print("// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.\n\n")
for _, baseDir := range baseDirs {
_, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(baseDir, "./"))
output, err := Generate(baseDir)
if err != nil {
log.Fatal(ctx, err.Error())
}
// Just cat the output to a file to capture it
_, _ = fmt.Println(output)
// Just cat the output to a file to capture it
_, _ = fmt.Print(output, "\n\n")
}
}
func Generate(directory string) (string, error) {
@@ -63,11 +69,6 @@ type TypescriptTypes struct {
// String just combines all the codeblocks.
func (t TypescriptTypes) String() string {
var s strings.Builder
const prelude = `
// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.
`
_, _ = s.WriteString(prelude)
sortedTypes := make([]string, 0, len(t.Types))
sortedEnums := make([]string, 0, len(t.Enums))
@@ -176,6 +177,7 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) {
Enums: make(map[string]types.Object),
EnumConsts: make(map[string][]*types.Const),
IgnoredTypes: make(map[string]struct{}),
AllowedTypes: make(map[string]struct{}),
}
// Look for comments that indicate to ignore a type for typescript generation.
@@ -196,6 +198,24 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) {
}
}
// This allows opt-in generation, instead of opt-out.
allowRegex := regexp.MustCompile("@typescript-generate[:]?(?P<allowed_types>.*)")
for _, file := range g.pkg.Syntax {
for _, comment := range file.Comments {
for _, line := range comment.List {
text := line.Text
matches := allowRegex.FindStringSubmatch(text)
allowed := allowRegex.SubexpIndex("allowed_types")
if len(matches) >= allowed && matches[allowed] != "" {
arr := strings.Split(matches[allowed], ",")
for _, s := range arr {
m.AllowedTypes[strings.TrimSpace(s)] = struct{}{}
}
}
}
}
}
for _, n := range g.pkg.Types.Scope().Names() {
obj := g.pkg.Types.Scope().Lookup(n)
err := g.generateOne(m, obj)
@@ -260,6 +280,15 @@ type Maps struct {
Enums map[string]types.Object
EnumConsts map[string][]*types.Const
IgnoredTypes map[string]struct{}
AllowedTypes map[string]struct{}
}
// objName prepends the package name of a type if it is outside of codersdk.
func objName(obj types.Object) string {
if pkgName := obj.Pkg().Name(); pkgName != "codersdk" {
return cases.Title(language.English).String(pkgName) + obj.Name()
}
return obj.Name()
}
func (g *Generator) generateOne(m *Maps, obj types.Object) error {
@@ -273,6 +302,13 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
return nil
}
// If we have allowed types, only allow those to be generated.
if _, ok := m.AllowedTypes[obj.Name()]; len(m.AllowedTypes) > 0 && !ok {
return nil
}
objName := objName(obj)
switch obj := obj.(type) {
// All named types are type declarations
case *types.TypeName:
@@ -286,13 +322,13 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
// Structs are obvious.
codeBlock, err := g.buildStruct(obj, underNamed)
if err != nil {
return xerrors.Errorf("generate %q: %w", obj.Name(), err)
return xerrors.Errorf("generate %q: %w", objName, err)
}
m.Structs[obj.Name()] = codeBlock
m.Structs[objName] = codeBlock
case *types.Basic:
// type <Name> string
// These are enums. Store to expand later.
m.Enums[obj.Name()] = obj
m.Enums[objName] = obj
case *types.Map, *types.Array, *types.Slice:
// Declared maps that are not structs are still valid codersdk objects.
// Handle them custom by calling 'typescriptType' directly instead of
@@ -301,7 +337,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
// These are **NOT** enums, as a map in Go would never be used for an enum.
ts, err := g.typescriptType(obj.Type().Underlying())
if err != nil {
return xerrors.Errorf("(map) generate %q: %w", obj.Name(), err)
return xerrors.Errorf("(map) generate %q: %w", objName, err)
}
var str strings.Builder
@@ -311,8 +347,8 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
_, _ = str.WriteRune('\n')
}
// Use similar output syntax to enums.
_, _ = str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType))
m.Structs[obj.Name()] = str.String()
_, _ = str.WriteString(fmt.Sprintf("export type %s = %s\n", objName, ts.ValueType))
m.Structs[objName] = str.String()
case *types.Interface:
// Interfaces are used as generics. Non-generic interfaces are
// not supported.
@@ -330,9 +366,9 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
block, err := g.buildUnion(obj, union)
if err != nil {
return xerrors.Errorf("generate union %q: %w", obj.Name(), err)
return xerrors.Errorf("generate union %q: %w", objName, err)
}
m.Generics[obj.Name()] = block
m.Generics[objName] = block
}
case *types.Signature:
// Ignore named functions.
@@ -353,7 +389,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
case *types.Func:
// Noop
default:
_, _ = fmt.Println(obj.Name())
_, _ = fmt.Println(objName)
}
return nil
}
@@ -361,7 +397,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
func (g *Generator) posLine(obj types.Object) string {
file := g.pkg.Fset.File(obj.Pos())
// Do not use filepath, as that changes behavior based on OS
return fmt.Sprintf("// From %s\n", path.Join("codersdk", filepath.Base(file.Name())))
return fmt.Sprintf("// From %s\n", path.Join(obj.Pkg().Name(), filepath.Base(file.Name())))
}
// buildStruct just prints the typescript def for a type.
@@ -387,7 +423,7 @@ func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error
allTypes = slice.Unique(allTypes)
_, _ = s.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), strings.Join(allTypes, " | ")))
_, _ = s.WriteString(fmt.Sprintf("export type %s = %s\n", objName(obj), strings.Join(allTypes, " | ")))
return s.String(), nil
}
@@ -421,7 +457,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err
}
state.PosLine = g.posLine(obj)
state.Name = obj.Name()
state.Name = objName(obj)
// Handle named embedded structs in the codersdk package via extension.
var extends []string
@@ -453,6 +489,10 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err
panic("invalid struct tags on type " + obj.String())
}
if !field.Exported() {
continue
}
// Use the json name if present
jsonTag, err := tags.Get("json")
var (
@@ -729,6 +769,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
case "time.Time":
// We really should come up with a standard for time.
return TypescriptType{ValueType: "string"}, nil
case "time.Duration":
return TypescriptType{ValueType: "number"}, nil
case "database/sql.NullTime":
return TypescriptType{ValueType: "string", Optional: true}, nil
case "github.com/coder/coder/codersdk.NullTime":
@@ -744,12 +786,13 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
}
// Then see if the type is defined elsewhere. If it is, we can just
// put the name as it will be defined in the typescript codeblock
// put the objName as it will be defined in the typescript codeblock
// we generate.
name := n.Obj().Name()
objName := objName(n.Obj())
genericName := ""
genericTypes := make(map[string]string)
if obj := g.pkg.Types.Scope().Lookup(name); obj != nil {
pkgName := n.Obj().Pkg().Name()
if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil {
// Sweet! Using other typescript types as fields. This could be an
// enum or another struct
if args := n.TypeArgs(); args != nil && args.Len() > 0 {
@@ -758,7 +801,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
for i := 0; i < args.Len(); i++ {
genType, err := g.typescriptType(args.At(i))
if err != nil {
return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", name, args.At(i).String(), err)
return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", objName, args.At(i).String(), err)
}
if param, ok := args.At(i).(*types.TypeParam); ok {
@@ -773,13 +816,13 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
genericConstraints = append(genericConstraints, genType.ValueType)
}
genericName = name + fmt.Sprintf("<%s>", strings.Join(genericNames, ", "))
name += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", "))
genericName = objName + fmt.Sprintf("<%s>", strings.Join(genericNames, ", "))
objName += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", "))
}
return TypescriptType{
GenericTypes: genericTypes,
GenericValue: genericName,
ValueType: name,
ValueType: objName,
}, nil
}
+1 -1
View File
@@ -1,4 +1,4 @@
package enums
package codersdk
type (
Enum string
-2
View File
@@ -1,5 +1,3 @@
// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.
// From codersdk/enums.go
export type Enums = Enum[]
+1 -1
View File
@@ -1,4 +1,4 @@
package genericmap
package codersdk
type Foo struct {
Bar string `json:"bar"`
-2
View File
@@ -1,5 +1,3 @@
// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.
// From codersdk/genericmap.go
export interface Buzz {
readonly foo: Foo
+17 -17
View File
@@ -1,4 +1,4 @@
package generics
package codersdk
import "time"
@@ -10,26 +10,26 @@ type Custom interface {
string | bool | int | time.Duration | []string | *int
}
// StaticGeneric has all generic fields defined in the field
type StaticGeneric struct {
Static GenericFields[string, int, time.Duration, string] `json:"static"`
// Static has all generic fields defined in the field
type Static struct {
Static Fields[string, int, time.Duration, string] `json:"static"`
}
// DynamicGeneric can has some dynamic fields
type DynamicGeneric[A any, S Single] struct {
Dynamic GenericFields[bool, A, string, S] `json:"dynamic"`
Comparable bool `json:"comparable"`
// Dynamic has some dynamic fields.
type Dynamic[A any, S Single] struct {
Dynamic Fields[bool, A, string, S] `json:"dynamic"`
Comparable bool `json:"comparable"`
}
type ComplexGeneric[C comparable, S Single, T Custom] struct {
Dynamic GenericFields[C, bool, string, S] `json:"dynamic"`
Order GenericFieldsDiffOrder[C, string, S, T] `json:"order"`
Comparable C `json:"comparable"`
Single S `json:"single"`
Static StaticGeneric `json:"static"`
type Complex[C comparable, S Single, T Custom] struct {
Dynamic Fields[C, bool, string, S] `json:"dynamic"`
Order FieldsDiffOrder[C, string, S, T] `json:"order"`
Comparable C `json:"comparable"`
Single S `json:"single"`
Static Static `json:"static"`
}
type GenericFields[C comparable, A any, T Custom, S Single] struct {
type Fields[C comparable, A any, T Custom, S Single] struct {
Comparable C `json:"comparable"`
Any A `json:"any"`
@@ -38,6 +38,6 @@ type GenericFields[C comparable, A any, T Custom, S Single] struct {
SingleConstraint S `json:"single_constraint"`
}
type GenericFieldsDiffOrder[A any, C comparable, S Single, T Custom] struct {
GenericFields[C, A, T, S]
type FieldsDiffOrder[A any, C comparable, S Single, T Custom] struct {
Fields Fields[C, A, T, S]
}
+11 -13
View File
@@ -1,22 +1,20 @@
// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.
// From codersdk/generics.go
export interface ComplexGeneric<C extends comparable, S extends Single, T extends Custom> {
readonly dynamic: GenericFields<C, boolean, string, S>
readonly order: GenericFieldsDiffOrder<C, string, S, T>
export interface Complex<C extends comparable, S extends Single, T extends Custom> {
readonly dynamic: Fields<C, boolean, string, S>
readonly order: FieldsDiffOrder<C, string, S, T>
readonly comparable: C
readonly single: S
readonly static: StaticGeneric
readonly static: Static
}
// From codersdk/generics.go
export interface DynamicGeneric<A extends any, S extends Single> {
readonly dynamic: GenericFields<boolean, A, string, S>
export interface Dynamic<A extends any, S extends Single> {
readonly dynamic: Fields<boolean, A, string, S>
readonly comparable: boolean
}
// From codersdk/generics.go
export interface GenericFields<C extends comparable, A extends any, T extends Custom, S extends Single> {
export interface Fields<C extends comparable, A extends any, T extends Custom, S extends Single> {
readonly comparable: C
readonly any: A
readonly custom: T
@@ -25,13 +23,13 @@ export interface GenericFields<C extends comparable, A extends any, T extends Cu
}
// From codersdk/generics.go
export interface GenericFieldsDiffOrder<A extends any, C extends comparable, S extends Single, T extends Custom> {
readonly GenericFields: GenericFields<C, A, T, S>
export interface FieldsDiffOrder<A extends any, C extends comparable, S extends Single, T extends Custom> {
readonly Fields: Fields<C, A, T, S>
}
// From codersdk/generics.go
export interface StaticGeneric {
readonly static: GenericFields<string, number, number, string>
export interface Static {
readonly static: Fields<string, number, number, string>
}
// From codersdk/generics.go