-
Notifications
You must be signed in to change notification settings - Fork 0
/
param_parser.go
201 lines (174 loc) · 5.43 KB
/
param_parser.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
package router
import (
"fmt"
"strconv"
"strings"
)
type ParamTmpl struct {
// id
Name string
// int range(1,5)!fail # fail fails on int parse or on range, it will work reclusive
Expression string
// fail
FailStatusCode int
Macro MacroTmpl
}
type MacroTmpl struct {
// int
Name string
// Macro will allow more than one funcs.
// []*MacroFuncs{ {Name: range, Params: []string{1,5}}}
Funcs []MacroFuncTmpl
}
type MacroFuncTmpl struct {
// range
Name string
// 1,5
Params []string
}
const (
ParamNameSeperator = ':'
FuncSeperator = ' '
FuncStart = '('
FuncEnd = ')'
FuncParamSeperator = ','
FailSeparator = '!'
)
const DefaultFailStatusCode = 404
type CursorState int
func (cs *CursorState) Is(s CursorState) bool {
c := *cs
if c == s {
switch s {
case CursorStateNone:
*cs = CursorStateStarted
case CursorStateStarted:
*cs = CursorStatePending
case CursorStatePending:
*cs = CursorStateRecording
case CursorStateRecording:
*cs = CursorStateStarted
}
return true
}
return false
}
// we use that to set a value of each "state".
// because macro's function's arguments can accepts space and regex with any character
const (
// no : is found yet
CursorStateNone CursorState = iota
// macro name and first expression guess parsed
// and we' ready to walk forward
CursorStateStarted
// after space, waiting for a macro func begin or !+fail_status_code
CursorStatePending
// inside the macro func, after the opening parenthesis
CursorStateRecording
)
// Parse i.e:
// id:int range(1,5) otherFunc(3) !404
//
// id = param name | can front-end end here but the parser should add :any
// int = marco | can end here
//
// range = marco's funcs(range)
// 1,5 = range's func.params | can end here
// +
// otherFunc = marco's funcs(otherFunc)
// 3 = otherFunc's func.params | can end here
//
// 404 = fail http custom error status code -> handler , will fail rescuslive
func ParseParam(source string) (*ParamTmpl, error) {
// first, do a simple check if that's 'valid'
sepIndex := strings.IndexRune(source, ParamNameSeperator)
if sepIndex <= 0 {
// if not found or
// if starts with :
return nil, fmt.Errorf("invalid source '%s', separator should be after the parameter name", source)
}
t := new(ParamTmpl)
// id:int range(1,5)
// id:int min(1) max(5)
// id:int range(1,5)!404 or !404, space doesn't matters on fail error code.
// cursor := 0
state := CursorStateNone
cursor := 0
for i := 0; i < len(source); i++ {
// TODO: find a better way instead of introducing variables like waitForFunc, insideFunc,
// one way is to move the functions with the reverse order but this can fix the problem for now
// later it will introduce new bugs, we can find a better static way to check these things, tomorrow.
// :int ...
if source[i] == ParamNameSeperator && state.Is(CursorStateNone) {
if i+1 >= len(source) {
return nil, fmt.Errorf("missing marco or raw expression after seperator, on source '%s'", source)
}
// id: , take the left, skip the : and continue
t.Name = source[0:i]
// set the expression, after the i, i.e:
// int range(1,5)
t.Expression = source[i+1:]
// set the macro's name to the full expression
// because we don't know if the user has put functions
// and we follow the < left 'pattern'
// (I don't know if that's valid but that is what
// I think to do and is working).
t.Macro = MacroTmpl{Name: t.Expression}
cursor = i + 1
continue
}
if source[i] == FuncSeperator && state.Is(CursorStateStarted) {
// take the left part: int if it's the first
// space after the param name
if t.Macro.Name == t.Expression {
t.Macro.Name = source[cursor:i]
} // else we have one or more functions, skip.
cursor = i + 1
continue
}
// if not inside a func body
// the cursor is a point which can receive a func
// starts with (
if source[i] == FuncStart && state.Is(CursorStatePending) {
// take the left part: range
funcName := source[cursor:i]
t.Macro.Funcs = append(t.Macro.Funcs, MacroFuncTmpl{Name: funcName})
cursor = i + 1
continue
}
// 1,5)
// we are inside func and )
if source[i] == FuncEnd && state.Is(CursorStateRecording) {
// check if we have end parenthesis but not start
if len(t.Macro.Funcs) == 0 {
return nil, fmt.Errorf("missing start macro's '%s' function, on source '%s'", t.Macro.Name, source)
}
// take the left part, between Start and End: 1,5
funcParamsStr := source[cursor:i]
funcParams := strings.SplitN(funcParamsStr, string(FuncParamSeperator), -1)
t.Macro.Funcs[len(t.Macro.Funcs)-1].Params = funcParams
cursor = i + 1
continue
}
if source[i] == FailSeparator && (state.Is(CursorStateStarted) || state.Is(CursorStatePending)) {
// it should be the last element
// so no problem if we set the cursor here and work with that
// we will not need that later.
cursor += 1
if i+1 >= len(source) {
return nil, fmt.Errorf("missing fail status code after '%q', on source '%s'", FailSeparator, source)
}
failCodeStr := source[cursor:] // should be the last
failCode, err := strconv.Atoi(failCodeStr)
if err != nil {
return nil, fmt.Errorf("fail status code should be integer but got '%s', on source '%s'", failCodeStr, source)
}
t.FailStatusCode = failCode
break
}
}
if t.FailStatusCode == 0 {
t.FailStatusCode = DefaultFailStatusCode
}
return t, nil
}