Skip to content

Commit 9922a77

Browse files
committed
improved format for query params in JSON + more typesafe URL parsing
includes: - validated integers via URL params - optional/required URL params (now empty string is distinct from unset) - not accepting unrecognized URL params
1 parent e1374b8 commit 9922a77

2 files changed

Lines changed: 53 additions & 64 deletions

File tree

codegen/codegentemplates/uiroutes.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99
1010
func RegisterUiRoutes(routes *mux.Router, uiHandler http.HandlerFunc) { {{range .Module.UiRoutes}}
11-
routes.HandleFunc("{{.PathWithoutQuery}}", uiHandler){{end}}
11+
routes.HandleFunc("{{.Path}}", uiHandler){{end}}
1212
}
1313
`
1414

@@ -23,31 +23,53 @@ export interface RouteHandlers { {{range .Module.UiRoutes}}
2323
}
2424
2525
{{range .Module.UiRoutes}}
26-
{{if .HasOpts}}export interface {{.TsOptsName}} { {{range .PathAndQueryOpts}}
27-
{{.}}: string;{{end}}
26+
{{if .HasOpts}}export interface {{.TsOptsName}} { {{range .PathPlaceholders}}
27+
{{.}}: string;{{end}}{{range .QueryParams}}
28+
{{.Key}}{{if .Type.Nullable}}?{{end}}: {{if eq .Type.NameRaw "integer"}}number{{else}}string{{end}};{{end}}
2829
}{{end}}
2930
3031
// {{.Path}}
3132
export function {{.Id}}Url({{if .HasOpts}}opts: {{.TsOptsName}}{{end}}): string {
32-
const query: queryParams = {}
33-
{{range .QueryPlaceholders}}
34-
if (opts.{{.}}) {
35-
query.{{.}} = opts.{{.}};
36-
} {{end}}
33+
const query: queryParams = {};
34+
{{range .QueryParams}}
35+
{{if .Type.Nullable}} if (opts.{{.Key}} !== undefined) {
36+
{{end}} query.{{.Key}} = opts.{{.Key}}{{if eq .Type.NameRaw "integer"}}.toString(){{end}};{{if .Type.Nullable}}
37+
}{{end}}
38+
{{end}}
3739
3840
return makeQueryParams(` + "`{{.TsPath}}`" + `, query);
3941
}
4042
41-
// @ts-ignore
4243
export function {{.Id}}Match(path: string, query: queryParams): {{if .HasOpts}}{{.TsOptsName}}{{else}}{}{{end}} | null {
4344
const matches = {{.PathReJavaScript}}.exec(path);
4445
if (matches == null) {
4546
return null;
4647
}
4748
49+
{{range .QueryParams}}{{if eq .Type.NameRaw "string"}}
50+
const {{.Key}}Par = query.{{.Key}};{{else}}
51+
let {{.Key}}Par: number | undefined;
52+
if (query{{.Key}} !== undefined) {
53+
// parseInt() accepts garbage after the number, and "+" accepts empty string
54+
if (query.{{.Key}} === '') {
55+
throw new Error("Invalid URL param: '{{.Key}}'; expecting integer, got empty string")
56+
}
57+
const parsed = +query.{{.Key}};
58+
if (isNaN(parsed)) {
59+
throw new Error("Invalid URL param: '{{.Key}}'; expecting integer")
60+
}
61+
{{.Key}}Par = parsed;
62+
}{{end}}{{if not .Type.Nullable}}
63+
if ({{.Key}}Par === undefined) {
64+
throw new Error("Required URL param '{{.Key}}' missing");
65+
} {{end}}
66+
{{end}}
67+
68+
assertNoUnrecognizedKeys(Object.keys(query), [{{range .QueryParams}}'{{.Key}}', {{end}}]);
69+
4870
return { {{range $idx, $key := .PathPlaceholders}}
49-
{{$key}}: matches[{{add $idx 1}}],{{end}}{{range .QueryPlaceholders}}
50-
{{.}}: query.{{.}} || '',{{end}}
71+
{{$key}}: matches[{{add $idx 1}}],{{end}}{{range .QueryParams}}
72+
{{.Key}}: {{.Key}}Par,{{end}}
5173
};
5274
}
5375
@@ -85,4 +107,11 @@ export function hasRouteFor(url: string): boolean {
85107
return false;
86108
}
87109
110+
function assertNoUnrecognizedKeys(gotKeys: string[], allowedKeys: string[]) {
111+
const unrecognizedKeys = gotKeys.filter(key => allowedKeys.indexOf(key) === -1);
112+
if (unrecognizedKeys.length > 0) {
113+
throw new Error("Unrecognized keys in URL params: "+unrecognizedKeys.join(', '));
114+
}
115+
}
116+
88117
`

