Skip to content

feat: generate typescript types from codersdk structs #1047

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 24 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 15 additions & 0 deletions .github/workflows/coder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Cache Node
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-

- name: Install node_modules
run: ./scripts/yarn_install.sh

- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ coderd/database/generate: fmt/sql coderd/database/dump.sql $(wildcard coderd/dat
coderd/database/generate.sh
.PHONY: coderd/database/generate

apitypings/generate: site/src/api/types.ts
go run scripts/apitypings/main.go > site/src/api/types-generated.ts
cd site && yarn run format:types
.PHONY: apitypings/generate

fmt/prettier:
@echo "--- prettier"
# Avoid writing files in CI to reduce file write activity
Expand Down Expand Up @@ -48,7 +53,7 @@ fmt/terraform: $(wildcard *.tf)
fmt: fmt/prettier fmt/sql fmt/terraform
.PHONY: fmt

gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto
gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto apitypings/generate
.PHONY: gen

install: bin
Expand Down
200 changes: 200 additions & 0 deletions scripts/apitypings/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package main

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"strings"

"golang.org/x/xerrors"
)

const (
baseDir = "./codersdk"
)

func main() {
err := run()
if err != nil {
log.Fatal(err)
}
}

func run() error {
var (
astFiles []*ast.File
enums = make(map[string]string)
)
fset := token.NewFileSet()
entries, err := os.ReadDir(baseDir)
if err != nil {
return xerrors.Errorf("reading dir %s: %w", baseDir, err)
}

// loop each file in directory
for _, entry := range entries {
astFile, err := parser.ParseFile(fset, filepath.Join(baseDir, entry.Name()), nil, 0)
if err != nil {
return xerrors.Errorf("parsing file %s: %w", filepath.Join(baseDir, entry.Name()), err)
}

astFiles = append(astFiles, astFile)
}

// TypeSpec case for structs and type alias
loopSpecs(astFiles, func(spec ast.Spec) {
pos := fset.Position(spec.Pos())
s, ok := spec.(*ast.TypeSpec)
if !ok {
return
}
out, err := handleTypeSpec(s, pos, enums)
if err != nil {
return
}

_, _ = fmt.Printf(out)
})

// ValueSpec case for loading type alias values into the enum map
loopSpecs(astFiles, func(spec ast.Spec) {
s, ok := spec.(*ast.ValueSpec)
if !ok {
return
}
handleValueSpec(s, enums)
})

// write each type alias declaration with possible values
for _, v := range enums {
_, _ = fmt.Printf("%s\n", v)
}

return nil
}

func loopSpecs(astFiles []*ast.File, fn func(spec ast.Spec)) {
for _, astFile := range astFiles {
// loop each declaration in file
for _, node := range astFile.Decls {
genDecl, ok := node.(*ast.GenDecl)
if !ok {
continue
}
for _, spec := range genDecl.Specs {
fn(spec)
}
}
}
}

func handleTypeSpec(typeSpec *ast.TypeSpec, pos token.Position, enums map[string]string) (string, error) {
jsonFields := 0
s := fmt.Sprintf("// From %s.\n", pos.String())
switch t := typeSpec.Type.(type) {
// Struct declaration
case *ast.StructType:
s = fmt.Sprintf("%sexport interface %s {\n", s, typeSpec.Name.Name)
for _, field := range t.Fields.List {
i, optional, err := getIdent(field.Type)
if err != nil {
continue
}

fieldType := toTsType(i.Name)
if fieldType == "" {
continue
}

fieldName := toJSONField(field)
if fieldName == "" {
continue
}

s = fmt.Sprintf("%s readonly %s%s: %s\n", s, fieldName, optional, fieldType)
jsonFields++
}

// Do not print struct if it has no json fields
if jsonFields == 0 {
return "", xerrors.New("no json fields")
}

return fmt.Sprintf("%s}\n\n", s), nil
// Type alias declaration
case *ast.Ident:
// save type declaration to map of types
// later we come back and add union types to this declaration
enums[typeSpec.Name.Name] = fmt.Sprintf("%sexport type %s = \n", s, typeSpec.Name.Name)
return "", xerrors.New("enums are not printed at this stage")
default:
return "", xerrors.New("not struct or alias")
}
}

func handleValueSpec(valueSpec *ast.ValueSpec, enums map[string]string) {
valueValue := ""
i, ok := valueSpec.Type.(*ast.Ident)
if !ok {
return
}
valueType := i.Name

for _, value := range valueSpec.Values {
bl, ok := value.(*ast.BasicLit)
if !ok {
return
}
valueValue = bl.Value
break
}

enums[valueType] = fmt.Sprintf("%s | %s\n", enums[valueType], valueValue)
}

func getIdent(e ast.Expr) (*ast.Ident, string, error) {
switch t := e.(type) {
case *ast.Ident:
return t, "", nil
case *ast.StarExpr:
i, ok := t.X.(*ast.Ident)
if !ok {
return nil, "", xerrors.New("failed to cast star expr to indent")
}
return i, "?", nil
default:
return nil, "", xerrors.New("unknown expr type")
}
}

func toTsType(fieldType string) string {
switch fieldType {
case "bool":
return "boolean"
case "uint64", "uint32", "float64":
return "number"
}

return fieldType
}

func toJSONField(field *ast.Field) string {
if field.Tag != nil && field.Tag.Value != "" {
fieldName := strings.Trim(field.Tag.Value, "`")
for _, pair := range strings.Split(fieldName, " ") {
if strings.Contains(pair, `json:`) {
fieldName := strings.TrimPrefix(pair, `json:`)
fieldName = strings.Trim(fieldName, `"`)
fieldName = strings.Split(fieldName, ",")[0]

return fieldName
}
}
}

return ""
}
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"chromatic": "chromatic",
"dev": "webpack-dev-server --config=webpack.dev.ts",
"format:check": "prettier --check '**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'",
"format:types": "prettier --write 'src/api/types-generated.ts'",
"format:write": "prettier --write '**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'",
"lint": "jest --selectProjects lint",
"lint:fix": "FIX=true yarn lint",
Expand Down
Loading