commit 8b2170b2ecda5e10f2daafe278bd42f9b01b1bd3 Author: jolheiser Date: Wed Nov 29 12:56:08 2023 -0600 initial commit Signed-off-by: jolheiser diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c198a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/nixfig +/nixfix.exe diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..56f09ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7b1e55 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# nixfig + +Read a nix file as a config. + +Essentially just wraps `nix eval (--json) --expr`. + + +Allows parsing the following: +```nix +let + user = "jolheiser"; +in { + log = { + level = "warn"; + # Name the log file after the user....for reasons + file = "${user}.log"; + }; + http = { + host = "0.0.0.0"; + port = 1234; + # Make user an admin, but also make a generic admin user + admins = [user "admin"]; + }; +} +``` + +Into a struct like: +```go +type Config struct { + Log struct { + Level string // warn + File string // jolheiser.log + } + HTTP struct { + Host string // 0.0.0.0 + Port int // 1234 + Admins []string // [jolheiser admin] + } +} +``` + +It can also marshal a struct into a valid (albeit minified) nix expression. + + +## License + +[MIT](LICENSE) diff --git a/cmd/nixfig/main.go b/cmd/nixfig/main.go new file mode 100644 index 0000000..f36d962 --- /dev/null +++ b/cmd/nixfig/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "go.jolheiser.com/nixfig" +) + +func maine() error { + if len(os.Args) != 2 { + return errors.New("nixfig requires 1 argument") + } + fn := os.Args[1] + + data, err := os.ReadFile(fn) + if err != nil { + return fmt.Errorf("could not read file %q: %w", fn, err) + } + + var out []byte + var a any + switch ext := filepath.Ext(fn); ext { + case ".json": + if err := json.Unmarshal(data, &a); err != nil { + return fmt.Errorf("invalid JSON file: %w", err) + } + out, err = nixfig.Marshal(a) + case ".nix": + if err := nixfig.Unmarshal(data, &a); err != nil { + return fmt.Errorf("invalid nix file: %w", err) + } + out, err = json.Marshal(a) + default: + return fmt.Errorf("unknown extension %q, must be json or nix", ext) + } + if err != nil { + return err + } + + fmt.Print(string(out)) + return nil +} + +func main() { + if err := maine(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..320eb92 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module go.jolheiser.com/nixfig + +go 1.21.4 + +require github.com/alecthomas/assert/v2 v2.4.0 + +require ( + github.com/alecthomas/repr v0.3.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bb218ed --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/alecthomas/assert/v2 v2.4.0 h1:/ZiZ0NnriAWPYYO+4eOjgzNELrFQLaHNr92mHSHFj9U= +github.com/alecthomas/assert/v2 v2.4.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= +github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= +github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= diff --git a/nixfig.go b/nixfig.go new file mode 100644 index 0000000..b91d349 --- /dev/null +++ b/nixfig.go @@ -0,0 +1,72 @@ +package nixfig + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" +) + +var ( + // Nix is the command to call for nix + Nix string + ErrNixNotFound = errors.New("nix was not found or set. You can set it either with `nixfig.Nix` or the `NIXFIG_NIX` environment variable") +) + +func init() { + nixPath, _ := exec.LookPath("nix") + Nix = nixPath + if envPath, ok := os.LookupEnv("NIXFIG_NIX"); ok { + Nix = envPath + } +} + +// Unmarshal unmarshals a nix expression as JSON into a struct +func Unmarshal(data []byte, v any) error { + if Nix == "" { + return ErrNixNotFound + } + + var stdout, stderr bytes.Buffer + cmd := exec.Command(Nix, "eval", "--json", "--expr", string(data)) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("could not run %q: %w", Nix, err) + } + if stderr.Len() > 0 { + return errors.New(stderr.String()) + } + + if err := json.Unmarshal(stdout.Bytes(), v); err != nil { + return fmt.Errorf("could not unmarshal nix config as JSON: %w", err) + } + return nil +} + +// Marshal marshals a struct into a nix expression +func Marshal(v any) ([]byte, error) { + if Nix == "" { + return nil, ErrNixNotFound + } + + data, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("could not marshal JSON: %w", err) + } + + var stdout, stderr bytes.Buffer + cmd := exec.Command(Nix, "eval", "--expr", fmt.Sprintf(`(builtins.fromJSON %q)`, string(data))) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("could not run %q: %w", Nix, err) + } + if stderr.Len() > 0 { + return nil, errors.New(stderr.String()) + } + + return stdout.Bytes(), nil +} diff --git a/nixfig_test.go b/nixfig_test.go new file mode 100644 index 0000000..517070e --- /dev/null +++ b/nixfig_test.go @@ -0,0 +1,69 @@ +package nixfig + +import ( + "os" + "testing" + + "github.com/alecthomas/assert/v2" +) + +type Config struct { + Log struct { + Level string + File string + } + HTTP struct { + Host string + Port int + Admins []string + } +} + +var testCfg = Config{ + Log: struct { + Level string + File string + }{ + Level: "warn", + File: "jolheiser.log", + }, + HTTP: struct { + Host string + Port int + Admins []string + }{ + Host: "0.0.0.0", + Port: 1234, + Admins: []string{"jolheiser", "admin"}, + }, +} + +func TestNixNotFound(t *testing.T) { + oldNix := Nix + Nix = "" + + _, err := Marshal(nil) + assert.IsError(t, err, ErrNixNotFound) + + Nix = oldNix +} + +func TestUnmarshal(t *testing.T) { + data, err := os.ReadFile("testdata/config.nix") + assert.NoError(t, err) + + var cfg Config + err = Unmarshal(data, &cfg) + assert.NoError(t, err) + assert.Equal(t, testCfg, cfg) +} + +func TestMarshal(t *testing.T) { + data, err := Marshal(testCfg) + assert.NoError(t, err) + + var cfg Config + err = Unmarshal(data, &cfg) + assert.NoError(t, err) + assert.Equal(t, testCfg, cfg) +} diff --git a/testdata/config.nix b/testdata/config.nix new file mode 100644 index 0000000..2d38d2b --- /dev/null +++ b/testdata/config.nix @@ -0,0 +1,15 @@ +let + user = "jolheiser"; +in { + log = { + level = "warn"; + # Name the log file after the user....for reasons + file = "${user}.log"; + }; + http = { + host = "0.0.0.0"; + port = 1234; + # Make user an admin, but also make a generic admin user + admins = [user "admin"]; + }; +}