Skip to content

feat: Switch packages for typescript generation code #1196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 28, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix ignore, add optional fields
  • Loading branch information
Emyrk committed Apr 28, 2022
commit 2741efadd413ba0dee7d0ea59b3323ccd62ec06e
104 changes: 66 additions & 38 deletions scripts/apitypings/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ func main() {
fmt.Println(codeBlocks.String())
}

// TypescriptTypes holds all the code blocks created.
type TypescriptTypes struct {
// Each entry is the type name, and it's typescript code block.
Types map[string]string
Enums map[string]string
}

// String just combines all the codeblocks. I store them in a map for unit testing purposes
// String just combines all the codeblocks.
func (t TypescriptTypes) String() string {
var s strings.Builder
sortedTypes := make([]string, 0, len(t.Types))
Expand Down Expand Up @@ -122,19 +123,25 @@ func (g *Generator) parsePackage(ctx context.Context, patterns ...string) error
func (g *Generator) generateAll() (*TypescriptTypes, error) {
structs := make(map[string]string)
enums := make(map[string]types.Object)
constants := make(map[string][]*types.Const)
enumConsts := make(map[string][]*types.Const)
//constants := make(map[string]string)

// Look for comments that indicate to ignore a type for typescript generation.
ignoredTypes := make(map[string]struct{})
ignoreRegex := regexp.MustCompile("@typescript-ignore:(?P<ignored_types>)")
ignoreRegex := regexp.MustCompile("@typescript-ignore[:]?(?P<ignored_types>.*)")
for _, file := range g.pkg.Syntax {
for _, comment := range file.Comments {
matches := ignoreRegex.FindStringSubmatch(comment.Text())
ignored := ignoreRegex.SubexpIndex("ignored_types")
if len(matches) >= ignored && matches[ignored] != "" {
arr := strings.Split(matches[ignored], ",")
for _, s := range arr {
ignoredTypes[strings.TrimSpace(s)] = struct{}{}
for _, line := range comment.List {
text := line.Text
matches := ignoreRegex.FindStringSubmatch(text)
ignored := ignoreRegex.SubexpIndex("ignored_types")
if len(matches) >= ignored && matches[ignored] != "" {
arr := strings.Split(matches[ignored], ",")
for _, s := range arr {
ignoredTypes[strings.TrimSpace(s)] = struct{}{}
}
}

}
}
}
Expand All @@ -146,6 +153,7 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) {
continue
}

// Exclude ignored types
if _, ok := ignoredTypes[obj.Name()]; ok {
continue
}
Expand All @@ -171,24 +179,28 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) {
enums[obj.Name()] = obj
}
case *types.Var:
// TODO: Are any enums var declarations?
// TODO: Are any enums var declarations? This is also codersdk.Me.
v := obj.(*types.Var)
var _ = v
case *types.Const:
c := obj.(*types.Const)
// We only care about named constant types, since they are enums
if named, ok := c.Type().(*types.Named); ok {
name := named.Obj().Name()
constants[name] = append(constants[name], c)
enumConsts[name] = append(enumConsts[name], c)
}
case *types.Func:
// Noop
default:
fmt.Println(obj.Name())
}
}

