257 lines
8.4 KiB
Go
257 lines
8.4 KiB
Go
package seatgen
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/token"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// A keyToken is one piece of a key form's pattern: a literal, or a named part (`<asker>`).
|
|
type keyToken struct {
|
|
part bool
|
|
text string
|
|
}
|
|
|
|
// keyTokens reads a key form's pattern into its pieces. Two parts with no literal between them are refused: a reader
|
|
// could not tell where the first ends.
|
|
func keyTokens(key string) ([]keyToken, error) {
|
|
var out []keyToken
|
|
rest := key
|
|
for rest != "" {
|
|
i := strings.Index(rest, "<")
|
|
if i < 0 {
|
|
out = append(out, keyToken{text: rest})
|
|
break
|
|
}
|
|
if i > 0 {
|
|
out = append(out, keyToken{text: rest[:i]})
|
|
}
|
|
j := strings.Index(rest[i:], ">")
|
|
if j < 0 {
|
|
return nil, fmt.Errorf("key %q: a part is not closed", key)
|
|
}
|
|
out = append(out, keyToken{part: true, text: rest[i+1 : i+j]})
|
|
rest = rest[i+j+1:]
|
|
}
|
|
for i := 1; i < len(out); i++ {
|
|
if out[i].part && out[i-1].part {
|
|
return nil, fmt.Errorf("key %q: two parts with no literal between them", key)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func keyParts(ts []keyToken) []string {
|
|
var out []string
|
|
for _, t := range ts {
|
|
if t.part {
|
|
out = append(out, t.text)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// formIdent is the name a key form's functions and value type are built from: the state's name, then "By" and the
|
|
// form's parts and literals in order (asks, `<asker>.<id>` → AsksByAskerID).
|
|
func formIdent(state, key string, ts []keyToken) string {
|
|
var b strings.Builder
|
|
b.WriteString(ident(state) + "By")
|
|
for _, t := range ts {
|
|
b.WriteString(ident(t.text))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
var reservedWords = map[string]bool{"default": true, "new": true, "delete": true, "class": true, "function": true, "var": true, "let": true, "const": true, "in": true, "of": true}
|
|
|
|
// paramName is a part's name as a function parameter: local-name → localName.
|
|
func paramName(part string) string {
|
|
n := ident(part)
|
|
n = strings.ToLower(n[:1]) + n[1:]
|
|
if strings.HasPrefix(ident(part), "ID") {
|
|
n = "id" + ident(part)[2:]
|
|
}
|
|
if token.IsKeyword(n) || reservedWords[n] {
|
|
n += "Part"
|
|
}
|
|
return n
|
|
}
|
|
|
|
// stateValueName is the Go and TypeScript type of one value of a state under a key form.
|
|
func stateValueName(state string, kf seatdef.KeyForm) (string, error) {
|
|
ts, err := keyTokens(kf.Key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return formIdent(state, kf.Key, ts) + "Value", nil
|
|
}
|
|
|
|
// stateFormFor is the state and key form a fixture's `state:<name>/<key>` names.
|
|
func stateFormFor(d seatdef.Definition, name string) (string, bool) {
|
|
state, key, _ := strings.Cut(name, "/")
|
|
for _, s := range d.States {
|
|
if s.Name != state {
|
|
continue
|
|
}
|
|
for _, kf := range s.KeyForms {
|
|
if kf.Key == key {
|
|
n, err := stateValueName(s.Name, kf)
|
|
return n, err == nil
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// goStates writes a seat's states: a constant for each local name, and for each key form its builder, its parser and
|
|
// the strict reader of a value (a field the type does not know is refused).
|
|
func goStates(b *bytes.Buffer, states []seatdef.State) error {
|
|
names := make([]string, len(states))
|
|
for i, s := range states {
|
|
names[i] = s.Name
|
|
}
|
|
writeNames(b, "State", names)
|
|
for _, s := range states {
|
|
for _, kf := range s.KeyForms {
|
|
ts, err := keyTokens(kf.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("state %s: %w", s.Name, err)
|
|
}
|
|
fi := formIdent(s.Name, kf.Key, ts)
|
|
params := keyParts(ts)
|
|
var plist, expr []string
|
|
for _, t := range ts {
|
|
if t.part {
|
|
expr = append(expr, paramName(t.text))
|
|
} else {
|
|
expr = append(expr, fmt.Sprintf("%q", t.text))
|
|
}
|
|
}
|
|
for _, p := range params {
|
|
plist = append(plist, paramName(p))
|
|
}
|
|
sig := ""
|
|
if len(plist) > 0 {
|
|
sig = strings.Join(plist, ", ") + " string"
|
|
}
|
|
fmt.Fprintf(b, "// %sKey is the key of one value of the state %s under the form %q.\nfunc %sKey(%s) string { return %s }\n\n",
|
|
fi, s.Name, kf.Key, fi, sig, strings.Join(expr, " + "))
|
|
rets := make([]string, 0, len(plist)+1)
|
|
zero := make([]string, 0, len(plist)+1)
|
|
for range plist {
|
|
rets = append(rets, "string")
|
|
zero = append(zero, `""`)
|
|
}
|
|
rets = append(rets, "bool")
|
|
zero = append(zero, "false")
|
|
fmt.Fprintf(b, "// Parse%sKey reads the parts of a key of the form %q, in the order of the form; the last result is false for a key of another form.\nfunc Parse%sKey(key string) (%s) {\n",
|
|
fi, kf.Key, fi, strings.Join(rets, ", "))
|
|
fail := "return " + strings.Join(zero, ", ")
|
|
b.WriteString("\trest := key\n")
|
|
var got []string
|
|
for i := 0; i < len(ts); {
|
|
t := ts[i]
|
|
switch {
|
|
case !t.part:
|
|
fmt.Fprintf(b, "\tif !strings.HasPrefix(rest, %q) {\n\t\t%s\n\t}\n\trest = rest[%d:]\n", t.text, fail, len(t.text))
|
|
i++
|
|
case i+1 < len(ts):
|
|
fmt.Fprintf(b, "\tv%d, after%d, found%d := strings.Cut(rest, %q)\n\tif !found%d || v%d == \"\" {\n\t\t%s\n\t}\n\trest = after%d\n",
|
|
i, i, i, ts[i+1].text, i, i, fail, i)
|
|
got = append(got, fmt.Sprintf("v%d", i))
|
|
i += 2
|
|
default:
|
|
fmt.Fprintf(b, "\tif rest == \"\" {\n\t\t%s\n\t}\n\tv%d := rest\n\trest = \"\"\n", fail, i)
|
|
got = append(got, fmt.Sprintf("v%d", i))
|
|
i++
|
|
}
|
|
}
|
|
b.WriteString("\tif rest != \"\" {\n\t\t" + fail + "\n\t}\n")
|
|
fmt.Fprintf(b, "\treturn %s\n}\n\n", strings.Join(append(got, "true"), ", "))
|
|
vn := fi + "Value"
|
|
fmt.Fprintf(b, "// Decode%s reads one value of the state %s under the form %q, refusing a field %s does not know and any text after the value.\n"+
|
|
"func Decode%s(raw []byte) (%s, error) {\n\tvar v %s\n\tdec := json.NewDecoder(bytes.NewReader(raw))\n\tdec.DisallowUnknownFields()\n"+
|
|
"\tif err := dec.Decode(&v); err != nil {\n\t\treturn v, err\n\t}\n\tif dec.More() {\n\t\treturn v, errors.New(\"text after the value\")\n\t}\n\treturn v, nil\n}\n\n",
|
|
vn, s.Name, kf.Key, vn, vn, vn, vn)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// goStatesImports is the import block a seat's states need: the strict reader always, the parser's string functions
|
|
// where a key form has a literal or a part that is not last.
|
|
func goStatesImports(states []seatdef.State) string {
|
|
needStrings := false
|
|
for _, s := range states {
|
|
for _, kf := range s.KeyForms {
|
|
ts, _ := keyTokens(kf.Key)
|
|
for i, t := range ts {
|
|
if !t.part || i+1 < len(ts) {
|
|
needStrings = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out := "import (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n"
|
|
if needStrings {
|
|
out += "\t\"strings\"\n"
|
|
}
|
|
return out + ")\n\n"
|
|
}
|
|
|
|
// tsStates writes a seat's states for TypeScript: the local names, and for each key form its builder and parser (the
|
|
// value's type is declared with the seat's other types).
|
|
func tsStates(b *bytes.Buffer, states []seatdef.State) error {
|
|
names := make([]string, len(states))
|
|
for i, s := range states {
|
|
names[i] = s.Name
|
|
}
|
|
tsNames(b, "states", names)
|
|
for _, s := range states {
|
|
for _, kf := range s.KeyForms {
|
|
ts, err := keyTokens(kf.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("state %s: %w", s.Name, err)
|
|
}
|
|
fn := tsName(formIdent(s.Name, kf.Key, ts))
|
|
var args, expr, fields, got []string
|
|
for _, t := range ts {
|
|
if t.part {
|
|
p := paramName(t.text)
|
|
args = append(args, p+": string")
|
|
expr = append(expr, p)
|
|
fields = append(fields, p+": string")
|
|
got = append(got, p)
|
|
} else {
|
|
expr = append(expr, strconv.Quote(t.text))
|
|
}
|
|
}
|
|
fmt.Fprintf(b, "/** The key of one value of the state %s under the form %s. */\nexport function %sKey(%s): string {\n return %s;\n}\n\n",
|
|
s.Name, strconv.Quote(kf.Key), fn, strings.Join(args, ", "), strings.Join(expr, " + "))
|
|
fmt.Fprintf(b, "/** The parts of a key of the form %s, or undefined for a key of another form. */\nexport function parse%sKey(key: string): { %s } | undefined {\n let rest = key;\n",
|
|
strconv.Quote(kf.Key), ident(fn), strings.Join(fields, "; "))
|
|
for i := 0; i < len(ts); {
|
|
t := ts[i]
|
|
switch {
|
|
case !t.part:
|
|
fmt.Fprintf(b, " if (!rest.startsWith(%s)) return undefined;\n rest = rest.slice(%d);\n", strconv.Quote(t.text), len(t.text))
|
|
i++
|
|
case i+1 < len(ts):
|
|
fmt.Fprintf(b, " const at%d = rest.indexOf(%s);\n if (at%d <= 0) return undefined;\n const %s = rest.slice(0, at%d);\n rest = rest.slice(at%d + %d);\n",
|
|
i, strconv.Quote(ts[i+1].text), i, paramName(t.text), i, i, len(ts[i+1].text))
|
|
i += 2
|
|
default:
|
|
fmt.Fprintf(b, " if (rest === \"\") return undefined;\n const %s = rest;\n rest = \"\";\n", paramName(t.text))
|
|
i++
|
|
}
|
|
}
|
|
fmt.Fprintf(b, " if (rest !== \"\") return undefined;\n return { %s };\n}\n\n", strings.Join(got, ", "))
|
|
}
|
|
}
|
|
return nil
|
|
}
|