Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
256 lines
7.9 KiB
Go
256 lines
7.9 KiB
Go
//go:build js && wasm
|
|
|
|
// Command lwconf is the configuration engine as WebAssembly: it sets
|
|
// globalThis.lwconf for lwconf.js, calls taking strings and returning JSON,
|
|
// {"value"} or {"error"} with its path, one in a pipeline placed on its node
|
|
// and key, any other a preset's or flag's key. Pipelines are compose's JSON
|
|
// form; dirs answers config.IsDir by path; an error's ask is a path to answer.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"syscall/js"
|
|
|
|
"github.com/lixenwraith/logwisp/internal/compose"
|
|
"github.com/lixenwraith/logwisp/internal/config"
|
|
)
|
|
|
|
// calls are lwconf's, by name, with the string arguments each takes
|
|
var calls = map[string]struct {
|
|
args []string
|
|
call func(args []string) (any, error)
|
|
}{
|
|
"schema": {nil, func([]string) (any, error) { return compose.NewSchema(), nil }},
|
|
"preset": {[]string{"name", "values", "dirs"}, func(a []string) (any, error) {
|
|
values, err := presetValues(a[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isDir, err := answers(a[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pipelines(compose.FromPreset(a[0], values, isDir))
|
|
}},
|
|
"parse": {[]string{"text", "form", "dirs"}, func(a []string) (any, error) {
|
|
f, err := form(a[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isDir, err := answers(a[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pipelines(f.Read(a[0], isDir))
|
|
}},
|
|
"validate": {[]string{"pipelines"}, func(a []string) (any, error) {
|
|
c, err := compose.FromJSON([]byte(a[0]))
|
|
if c == nil {
|
|
return nil, err
|
|
}
|
|
value, err := pipelines(c, nil) // a value of the wrong kind is a problem Validate names
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
problems := []problem{}
|
|
for _, p := range compose.Problems(c.Validate()) {
|
|
problems = append(problems, place(p))
|
|
}
|
|
return map[string]any{"pipelines": value, "problems": problems}, nil
|
|
}},
|
|
"emit": {[]string{"pipelines", "form"}, func(a []string) (any, error) {
|
|
c, err := compose.FromJSON([]byte(a[0]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := form(a[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f.Write(c)
|
|
}},
|
|
"edit": {[]string{"pipelines", "op"}, func(a []string) (any, error) {
|
|
c, err := compose.FromJSON([]byte(a[0]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var o op
|
|
in := json.NewDecoder(bytes.NewReader([]byte(a[1])))
|
|
in.DisallowUnknownFields()
|
|
if err := in.Decode(&o); err != nil {
|
|
return nil, fmt.Errorf("op: %w", err)
|
|
}
|
|
edit, ok := edits[o.Op]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no op %q (valid: %s)", o.Op, strings.Join(slices.Sorted(maps.Keys(edits)), ", "))
|
|
}
|
|
n, err := edit(c, o)
|
|
if _, ok := errors.AsType[*config.KeyError](err); ok && o.Node.Role != "" {
|
|
err = config.At(o.Node.Path(o.Pipeline), err) // a key set or unset in the node
|
|
}
|
|
value, err := pipelines(c, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"pipelines": value, "node": n}, nil
|
|
}},
|
|
}
|
|
|
|
// op is one edit: the compose method Op names, and its arguments
|
|
type op struct {
|
|
Op string `json:"op"`
|
|
Pipeline int `json:"pipeline"`
|
|
Node compose.Node `json:"node"`
|
|
Role string `json:"role"`
|
|
Type string `json:"type"`
|
|
Key string `json:"key"`
|
|
Values []string `json:"values"`
|
|
Name string `json:"name"`
|
|
From int `json:"from"`
|
|
To int `json:"to"`
|
|
}
|
|
|
|
// edits call compose's edit methods, each returning the node it leaves chosen
|
|
var edits = map[string]func(*compose.Composition, op) (compose.Node, error){
|
|
"add_pipeline": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return compose.Node{}, c.AddPipeline(o.Name)
|
|
},
|
|
"rename_pipeline": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return compose.Node{}, c.RenamePipeline(o.Pipeline, o.Name)
|
|
},
|
|
"remove_pipeline": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return compose.Node{}, c.RemovePipeline(o.Pipeline)
|
|
},
|
|
"add": func(c *compose.Composition, o op) (compose.Node, error) { return c.Add(o.Pipeline, o.Role, o.Type) },
|
|
"retype": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return c.Retype(o.Pipeline, o.Node, o.Type)
|
|
},
|
|
"remove": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return compose.Node{}, c.Remove(o.Pipeline, o.Node)
|
|
},
|
|
"move_filter": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return compose.Node{Role: "filters", Index: o.To}, c.MoveFilter(o.Pipeline, o.From, o.To)
|
|
},
|
|
"set": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return o.Node, c.Set(o.Pipeline, o.Node, o.Key, o.Values...)
|
|
},
|
|
"unset": func(c *compose.Composition, o op) (compose.Node, error) {
|
|
return o.Node, c.Unset(o.Pipeline, o.Node, o.Key)
|
|
},
|
|
}
|
|
|
|
// problem is where a problem, or a call's refusal, lies as far as the engine
|
|
// knows: its path, and the pipeline, node and key Locate reads from it; ask
|
|
// is a preset's path whose kind dirs must answer
|
|
type problem struct {
|
|
Error string `json:"error,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Pipeline *int `json:"pipeline,omitempty"`
|
|
Node *compose.Node `json:"node,omitempty"`
|
|
Key string `json:"key,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
Ask string `json:"ask,omitempty"`
|
|
}
|
|
|
|
func place(err error) problem {
|
|
p := problem{Text: err.Error()}
|
|
if ke, ok := errors.AsType[*config.KeyError](err); ok {
|
|
p.Path = ke.Key
|
|
}
|
|
if pk, ok := errors.AsType[*config.PathKindError](err); ok {
|
|
p.Ask = pk.Path
|
|
}
|
|
if pi, n, key, text, ok := compose.Locate(err); ok {
|
|
p.Pipeline, p.Key, p.Text = &pi, key, text
|
|
if n.Role != "" {
|
|
p.Node = &n
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// presetValues reads a preset's values: a string each, or a list key's
|
|
// strings, which the preset takes ',' between them
|
|
func presetValues(data string) (map[string]string, error) {
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(data), &raw); err != nil {
|
|
return nil, fmt.Errorf("values: %w", err)
|
|
}
|
|
values := map[string]string{}
|
|
for k, v := range raw {
|
|
var one string
|
|
var many []string
|
|
switch {
|
|
case json.Unmarshal(v, &one) == nil:
|
|
values[k] = one
|
|
case json.Unmarshal(v, &many) == nil:
|
|
values[k] = strings.Join(many, ",")
|
|
default:
|
|
return nil, &config.KeyError{Key: k, Err: errors.New("takes a string, or a list of strings")}
|
|
}
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// answers reads dirs: whether each path a preset names is a directory
|
|
func answers(dirs string) (config.IsDir, error) {
|
|
var m map[string]bool
|
|
if err := json.Unmarshal([]byte(dirs), &m); err != nil {
|
|
return nil, fmt.Errorf("dirs: %w", err)
|
|
}
|
|
return config.Answers(m), nil
|
|
}
|
|
|
|
func form(name string) (compose.Form, error) {
|
|
forms := compose.Forms()
|
|
if i := slices.IndexFunc(forms, func(f compose.Form) bool { return f.Name == name }); i >= 0 {
|
|
return forms[i], nil
|
|
}
|
|
return compose.Form{}, fmt.Errorf("no form %q", name)
|
|
}
|
|
|
|
// pipelines is a composition's value: its pipelines settled, ids given
|
|
func pipelines(c *compose.Composition, err error) (any, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := c.JSON()
|
|
return json.RawMessage(data), err
|
|
}
|
|
|
|
func main() {
|
|
api := map[string]any{}
|
|
for name := range calls {
|
|
api[name] = js.FuncOf(func(_ js.Value, args []js.Value) any { return answer(name, args) })
|
|
}
|
|
js.Global().Set("lwconf", js.ValueOf(api))
|
|
select {} // the page calls in until it closes
|
|
}
|
|
|
|
// answer is a call's JSON for the page: its value, or why it was refused
|
|
func answer(name string, args []js.Value) string {
|
|
c := calls[name]
|
|
value, err := any(nil), fmt.Errorf("usage: lwconf.%s(%s), each a string", name, strings.Join(c.args, ", "))
|
|
if len(args) == len(c.args) && !slices.ContainsFunc(args, func(v js.Value) bool { return v.Type() != js.TypeString }) {
|
|
strs := make([]string, len(args))
|
|
for i, arg := range args {
|
|
strs[i] = arg.String()
|
|
}
|
|
value, err = c.call(strs)
|
|
}
|
|
var out any = map[string]any{"value": value}
|
|
if err != nil {
|
|
p := place(err)
|
|
p.Error, p.Text = err.Error(), ""
|
|
out = p
|
|
}
|
|
data, _ := json.Marshal(out)
|
|
return string(data)
|
|
}
|