chore(gonf): first draft of the gonf lib with commands, files, packages and variables

This commit is contained in:
Julien Dessaux 2024-02-05 00:59:31 +01:00
parent ada87c622c
commit ceac85dbc1
Signed by: adyxax
GPG key ID: F92E51B86E07177E
10 changed files with 522 additions and 0 deletions

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.adyxax.org/adyxax/gonf/v2
go 1.21.5

95
gonf/commands.go Normal file
View file

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

153
gonf/files.go Normal file
View file

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

44
gonf/gonf.go Normal file
View file

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

73
gonf/packages.go Normal file
View file

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

50
gonf/promises.go Normal file
View file

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

11
gonf/triggers.go Normal file
View file

@ -0,0 +1,11 @@
package gonf
type simpleTrigger struct {
fact string
status Status
}
//type compositeTrigger struct {
// triggers []simpleTrigger
// operation Operation
//}

9
gonf/utils.go Normal file
View file

@ -0,0 +1,9 @@
package gonf
import "crypto/sha256"
func sha256sum(contents []byte) []byte {
h := sha256.New()
h.Write(contents)
return h.Sum(nil)
}

27
gonf/values.go Normal file
View file

@ -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?

57
gonf/variables.go Normal file
View file

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