/
processing.go
213 lines (201 loc) · 6.15 KB
/
processing.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package routemap
import (
"fmt"
"path"
"regexp"
"sort"
toml "github.com/pelletier/go-toml"
)
var (
// view '_ref' must be strictly lower, kebab-case string
refRegex = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$")
// TODO: consider making the values regexp for validation purposes
knownKeys = map[string]bool{
"_ref": true,
"_uri": true,
"_entrypoint": true,
"_default": true,
"_doc": true,
"_path": true,
"_template": true,
"_handler": true,
"_method": true,
"_fragment": true,
"_partial": true,
"_merge": true,
"_includes": true,
}
)
// TemplateBlock is a named child template slot within in a
// go template. Views is a list of RouteViews directly defined for that
// slot
type TemplateBlock struct {
Name string
Views []RouteView
}
// RouteView is a handler + template pair corresponding to a single
// partial, it may contain a number of slots available for extention
// by sub views
type RouteView struct {
Ref string `toml:"_ref"`
Default bool `toml:"_default"`
Doc string `toml:"_doc"`
Path string `toml:"_path"`
Template string `toml:"_template"`
Handler string `toml:"_handler"`
Method string `toml:"_method"`
Fragment bool `toml:"_fragment"`
Partial bool `toml:"_partial"`
Merge string `toml:"_merge"`
Includes []string `toml:"_includes"`
Blocks []TemplateBlock
Block string
}
// PageRoutes is the top level view for a site page, it includes
// a URI and golang package namespace
type PageRoutes struct {
RouteView
EntryPoint string `toml:"_entrypoint"`
}
// Missing is a missing value in the TOML file tree, it pairs the route view data
// with the position reference in the source TOML file
type Missing struct {
Ref string
Position toml.Position
InsertContent string
}
// ProcessRoutemap will unmarshal and validate a TOML routemap.
// This will keep track of missing template and handler values.
func ProcessRoutemap(tree *toml.Tree, templatePath string, bindResources bool) (routes PageRoutes, templates []Missing, handlers []Missing, err error) {
parser := routeParser{
usedRefs: make(map[string]toml.Position),
templateBase: templatePath,
bindResources: bindResources,
}
routes.RouteView, err = parser.unmarshalView(tree)
if err != nil {
return
}
if uri, ok := tree.Get("_entrypoint").(string); ok {
routes.EntryPoint = uri
} else if uri, ok := tree.Get("_uri").(string); ok {
// legacy routemaps use '_uri' for this purpose
routes.EntryPoint = uri
}
templates = parser.missingTemplates
handlers = parser.missingHandlers
return
}
// parser state during unmarshal/validate process
type routeParser struct {
usedRefs map[string]toml.Position
missingTemplates []Missing
missingHandlers []Missing
blockPath []string
templateBase string
bindResources bool
}
// unmarshalView will parse fields, validate and descend to unmarshal sub views routes
func (parser *routeParser) unmarshalView(tree *toml.Tree) (view RouteView, err error) {
// Popupate struct with known '_*' properties
err = tree.Unmarshal(&view)
if err != nil {
pos := tree.Position()
err = fmt.Errorf(":%d:%d: unmarshal error, %s", pos.Line, pos.Col, err)
return
}
// validate reference value and check for uniqueness
if !refRegex.MatchString(view.Ref) {
pos := tree.Position()
err = fmt.Errorf(
":%d:%d: Unknown or invalid _ref '%s', references be all lowercase joined by a dash '-'",
pos.Line, pos.Col, view.Ref,
)
return
}
if usePos, ok := parser.usedRefs[view.Ref]; ok {
pos := tree.Position()
err = fmt.Errorf(
":%d:%d: duplicate _ref '%s', already used in routemap file at line %d, column %d",
pos.Line, pos.Col, view.Ref, usePos.Line, usePos.Col,
)
return
} else {
// stash the location in the input file where this reference name appears
parser.usedRefs[view.Ref] = tree.GetPosition("_ref")
}
// fill template field if missing
if view.Template == "" {
view.Template = path.Join(append(append([]string{parser.templateBase}, parser.blockPath...), view.Ref+".html.tmpl")...)
parser.missingTemplates = append(parser.missingTemplates, Missing{
Position: tree.GetPosition("_ref"),
Ref: view.Ref,
InsertContent: view.Template,
})
}
// fill handler field if missing
if view.Handler == "" {
if parser.bindResources {
view.Handler = fmt.Sprintf("hlp.BindEnv(bindResources(%sHandler))", kebabToCamel(view.Ref))
} else {
view.Handler = fmt.Sprintf("hlp.BindEnv(%sHandler)", kebabToCamel(view.Ref))
}
parser.missingHandlers = append(parser.missingHandlers, Missing{
Position: tree.GetPosition("_ref"),
Ref: view.Ref,
InsertContent: view.Handler,
})
}
view.Block = safeLast(parser.blockPath)
// Now descend into sub view blocks by scanning for non-underscore keys
keys := tree.Keys()
sort.Strings(keys) // make output stable (we don't care about the original order)
currentBlockPath := parser.blockPath
for _, key := range keys {
if knownKeys[key] {
continue
}
pos := tree.GetPositionPath([]string{key})
// this is a block name, validate format
if !refRegex.MatchString(key) {
err = fmt.Errorf(
":%d:%d: Unknown or invalid key '%s', block names must be all lowercase joined by a dash '-'",
pos.Line, pos.Col, key,
)
return
}
parser.blockPath = append(currentBlockPath, key)
block := TemplateBlock{
Name: key,
}
val := tree.GetArray(key)
if subtrees, ok := val.([]*toml.Tree); ok {
if len(subtrees) == 1 {
if len(subtrees[0].Keys()) == 0 {
// This is an array with a single empty object `[{}]`
// For our purposes, treat this as equivalent to an empty array
goto APPEND_BLOCK
}
}
for _, sTree := range subtrees {
var sView RouteView
// recursive call
sView, err = parser.unmarshalView(sTree)
if err != nil {
return
}
block.Views = append(block.Views, sView)
}
} else {
err = fmt.Errorf(
":%d:%d: invalid value for key '%s', expecting an array of tables, got %#v",
pos.Line, pos.Col, key, val,
)
return
}
APPEND_BLOCK:
view.Blocks = append(view.Blocks, block)
}
parser.blockPath = currentBlockPath
return
}