// Write all enums
enumCodeBlocks := make(map[string]string)
for name, v := range enums {
var values []string
for _, elem := range constants[name] {
for _, elem := range enumConsts[name] {
// TODO: If we have non string constants, we need to handle that
// here.
values = append(values, elem.Val().String())
Expand Down Expand Up @@ -236,37 +248,47 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err
jsonName = field.Name()
}

var tsType string
var comment string
var tsType TypescriptType
// If a `typescript:"string"` exists, we take this, and do not try to infer.
typescriptTag := tag.Get("typescript")
if typescriptTag == "-" {
// Ignore this field
continue
} else if typescriptTag != "" {
tsType = typescriptTag
tsType.ValueType = typescriptTag
} else {
var err error
tsType, comment, err = g.typescriptType(obj, field.Type())
tsType, err = g.typescriptType(obj, field.Type())
if err != nil {
return "", xerrors.Errorf("typescript type: %w", err)
}
}

if comment != "" {
s.WriteString(fmt.Sprintf("\t// %s\n", comment))
if tsType.Comment != "" {
s.WriteString(fmt.Sprintf("\t// %s\n", tsType.Comment))
}
optional := ""
if tsType.Optional {
optional = "?"
}
s.WriteString(fmt.Sprintf("\treadonly %s: %s\n", jsonName, tsType))
s.WriteString(fmt.Sprintf("\treadonly %s%s: %s\n", jsonName, optional, tsType.ValueType))
}
s.WriteString("}\n")
return s.String(), nil
}

type TypescriptType struct {
ValueType string
Comment string
// Optional indicates the value is an optional field in typescript.
Optional bool
}

// typescriptType this function returns a typescript type for a given
// golang type.
// Eg:
// []byte returns "string"
func (g *Generator) typescriptType(obj types.Object, ty types.Type) (string, string, error) {
func (g *Generator) typescriptType(obj types.Object, ty types.Type) (TypescriptType, error) {
switch ty.(type) {
case *types.Basic:
bs := ty.(*types.Basic)
Expand All @@ -275,22 +297,22 @@ func (g *Generator) typescriptType(obj types.Object, ty types.Type) (string, str
// we want to put another switch to capture these types
// and rename to typescript.
switch {
case bs.Info() == types.IsNumeric:
return "number", "", nil
case bs.Info() == types.IsBoolean:
return "boolean", "", nil
case bs.Info()&types.IsNumeric > 0:
return TypescriptType{ValueType: "number"}, nil
case bs.Info()&types.IsBoolean > 0:
return TypescriptType{ValueType: "boolean"}, nil
case bs.Kind() == types.Byte:
// TODO: @emyrk What is a byte for typescript? A string? A uint8?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add []byte to a type, we can adjust as needed. For now, I just use a number for a byte, and string for []byte 🤷.

TIL about that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was guessing these types should accept the golang marshal json. So this will at least work in the sense the 2 types can be communicated over json.

return "byte", "", nil
return TypescriptType{ValueType: "number", Comment: "This is a byte in golang"}, nil
default:
return bs.Name(), "", nil
return TypescriptType{ValueType: bs.Name()}, nil
}
case *types.Struct:
// TODO: This kinda sucks right now. It just dumps the struct def
return ty.String(), "Unknown struct, this might not work", nil
return TypescriptType{ValueType: ty.String(), Comment: "Unknown struct, this might not work"}, nil
case *types.Map:
// TODO: Typescript dictionary??? Object?
return "map_not_implemented", "", nil
return TypescriptType{ValueType: "map_not_implemented"}, nil
case *types.Slice, *types.Array:
// Slice/Arrays are pretty much the same.
type hasElem interface {
Expand All @@ -304,14 +326,14 @@ func (g *Generator) typescriptType(obj types.Object, ty types.Type) (string, str
case arr.Elem().String() == "byte":
// All byte arrays are strings on the typescript.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vapurrmaid could you weigh in here?

Copy link
Member Author

@Emyrk Emyrk Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, really curious here. The json marshal is a string, which is why I chose it. It's base64 encoded. Not ideal still?

https://go.dev/play/p/mv6bH0b9g4N

// Is this ok?
return "string", "", nil
return TypescriptType{ValueType: "string"}, nil
default:
// By default, just do an array of the underlying type.
underlying, comment, err := g.typescriptType(obj, arr.Elem())
underlying, err := g.typescriptType(obj, arr.Elem())
if err != nil {
return "", "", xerrors.Errorf("array: %w", err)
return TypescriptType{}, xerrors.Errorf("array: %w", err)
}
return underlying + "[]", comment, nil
return TypescriptType{ValueType: underlying.ValueType + "[]", Comment: underlying.Comment}, nil
}
case *types.Named:
n := ty.(*types.Named)
Expand All @@ -322,20 +344,21 @@ func (g *Generator) typescriptType(obj types.Object, ty types.Type) (string, str
if obj := g.pkg.Types.Scope().Lookup(name); obj != nil {
// Sweet! Using other typescript types as fields. This could be an
// enum or another struct
return name, "", nil
return TypescriptType{ValueType: name}, nil
}

// These are special types that we handle uniquely.
switch n.String() {
case "net/url.URL":
return "string", "", nil
return TypescriptType{ValueType: "string"}, nil
case "time.Time":
return "string", "is this ok for time?", nil
// We really should come up with a standard for time.
return TypescriptType{ValueType: "string"}, nil
}

// If it's a struct, just use the name of the struct type
if _, ok := n.Underlying().(*types.Struct); ok {
return name, "Unknown named type, this might not work", nil
return TypescriptType{ValueType: name, Comment: "Unknown named type, this might not work"}, nil
}

// Defer to the underlying type.
Expand All @@ -345,10 +368,15 @@ func (g *Generator) typescriptType(obj types.Object, ty types.Type) (string, str
// TODO: Nullable fields? We could say these fields can be null in the
// typescript.
pt := ty.(*types.Pointer)
return g.typescriptType(obj, pt.Elem())
resp, err := g.typescriptType(obj, pt.Elem())
if err != nil {
return TypescriptType{}, xerrors.Errorf("pointer: %w", err)
}
resp.Optional = true
return resp, nil
}

// These are all the other types we need to support.
// time.Time, uuid, etc.
return "", "", xerrors.Errorf("unknown type: %s", ty.String())
return TypescriptType{}, xerrors.Errorf("unknown type: %s", ty.String())
}