From ceac85dbc11cecb95c738049f2d4fd43c7d18828 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Mon, 5 Feb 2024 00:59:31 +0100 Subject: chore(gonf): first draft of the gonf lib with commands, files, packages and variables --- go.mod | 3 ++ gonf/commands.go | 95 +++++++++++++++++++++++++++++++++ gonf/files.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ gonf/gonf.go | 44 ++++++++++++++++ gonf/packages.go | 73 ++++++++++++++++++++++++++ gonf/promises.go | 50 ++++++++++++++++++ gonf/triggers.go | 11 ++++ gonf/utils.go | 9 ++++ gonf/values.go | 27 ++++++++++ gonf/variables.go | 57 ++++++++++++++++++++ 10 files changed, 522 insertions(+) create mode 100644 go.mod create mode 100644 gonf/commands.go create mode 100644 gonf/files.go create mode 100644 gonf/gonf.go create mode 100644 gonf/packages.go create mode 100644 gonf/promises.go create mode 100644 gonf/triggers.go create mode 100644 gonf/utils.go create mode 100644 gonf/values.go create mode 100644 gonf/variables.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3faf555 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.adyxax.org/adyxax/gonf/v2 + +go 1.21.5 diff --git a/gonf/commands.go b/gonf/commands.go new file mode 100644 index 0000000..b5a11ca --- /dev/null +++ b/gonf/commands.go @@ -0,0 +1,95 @@ +package gonf + +import ( + "bytes" + "log/slog" + "os/exec" +) + +// ----- Globals --------------------------------------------------------------- +var commands []*CommandPromise + +// ----- Init ------------------------------------------------------------------ +func init() { + commands = make([]*CommandPromise, 0) +} + +// ----- Public ---------------------------------------------------------------- +func Command(cmd string, args ...string) *CommandPromise { + return CommandWithEnv([]string{}, cmd, args...) +} + +func CommandWithEnv(env []string, cmd string, args ...string) *CommandPromise { + return &CommandPromise{ + args: args, + chain: nil, + cmd: cmd, + env: env, + err: nil, + Status: PROMISED, + } +} + +type CommandPromise struct { + args []string + chain []Promise + cmd string + env []string + err error + Status Status + Stdout bytes.Buffer + Stderr bytes.Buffer +} + +func (c *CommandPromise) IfRepaired(p ...Promise) Promise { + c.chain = p + return c +} + +func (c *CommandPromise) Promise() Promise { + commands = append(commands, c) + return c +} + +func (c *CommandPromise) Resolve() { + cmd := exec.Command(c.cmd, c.args...) + for _, e := range c.env { + cmd.Env = append(cmd.Environ(), e) + } + cmd.Stdout = &c.Stdout + cmd.Stderr = &c.Stderr + + if c.err = cmd.Run(); c.err != nil { + c.Status = BROKEN + slog.Error("command", "args", c.args, "cmd", c.cmd, "env", c.env, "err", c.err, "stdout", c.Stdout.String(), "stderr", c.Stderr.String(), "status", c.Status) + return + } + if c.Stdout.Len() == 0 && c.Stderr.Len() > 0 { + c.Status = BROKEN + slog.Error("command", "args", c.args, "cmd", c.cmd, "env", c.env, "stdout", c.Stdout.String(), "stderr", c.Stderr.String(), "status", c.Status) + return + } + c.Status = REPAIRED + slog.Info("command", "args", c.args, "cmd", c.cmd, "env", c.env, "stderr", c.Stderr.String(), "status", c.Status) + // TODO add a notion of repaired? + for _, p := range c.chain { + p.Resolve() + } +} + +// ----- Internal -------------------------------------------------------------- +func resolveCommands() (status Status) { + status = KEPT + for _, c := range commands { + if c.Status == PROMISED { + c.Resolve() + switch c.Status { + case BROKEN: + return BROKEN + case REPAIRED: + status = REPAIRED + } + } + } + return +} diff --git a/gonf/files.go b/gonf/files.go new file mode 100644 index 0000000..2401a25 --- /dev/null +++ b/gonf/files.go @@ -0,0 +1,153 @@ +package gonf + +import ( + "bytes" + "crypto/sha256" + "io" + "log/slog" + "net/url" + "os" + "text/template" +) + +// ----- Globals --------------------------------------------------------------- +var files []*FilePromise + +// ----- Init ------------------------------------------------------------------ +func init() { + files = make([]*FilePromise, 0) +} + +// ----- Public ---------------------------------------------------------------- +func File(filename string, contents []byte) *FilePromise { + return &FilePromise{ + chain: nil, + contents: contents, + err: nil, + filename: filename, + status: PROMISED, + templateFunctions: nil, + useTemplate: false, + } +} + +type FilePromise struct { + chain []Promise + contents []byte + err error + filename string + status Status + templateFunctions map[string]any + useTemplate bool +} + +func (f *FilePromise) IfRepaired(p ...Promise) Promise { + f.chain = p + return f +} + +func (f *FilePromise) Promise() Promise { + files = append(files, f) + return f +} + +func (f *FilePromise) Resolve() { + if f.useTemplate { + tpl := template.New(f.filename) + tpl.Option("missingkey=error") + tpl.Funcs(builtinTemplateFunctions) + tpl.Funcs(f.templateFunctions) + if ttpl, err := tpl.Parse(string(f.contents)); err != nil { + f.status = BROKEN + slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) + return + } else { + var buff bytes.Buffer + if err := ttpl.Execute(&buff, 0 /* TODO */); err != nil { + f.status = BROKEN + slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) + return + } + f.contents = buff.Bytes() + } + } + var sumFile []byte + sumFile, f.err = sha256sumOfFile(f.filename) + if f.err != nil { + f.status = BROKEN + return + } + sumContents := sha256sum(f.contents) + if !bytes.Equal(sumFile, sumContents) { + if f.err = writeFile(f.filename, f.contents); f.err != nil { + f.status = BROKEN + slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err) + return + } + f.status = REPAIRED + slog.Info("file", "filename", f.filename, "status", f.status) + for _, p := range f.chain { + p.Resolve() + } + return + } + f.status = KEPT + slog.Debug("file", "filename", f.filename, "status", f.status) +} + +func Template(filename string, contents []byte) *FilePromise { + f := File(filename, contents) + f.useTemplate = true + return f +} + +func TemplateWith(filename string, contents []byte, templateFunctions map[string]any) *FilePromise { + f := Template(filename, contents) + f.templateFunctions = templateFunctions + return f +} + +// ----- Internal -------------------------------------------------------------- +var builtinTemplateFunctions = map[string]any{ + "encodeURIQueryParameter": url.QueryEscape, + "var": getVariable, +} + +func resolveFiles() (status Status) { + status = KEPT + for _, f := range files { + if f.status == PROMISED { + f.Resolve() + switch f.status { + case BROKEN: + return BROKEN + case REPAIRED: + status = REPAIRED + } + } + } + return +} + +func sha256sumOfFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +func writeFile(filename string, contents []byte) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write(contents) + return err +} diff --git a/gonf/gonf.go b/gonf/gonf.go new file mode 100644 index 0000000..b5aec4f --- /dev/null +++ b/gonf/gonf.go @@ -0,0 +1,44 @@ +package gonf + +import ( + "log/slog" + "os" +) + +func EnableDebugLogs() { + h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + slog.SetDefault(slog.New(h)) +} + +func Resolve() (status Status) { + for { + // ----- Files ------------------------------------------------- + status = resolveFiles() + switch status { + case BROKEN: + return BROKEN + case REPAIRED: + continue + } + // ----- Packages ---------------------------------------------- + status = resolvePackages() + switch status { + case BROKEN: + return BROKEN + case REPAIRED: + packages_list_function() + continue + } + // ----- Commands ---------------------------------------------- + status = resolveCommands() + switch status { + case BROKEN: + return BROKEN + case REPAIRED: + continue + } + return + } +} diff --git a/gonf/packages.go b/gonf/packages.go new file mode 100644 index 0000000..7ff512d --- /dev/null +++ b/gonf/packages.go @@ -0,0 +1,73 @@ +package gonf + +// ----- Globals --------------------------------------------------------------- +var packages []*PackagePromise + +// packages management functions +var packages_install_function func([]string) Status +var packages_list_function func() +var packages_update_function *CommandPromise + +// ----- Init ------------------------------------------------------------------ +func init() { + packages = make([]*PackagePromise, 0) +} + +// ----- Public ---------------------------------------------------------------- +func SetPackagesConfiguration(install func([]string) Status, list func(), update *CommandPromise) { + packages_install_function = install + packages_list_function = list + packages_update_function = update +} + +func Package(names ...string) *PackagePromise { + return &PackagePromise{ + chain: nil, + err: nil, + names: names, + status: PROMISED, + } +} + +type PackagePromise struct { + chain []Promise + err error + names []string + status Status +} + +func (p *PackagePromise) IfRepaired(ps ...Promise) Promise { + p.chain = ps + return p +} + +func (p *PackagePromise) Promise() Promise { + packages = append(packages, p) + return p +} + +func (p *PackagePromise) Resolve() { + status := packages_install_function(p.names) + if status == REPAIRED { + for _, pp := range p.chain { + pp.Resolve() + } + } +} + +// ----- Internal -------------------------------------------------------------- +func resolvePackages() (status Status) { + status = KEPT + for _, c := range packages { + if c.status == PROMISED { + c.Resolve() + switch c.status { + case BROKEN: + return BROKEN + case REPAIRED: + status = REPAIRED + } + } + } + return +} diff --git a/gonf/promises.go b/gonf/promises.go new file mode 100644 index 0000000..326d500 --- /dev/null +++ b/gonf/promises.go @@ -0,0 +1,50 @@ +package gonf + +type Promise interface { + IfRepaired(...Promise) Promise + Promise() Promise + Resolve() +} + +//type Operation int +// +//const ( +// AND = iota +// OR +// NOT +//) +// +//func (o Operation) String() string { +// switch o { +// case AND: +// return "and" +// case OR: +// return "or" +// case NOT: +// return "not" +// } +// panic("unknown") +//} + +type Status int + +const ( + PROMISED = iota + BROKEN + KEPT + REPAIRED +) + +func (s Status) String() string { + switch s { + case PROMISED: + return "promised" + case BROKEN: + return "broken" + case KEPT: + return "kept" + case REPAIRED: + return "repaired" + } + panic("unknown") +} diff --git a/gonf/triggers.go b/gonf/triggers.go new file mode 100644 index 0000000..370ab1c --- /dev/null +++ b/gonf/triggers.go @@ -0,0 +1,11 @@ +package gonf + +type simpleTrigger struct { + fact string + status Status +} + +//type compositeTrigger struct { +// triggers []simpleTrigger +// operation Operation +//} diff --git a/gonf/utils.go b/gonf/utils.go new file mode 100644 index 0000000..7031062 --- /dev/null +++ b/gonf/utils.go @@ -0,0 +1,9 @@ +package gonf + +import "crypto/sha256" + +func sha256sum(contents []byte) []byte { + h := sha256.New() + h.Write(contents) + return h.Sum(nil) +} diff --git a/gonf/values.go b/gonf/values.go new file mode 100644 index 0000000..15c109b --- /dev/null +++ b/gonf/values.go @@ -0,0 +1,27 @@ +package gonf + +type Value interface { + Equals(Value) bool + String() string +} + +// ----- String variables ------------------------------------------------------ +type StringValue struct { + Value string +} + +func (s StringValue) Equals(v Value) bool { + sv, ok := v.(StringValue) + return ok && s.Value == sv.Value +} + +func (s StringValue) String() string { + // TODO handle interpolation + return s.Value +} + +// TODO lists + +// TODO maps + +// TODO what else? diff --git a/gonf/variables.go b/gonf/variables.go new file mode 100644 index 0000000..3c365ae --- /dev/null +++ b/gonf/variables.go @@ -0,0 +1,57 @@ +package gonf + +import "log/slog" + +// ----- Globals --------------------------------------------------------------- +var variables map[string]*VariablePromise + +// ----- Init ------------------------------------------------------------------ +func init() { + variables = make(map[string]*VariablePromise) +} + +// ----- Public ---------------------------------------------------------------- +func Default(name string, value Value) *VariablePromise { + if v, ok := variables[name]; ok { + if !v.isDefault { + slog.Debug("default would overwrite a variable, ignoring", "name", name, "old_value", v.value, "new_value", value) + return nil + } + slog.Error("default is being overwritten", "name", name, "old_value", v.value, "new_value", value) + } + v := &VariablePromise{ + isDefault: true, + name: name, + value: value, + } + variables[name] = v + return v +} +func Variable(name string, value Value) *VariablePromise { + if v, ok := variables[name]; ok && !v.isDefault { + slog.Error("variable is being overwritten", "name", name, "old_value", v, "new_value", value) + } + v := &VariablePromise{ + isDefault: false, + name: name, + value: value, + } + variables[name] = v + return v +} + +type VariablePromise struct { + isDefault bool + name string + value Value +} + +// ----- Internal -------------------------------------------------------------- +func getVariable(name string) string { + if v, ok := variables[name]; ok { + return v.value.String() + } else { + slog.Error("undefined variable or default", "name", name) + return name + } +} -- cgit v1.2.3