Files
logwisp/cmd/lwconf/main.go
T
lixen de87b9a008
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
v0.35.2 lwconf, doc and deploy update
2026-10-13 14:23:37 -04:00

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)
}