codegen/uiroutes.go

Lines changed: 13 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package codegen
22

33
import (
4-
"net/url"
54
"strings"
65
)
76

87
type uiRouteSpec struct {
9-
Id string `json:"id"`
10-
Path string `json:"path"`
8+
Id string `json:"id"`
9+
Path string `json:"path"`
10+
QueryParams []struct {
11+
Key string `json:"key"`
12+
Type DatatypeDef `json:"type"`
13+
} `json:"query_params"`
1114
}
1215

1316
// need to uppercase b/c tslint complains about pascal case
@@ -16,74 +19,31 @@ func (u *uiRouteSpec) TsOptsName() string {
1619
}
1720

1821
func (u *uiRouteSpec) HasOpts() bool {
19-
return len(u.PathAndQueryOpts()) > 0
20-
}
21-
22-
// "/accounts/{id}?token={token}" => ["id", "token"]
23-
func (u *uiRouteSpec) PathAndQueryOpts() []string {
24-
return u.placeholders(u.Path)
22+
return (len(u.PathPlaceholders()) + len(u.QueryParams)) > 0
2523
}
2624

2725
// "/accounts/{id}?token={token}" => ["id"]
2826
func (u *uiRouteSpec) PathPlaceholders() []string {
29-
return u.placeholders(u.PathWithoutQuery())
30-
}
31-
32-
// "/accounts/{id}?token={token}" => ["token"]
33-
func (u *uiRouteSpec) QueryPlaceholders() []string {
34-
return u.placeholders(u.Query())
35-
}
36-
37-
func (u *uiRouteSpec) placeholders(pathOrQuery string) []string {
38-
placeholders := []string{}
39-
for _, match := range routePlaceholderParseRe.FindAllStringSubmatch(pathOrQuery, -1) {
40-
placeholders = append(placeholders, match[1])
27+
keys := []string{}
28+
for _, match := range routePlaceholderParseRe.FindAllStringSubmatch(u.Path, -1) {
29+
keys = append(keys, match[1])
4130
}
4231

43-
return placeholders
32+
return keys
4433
}
4534

4635
func (u *uiRouteSpec) PathReJavaScript() string {
4736
// "/account/{account}/import_otp_token" => "/account/([^/]+)/import_otp_token"
48-
reString := routePlaceholderParseRe.ReplaceAllStringFunc(u.PathWithoutQuery(), func(_ string) string {
37+
reString := routePlaceholderParseRe.ReplaceAllStringFunc(u.Path, func(_ string) string {
4938
return "([^/]+)"
5039
})
5140

5241
// "/^\/account\/([^\/]+)\/import_otp_token$/"
5342
return "/^" + strings.ReplaceAll(reString, "/", `\/`) + "$/"
5443
}
5544

56-
func (u *uiRouteSpec) PathWithoutQuery() string {
57-
// FIXME: trick to make it work with hashes
58-
if strings.HasPrefix(u.Path, "#") {
59-
parsedUrl, err := url.Parse(u.Path[1:])
60-
if err != nil {
61-
panic(err)
62-
}
63-
64-
return "#" + parsedUrl.Path
65-
} else {
66-
parsedUrl, err := url.Parse(u.Path)
67-
if err != nil {
68-
panic(err)
69-
}
70-
71-
return parsedUrl.Path
72-
}
73-
}
74-
75-
func (u *uiRouteSpec) Query() string {
76-
// FIXME: trick to make it work with hashes
77-
parsedUrl, err := url.Parse(strings.TrimPrefix(u.Path, "#"))
78-
if err != nil {
79-
panic(err)
80-
}
81-
82-
return parsedUrl.RawQuery
83-
}
84-
8545
func (u *uiRouteSpec) TsPath() string {
86-
return routePlaceholderParseRe.ReplaceAllStringFunc(u.PathWithoutQuery(), func(match string) string {
46+
return routePlaceholderParseRe.ReplaceAllStringFunc(u.Path, func(match string) string {
8747
// "{id}" => "id"
8848
placeholder := removeBraces(match)
8949

0 commit comments

Comments
 (0)