Skip to content

Commit 3b11dc3

Browse files
committed
generate separate file
1 parent 733a17e commit 3b11dc3

File tree

9 files changed

+510
-195
lines changed

9 files changed

+510
-195
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ coderd/database/generate: fmt/sql coderd/database/dump.sql $(wildcard coderd/dat
2020
.PHONY: coderd/database/generate
2121

2222
apitypings/generate: site/src/api/types.ts
23-
go run scripts/apitypings/main.go > site/src/api/types.ts
23+
go run scripts/apitypings/main.go > site/src/api/types-generated.ts
2424
cd site && yarn run format:types
2525
.PHONY: coderts/generate
2626

scripts/apitypings/main.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/parser"
7+
"go/token"
8+
"log"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
13+
"golang.org/x/xerrors"
14+
)
15+
16+
const (
17+
baseDir = "./codersdk"
18+
)
19+
20+
func main() {
21+
err := run()
22+
if err != nil {
23+
log.Fatal(err)
24+
}
25+
}
26+
27+
func run() error {
28+
var (
29+
astFiles []*ast.File
30+
enums = make(map[string]string)
31+
)
32+
fset := token.NewFileSet()
33+
entries, err := os.ReadDir(baseDir)
34+
if err != nil {
35+
return xerrors.Errorf("reading dir %s: %w", baseDir, err)
36+
}
37+
38+
// loop each file in directory
39+
for _, entry := range entries {
40+
astFile, err := parser.ParseFile(fset, filepath.Join(baseDir, entry.Name()), nil, 0)
41+
if err != nil {
42+
return xerrors.Errorf("parsing file %s: %w", filepath.Join(baseDir, entry.Name()), err)
43+
}
44+
45+
astFiles = append(astFiles, astFile)
46+
}
47+
48+
loopSpecs(astFiles, func(spec ast.Spec) {
49+
pos := fset.Position(spec.Pos())
50+
switch s := spec.(type) {
51+
// TypeSpec case for structs and type alias
52+
case *ast.TypeSpec:
53+
out, err := handleTypeSpec(s, pos, enums)
54+
if err != nil {
55+
break
56+
}
57+
58+
_, _ = fmt.Printf(out)
59+
}
60+
})
61+
62+
loopSpecs(astFiles, func(spec ast.Spec) {
63+
switch s := spec.(type) {
64+
// ValueSpec case for const "enums"
65+
case *ast.ValueSpec:
66+
handleValueSpec(s, enums)
67+
}
68+
})
69+
70+
for _, v := range enums {
71+
_, _ = fmt.Printf("%s\n", v)
72+
}
73+
74+
return nil
75+
}
76+
77+
func loopSpecs(astFiles []*ast.File, fn func(spec ast.Spec)) {
78+
for _, astFile := range astFiles {
79+
// loop each declaration in file
80+
for _, node := range astFile.Decls {
81+
genDecl, ok := node.(*ast.GenDecl)
82+
if !ok {
83+
continue
84+
}
85+
for _, spec := range genDecl.Specs {
86+
fn(spec)
87+
}
88+
}
89+
}
90+
}
91+
92+
func handleTypeSpec(typeSpec *ast.TypeSpec, pos token.Position, enums map[string]string) (string, error) {
93+
jsonFields := 0
94+
s := fmt.Sprintf("// From %s.\n", pos.String())
95+
switch t := typeSpec.Type.(type) {
96+
// Struct declaration
97+
case *ast.StructType:
98+
s = fmt.Sprintf("%sexport interface %s {\n", s, typeSpec.Name.Name)
99+
for _, field := range t.Fields.List {
100+
i, optional, err := getIdent(field.Type)
101+
if err != nil {
102+
continue
103+
}
104+
105+
fieldType := toTsType(i.Name)
106+
if fieldType == "" {
107+
continue
108+
}
109+
110+
fieldName := toJSONField(field)
111+
if fieldName == "" {
112+
continue
113+
}
114+
115+
s = fmt.Sprintf("%s %s%s: %s\n", s, fieldName, optional, fieldType)
116+
jsonFields++
117+
}
118+
119+
// Do not print struct if it has no json fields
120+
if jsonFields == 0 {
121+
return "", xerrors.New("no json fields")
122+
}
123+
124+
return fmt.Sprintf("%s}\n\n", s), nil
125+
// Type alias declaration
126+
case *ast.Ident:
127+
// save type declaration to map of types
128+
// later we come back and add union types to this declaration
129+
enums[typeSpec.Name.Name] = fmt.Sprintf("%stype %s = \n", s, typeSpec.Name.Name)
130+
return "", xerrors.New("enums are not printed at this stage")
131+
default:
132+
return "", xerrors.New("not struct or alias")
133+
}
134+
}
135+
136+
func handleValueSpec(valueSpec *ast.ValueSpec, enums map[string]string) {
137+
valueValue := ""
138+
i, ok := valueSpec.Type.(*ast.Ident)
139+
if !ok {
140+
return
141+
}
142+
valueType := i.Name
143+
144+
for _, value := range valueSpec.Values {
145+
bl, ok := value.(*ast.BasicLit)
146+
if !ok {
147+
return
148+
}
149+
valueValue = bl.Value
150+
break
151+
}
152+
153+
enums[valueType] = fmt.Sprintf("%s | %s\n", enums[valueType], valueValue)
154+
}
155+
156+
func getIdent(e ast.Expr) (*ast.Ident, string, error) {
157+
switch t := e.(type) {
158+
case *ast.Ident:
159+
return t, "", nil
160+
case *ast.StarExpr:
161+
i, ok := t.X.(*ast.Ident)
162+
if !ok {
163+
return nil, "", xerrors.New("failed to cast star expr to indent")
164+
}
165+
return i, "?", nil
166+
default:
167+
return nil, "", xerrors.New("unknown expr type")
168+
}
169+
}
170+
171+
func toTsType(fieldType string) string {
172+
switch fieldType {
173+
case "bool":
174+
return "boolean"
175+
case "uint64", "uint32", "float64":
176+
return "number"
177+
}
178+
179+
return fieldType
180+
}
181+
182+
func toJSONField(field *ast.Field) string {
183+
if field.Tag != nil && field.Tag.Value != "" {
184+
fieldName := strings.Trim(field.Tag.Value, "`")
185+
for _, pair := range strings.Split(fieldName, " ") {
186+
if strings.Contains(pair, `json:`) {
187+
fieldName := strings.TrimPrefix(pair, `json:`)
188+
fieldName = strings.Trim(fieldName, `"`)
189+
fieldName = strings.Split(fieldName, ",")[0]
190+
191+
return fieldName
192+
}
193+
}
194+
}
195+
196+
return ""
197+
}

site/src/api/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from "axios"
22
import { getApiKey, login, logout } from "."
3-
import { APIKeyResponse, LoginResponse } from "./types"
3+
import { GenerateAPIKeyResponse, LoginWithPasswordResponse } from "./types"
44

55
// Mock the axios module so that no real network requests are made, but rather
66
// we swap in a resolved or rejected value
@@ -10,9 +10,9 @@ jest.mock("axios")
1010

1111
describe("api.ts", () => {
1212
describe("login", () => {
13-
it("should return LoginResponse", async () => {
13+
it("should return LoginWithPasswordResponse", async () => {
1414
// given
15-
const loginResponse: LoginResponse = {
15+
const loginResponse: LoginWithPasswordResponse = {
1616
session_token: "abc_123_test",
1717
}
1818
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
@@ -87,7 +87,7 @@ describe("api.ts", () => {
8787
describe("getApiKey", () => {
8888
it("should return APIKeyResponse", async () => {
8989
// given
90-
const apiKeyResponse: APIKeyResponse = {
90+
const apiKeyResponse: GenerateAPIKeyResponse = {
9191
key: "abc_123_test",
9292
}
9393
const axiosMockPost = jest.fn().mockImplementationOnce(() => {

site/src/api/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ export namespace Workspace {
4343
}
4444
}
4545

46-
export const login = async (email: string, password: string): Promise<Types.LoginResponse> => {
46+
export const login = async (email: string, password: string): Promise<Types.LoginWithPasswordResponse> => {
4747
const payload = JSON.stringify({
4848
email,
4949
password,
5050
})
5151

52-
const response = await axios.post<Types.LoginResponse>("/api/v2/users/login", payload, {
52+
const response = await axios.post<Types.LoginWithPasswordResponse>("/api/v2/users/login", payload, {
5353
headers: { ...CONTENT_TYPE_JSON },
5454
})
5555

@@ -60,8 +60,8 @@ export const logout = async (): Promise<void> => {
6060
await axios.post("/api/v2/users/logout")
6161
}
6262

63-
export const getUser = async (): Promise<Types.UserResponse> => {
64-
const response = await axios.get<Types.UserResponse>("/api/v2/users/me")
63+
export const getUser = async (): Promise<Types.User> => {
64+
const response = await axios.get<Types.User>("/api/v2/users/me")
6565
return response.data
6666
}
6767

@@ -71,7 +71,7 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
7171
}
7272

7373
export const getUsers = async (): Promise<Types.PagedUsers> => {
74-
// const response = await axios.get<Types.UserResponse[]>("/api/v2/users")
74+
// const response = await axios.get<Types.User[]>("/api/v2/users")
7575
// return response.data
7676
return Promise.resolve({
7777
page: [MockUser, MockUser2],
@@ -104,7 +104,7 @@ export const putWorkspaceAutostop = async (
104104
})
105105
}
106106

107-
export const updateProfile = async (userId: string, data: Types.UpdateProfileRequest): Promise<Types.UserResponse> => {
107+
export const updateProfile = async (userId: string, data: Types.UpdateProfileRequest): Promise<Types.User> => {
108108
const response = await axios.put(`/api/v2/users/${userId}/profile`, data)
109109
return response.data
110110
}

0 commit comments

Comments
 (0)