├── testdata ├── bar.jq ├── foo.yaml ├── bar.json ├── foo.json ├── filter.jq └── foo.libsonnet ├── go.mod ├── .gitignore ├── gen.go ├── .goreleaser.yml ├── cmd └── ycat │ └── main.go ├── go.sum ├── LICENSE ├── arguments_test.go ├── pipeline.go ├── eval.go ├── value_test.go ├── stream.go ├── ycat_std.go ├── README.md ├── codec.go ├── ycat.libsonnet ├── arguments.go └── value.go /testdata/bar.jq: -------------------------------------------------------------------------------- 1 | .bar+="foo" 2 | -------------------------------------------------------------------------------- /testdata/foo.yaml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | -------------------------------------------------------------------------------- /testdata/bar.json: -------------------------------------------------------------------------------- 1 | {"bar": "foo"} 2 | -------------------------------------------------------------------------------- /testdata/foo.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /testdata/filter.jq: -------------------------------------------------------------------------------- 1 | .args = $ARGS.positional 2 | -------------------------------------------------------------------------------- /testdata/foo.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | hello(x, name):: x + {name: name}, 3 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alxarch/ycat 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/google/go-jsonnet v0.12.1 7 | gopkg.in/yaml.v2 v2.4.0 8 | ) 9 | 10 | require github.com/sergi/go-diff v1.0.0 // indirect 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | dist/ -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | func main() { 12 | code, err := ioutil.ReadFile("ycat.libsonnet") 13 | if err != nil { 14 | panic(err) 15 | } 16 | code = strconv.AppendQuote(nil, string(code)) 17 | code = append([]byte(`// Code generated by ycat; DO NOT EDIT. 18 | package ycat 19 | const ycatStdLib = `), code...) 20 | code = append(code, '\n') 21 | ioutil.WriteFile("ycat_std.go", code, os.ModePerm) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # # you may remove this if you don't use vgo 6 | - go mod download 7 | # # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | main: ./cmd/ycat/main.go 13 | goos: 14 | - linux 15 | - darwin 16 | - windows 17 | - freebsd 18 | 19 | archive: 20 | replacements: 21 | darwin: Darwin 22 | linux: Linux 23 | windows: Windows 24 | 386: i386 25 | amd64: x86_64 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: "{{ .Tag }}-next" 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | -------------------------------------------------------------------------------- /cmd/ycat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | 8 | "github.com/alxarch/ycat" 9 | ) 10 | 11 | var ( 12 | logger = log.New(os.Stderr, "ycat: ", 0) 13 | ) 14 | 15 | func usage() { 16 | os.Stderr.WriteString(ycat.Usage) 17 | } 18 | 19 | func printUsage(err error) { 20 | if err != nil { 21 | logger.Println(err) 22 | } 23 | usage() 24 | } 25 | 26 | func main() { 27 | tasks, help, err := ycat.ParseArgs(os.Args[1:], os.Stdin, os.Stdout) 28 | if err != nil { 29 | printUsage(err) 30 | os.Exit(2) 31 | } 32 | if help { 33 | printUsage(nil) 34 | os.Exit(0) 35 | } 36 | 37 | ctx := context.Background() 38 | p := ycat.MakePipeline(ctx, tasks...) 39 | exitCode := 0 40 | for err := range p.Errors() { 41 | if err != nil { 42 | exitCode = 2 43 | logger.Println(err) 44 | } 45 | } 46 | os.Exit(exitCode) 47 | } 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-jsonnet v0.12.1 h1:v0iUm/b4SBz7lR/diMoz9tLAz8lqtnNRKIwMrmU2HEU= 2 | github.com/google/go-jsonnet v0.12.1/go.mod h1:gVu3UVSfOt5fRFq+dh9duBqXa5905QY8S1QvMNcEIVs= 3 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 4 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 5 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 6 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 7 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 8 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 9 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 10 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 11 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexandros Sigalas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /arguments_test.go: -------------------------------------------------------------------------------- 1 | package ycat_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/alxarch/ycat" 12 | ) 13 | 14 | type nopCloser struct { 15 | io.Writer 16 | } 17 | 18 | func (nopCloser) Close() error { 19 | return nil 20 | } 21 | func TestParseArgs(t *testing.T) { 22 | type TestCase struct { 23 | Args []string 24 | Stdin string 25 | Stdout string 26 | } 27 | tcs := []TestCase{ 28 | {[]string{"-e", "x"}, "null\n---\nnull\n", "null\n---\nnull\n"}, 29 | {nil, "1", "1\n"}, 30 | {[]string{"-n"}, "", "null\n"}, 31 | {[]string{"-n", "-v", "y==foo", "-e", "y"}, "", "foo\n"}, 32 | {[]string{"-n", "--input-var", "y", "-e", "y"}, "", "null\n"}, 33 | {[]string{"testdata/foo.yaml", "-i", "foo=testdata/foo.libsonnet", "-e", "foo.hello(x, 'world')"}, "", "foo: bar\nname: world\n"}, 34 | {[]string{"testdata/foo.yaml"}, "", "foo: bar\n"}, 35 | {[]string{"-y", "testdata/foo.yaml"}, "", "foo: bar\n"}, 36 | {[]string{"testdata/foo.yaml", "-o", "j"}, "", `{"foo":"bar"}` + "\n"}, 37 | {[]string{"testdata/foo.yaml", "testdata/bar.json"}, "", "foo: bar\n---\nbar: foo\n"}, 38 | {[]string{"testdata/foo.yaml", "testdata/bar.json", "-a"}, "", "- foo: bar\n- bar: foo\n"}, 39 | {[]string{"testdata/foo.yaml", "-e", `{bar: "baz"} + x`}, "", "bar: baz\nfoo: bar\n"}, 40 | // {[]string{""}, false, false, 2, "1", "1\n"}, 41 | } 42 | for i, tc := range tcs { 43 | name := fmt.Sprintf("%v", tc.Args) 44 | t.Run(name, func(t *testing.T) { 45 | buf := &bytes.Buffer{} 46 | stdout := &nopCloser{buf} 47 | stdin := strings.NewReader(tc.Stdin) 48 | tasks, _, err := ycat.ParseArgs(tc.Args, stdin, stdout) 49 | if err != nil { 50 | t.Fatal(i, err) 51 | } 52 | p := ycat.MakePipeline(context.Background(), tasks...) 53 | for err := range p.Errors() { 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | if buf.String() != tc.Stdout { 59 | t.Errorf("Wrong output: %q != %q", buf.String(), tc.Stdout) 60 | } 61 | }) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package ycat 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Pipeline is the endpoint of a value stream process 9 | type Pipeline struct { 10 | values <-chan RawValue 11 | errors <-chan error 12 | } 13 | 14 | // Errors returns a channel with errors from tasks 15 | func (p *Pipeline) Errors() <-chan error { 16 | if p.errors == nil { 17 | ch := make(chan error) 18 | close(ch) 19 | p.errors = ch 20 | } 21 | return p.errors 22 | } 23 | 24 | // Values returns a channel with values from tasks 25 | func (p *Pipeline) Values() <-chan RawValue { 26 | if p.values == nil { 27 | ch := make(chan RawValue) 28 | close(ch) 29 | p.values = ch 30 | } 31 | return p.values 32 | } 33 | 34 | // MakePipeline builds and runs a pipeline 35 | func MakePipeline(ctx context.Context, tasks ...StreamTask) (p *Pipeline) { 36 | p = new(Pipeline) 37 | return p.Pipe(ctx, tasks...) 38 | } 39 | 40 | // Pipe adds tasks ro a pipeline 41 | func (p *Pipeline) Pipe(ctx context.Context, tasks ...StreamTask) *Pipeline { 42 | ecs := make([]<-chan error, 0, len(tasks)+1) 43 | ecs = append(ecs, p.Errors()) 44 | for _, t := range tasks { 45 | p = p.task(ctx, t) 46 | ecs = append(ecs, p.Errors()) 47 | } 48 | return &Pipeline{p.Values(), MergeErrors(ecs...)} 49 | } 50 | 51 | func (p *Pipeline) task(ctx context.Context, task StreamTask) *Pipeline { 52 | src := p.Values() 53 | errc := make(chan error, 1) 54 | s := stream{ 55 | done: ctx.Done(), 56 | src: src, 57 | } 58 | var out chan RawValue 59 | switch task := task.(type) { 60 | case Consumer: 61 | out = make(chan RawValue) 62 | close(out) 63 | s.out = out 64 | go func() { 65 | defer close(errc) 66 | errc <- task.Consume(&s) 67 | // Drain src 68 | for _ = range src { 69 | } 70 | }() 71 | case Producer: 72 | out = make(chan RawValue, 1) 73 | s.out = out 74 | go func() { 75 | defer close(errc) 76 | defer close(out) 77 | Drain(&s) 78 | errc <- task.Produce(&s) 79 | }() 80 | default: 81 | out = make(chan RawValue) 82 | s.out = out 83 | go func() { 84 | defer close(errc) 85 | defer close(out) 86 | errc <- task.Run(&s) 87 | // Drain src 88 | for _ = range src { 89 | } 90 | }() 91 | } 92 | return &Pipeline{out, errc} 93 | 94 | } 95 | 96 | // MergeErrors is a helper function that merges error channels 97 | func MergeErrors(cs ...<-chan error) <-chan error { 98 | switch n := len(cs); n { 99 | case 1: 100 | return cs[0] 101 | case 0: 102 | out := make(chan error) 103 | close(out) 104 | return out 105 | default: 106 | out := make(chan error, n) 107 | wg := sync.WaitGroup{} 108 | wg.Add(n) 109 | for i := range cs { 110 | c := cs[i] 111 | go func() { 112 | defer wg.Done() 113 | for v := range c { 114 | out <- v 115 | } 116 | }() 117 | } 118 | go func() { 119 | defer close(out) 120 | wg.Wait() 121 | }() 122 | return out 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package ycat 2 | 3 | //go:generate go run gen.go 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | jsonnet "github.com/google/go-jsonnet" 12 | ) 13 | 14 | // Eval is the execution environment for Jsonnet 15 | type Eval struct { 16 | Bind string 17 | MaxStackSize int 18 | Array bool 19 | Vars map[string]Var 20 | vm *jsonnet.VM 21 | } 22 | 23 | // VarType is the type of an external variable 24 | type VarType uint 25 | 26 | // VarTypes 27 | const ( 28 | _ VarType = iota 29 | FileVar 30 | CodeVar 31 | RawVar 32 | ) 33 | 34 | // Var is an external variable 35 | type Var struct { 36 | Type VarType 37 | Value string 38 | } 39 | 40 | // AddVar adds an external variable 41 | func (e *Eval) AddVar(typ VarType, name, value string) { 42 | if e.Vars == nil { 43 | e.Vars = make(map[string]Var) 44 | } 45 | e.Vars[name] = Var{typ, value} 46 | } 47 | 48 | // Render renders the Jsonnet snippet to be executed 49 | func (v Var) Render(w *strings.Builder, name string) { 50 | w.WriteString("local ") 51 | w.WriteString(name) 52 | w.WriteString(" = ") 53 | switch v.Type { 54 | case FileVar: 55 | switch path.Ext(v.Value) { 56 | case ".json", ".libsonnet", ".jsonnet": 57 | w.WriteString(`import "`) 58 | case ".yaml", ".yml": 59 | w.WriteString(`importyaml "`) 60 | default: 61 | w.WriteString(`importstr "`) 62 | } 63 | w.WriteString(v.Value) 64 | w.WriteString("\";\n") 65 | default: 66 | w.WriteString(`std.extVar("`) 67 | w.WriteString(name) 68 | w.WriteString("\");\n") 69 | } 70 | 71 | } 72 | 73 | // Render renders a snippet binding local variables 74 | func (e *Eval) Render(snippet string) string { 75 | w := strings.Builder{} 76 | for name, v := range e.Vars { 77 | v.Render(&w, name) 78 | } 79 | bind := bindVar(e.Bind) 80 | Var{Type: CodeVar}.Render(&w, "_") 81 | Var{Type: CodeVar}.Render(&w, bind) 82 | w.WriteString(snippet) 83 | return w.String() 84 | } 85 | 86 | // VM updates or creates a Jsonnet VM 87 | func (e *Eval) VM() (vm *jsonnet.VM) { 88 | if e.vm == nil { 89 | e.vm = jsonnet.MakeVM() 90 | } 91 | vm = e.vm 92 | if e.MaxStackSize > 0 { 93 | vm.MaxStack = e.MaxStackSize 94 | } 95 | 96 | for name, v := range e.Vars { 97 | switch v.Type { 98 | case FileVar: 99 | // Handled by import 100 | case CodeVar: 101 | vm.ExtCode(name, v.Value) 102 | default: 103 | vm.ExtVar(name, v.Value) 104 | } 105 | } 106 | vm.ExtCode("_", ycatStdLib) 107 | return vm 108 | 109 | } 110 | 111 | // DefaultInputVar is the default name for the stream value 112 | const DefaultInputVar = "x" 113 | 114 | func bindVar(v string) string { 115 | if v == "" { 116 | return DefaultInputVar 117 | } 118 | return v 119 | } 120 | 121 | func (e *Eval) SnippetFromFile(filename string) StreamTask { 122 | return StreamFunc(func(s Stream) error { 123 | snippet, err := ioutil.ReadFile(filename) 124 | if err != nil { 125 | return err 126 | } 127 | return e.Snippet(filename, string(snippet)).Run(s) 128 | }) 129 | } 130 | 131 | // EvalSnippetTask transforms a stream of input values with Jsonnet 132 | func (e *Eval) Snippet(filename, snippet string) StreamTask { 133 | bind := bindVar(e.Bind) 134 | vm := e.VM() 135 | snippet = e.Render(snippet) 136 | return StreamFunc(func(s Stream) error { 137 | for { 138 | v, ok := s.Next() 139 | if !ok { 140 | return nil 141 | } 142 | vm.ExtCode(bind, v.MarshalJSONString()) 143 | result, err := vm.EvaluateSnippet(filename, snippet) 144 | if err != nil { 145 | return err 146 | } 147 | if !s.Push(RawValue(result)) { 148 | return nil 149 | } 150 | } 151 | }) 152 | } 153 | 154 | // EvalFilename returns a filename on CWD 155 | func EvalFilename() (string, error) { 156 | cwd, err := os.Getwd() 157 | if err != nil { 158 | return "", err 159 | } 160 | return path.Join(cwd, "ycat.jsonnet"), nil 161 | } 162 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | package ycat_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/alxarch/ycat" 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func TestMap_MarshalJSON(t *testing.T) { 13 | m := ycat.NewMap 14 | a := func(values ...interface{}) []interface{} { 15 | return values 16 | } 17 | tests := []struct { 18 | Map ycat.Map 19 | wantJSON string 20 | wantErr bool 21 | }{ 22 | {nil, `null`, false}, 23 | {m(), `{}`, false}, 24 | {m("foo", m()), `{"foo":{}}`, false}, 25 | {m("foo", a(42)), `{"foo":[42]}`, false}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(string(tt.wantJSON), func(t *testing.T) { 29 | data, err := json.Marshal(tt.Map) 30 | if (err != nil) != tt.wantErr { 31 | t.Errorf("Map.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 32 | } 33 | if string(data) != tt.wantJSON { 34 | t.Errorf("Map.MarshalJSON() %q != %q", data, tt.wantJSON) 35 | } 36 | }) 37 | } 38 | 39 | } 40 | func TestMap_UnmarshalJSON(t *testing.T) { 41 | m := ycat.NewMap 42 | a := func(values ...interface{}) []interface{} { 43 | return values 44 | } 45 | tests := []struct { 46 | Map ycat.Map 47 | JSON string 48 | wantErr bool 49 | }{ 50 | {m("foo", m("bar", "baz")), `{"foo":{"bar":"baz"}}`, false}, 51 | {m("foo", a(1, 2, 3)), `{"foo":[1,2,3]}`, false}, 52 | {nil, `null`, false}, 53 | {m(), `{}`, false}, 54 | {m("foo", m()), `{"foo":{}}`, false}, 55 | } 56 | for _, tt := range tests { 57 | t.Run("json="+string(tt.JSON), func(t *testing.T) { 58 | var v ycat.Map 59 | err := json.Unmarshal([]byte(tt.JSON), &v) 60 | if (err != nil) != tt.wantErr { 61 | t.Errorf("map.UnmarshalJSON() error = %v, wanterr %v", err, tt.wantErr) 62 | } 63 | data, err := json.Marshal(v) 64 | if (err != nil) != tt.wantErr { 65 | t.Errorf("map.UnmarshalJSON() error = %v, wanterr %v", err, tt.wantErr) 66 | } 67 | 68 | if string(data) != tt.JSON { 69 | t.Errorf("Map.UnmarshalJSON() %q != %q", string(data), tt.JSON) 70 | } 71 | }) 72 | } 73 | 74 | } 75 | func TestRawValue_UnmarshalYAML(t *testing.T) { 76 | tests := []struct { 77 | YAML string 78 | wantValue ycat.RawValue 79 | wantErr bool 80 | }{ 81 | {`foo: {}`, `{"foo":{}}`, false}, 82 | {"{}", `{}`, false}, 83 | {`""`, `""`, false}, 84 | {"[]", `[]`, false}, 85 | {"42", "42", false}, 86 | {"foo: bar", `{"foo":"bar"}`, false}, 87 | {"null", ``, false}, 88 | {"[1,2,3]", `[1,2,3]`, false}, 89 | } 90 | for _, tt := range tests { 91 | t.Run(string(tt.wantValue), func(t *testing.T) { 92 | var v ycat.RawValue 93 | if err := yaml.Unmarshal([]byte(tt.YAML), &v); (err != nil) != tt.wantErr { 94 | t.Errorf("RawValue.UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr) 95 | } 96 | v, err := v.Compact() 97 | if (err != nil) != tt.wantErr { 98 | t.Errorf("RawValue.Compact() error = %v, wantErr %v", err, tt.wantErr) 99 | } 100 | if v != tt.wantValue { 101 | t.Errorf("RawValue.UnmarshalYAML() %q != %q", v, tt.wantValue) 102 | } 103 | }) 104 | } 105 | } 106 | 107 | func TestRawValue_MarshalYAML(t *testing.T) { 108 | tests := []struct { 109 | YAML string 110 | value ycat.RawValue 111 | wantErr bool 112 | }{ 113 | {`foo: {}`, `{"foo":{}}`, false}, 114 | {"{}", `{}`, false}, 115 | {`""`, `""`, false}, 116 | {"[]", "[]", false}, 117 | {"answer: 42", `{"answer":42}`, false}, 118 | {"answer:\n- 42\n", `{"answer":[42]}`, false}, 119 | {"42", "42", false}, 120 | {"foo: bar", `{"foo":"bar"}`, false}, 121 | {"null", ``, false}, 122 | {"- 1\n- 2\n- 3", `[1,2,3]`, false}, 123 | } 124 | for _, tt := range tests { 125 | t.Run(string(tt.value), func(t *testing.T) { 126 | data, err := yaml.Marshal(tt.value) 127 | if (err != nil) != tt.wantErr { 128 | t.Errorf("RawValue.MarshalYAML() error = %v, wantErr %v", err, tt.wantErr) 129 | } 130 | want := tt.YAML 131 | if !strings.HasSuffix(want, "\n") { 132 | want += "\n" 133 | } 134 | 135 | if string(data) != want { 136 | t.Errorf("RawValue.MarsalYAML() %q != %q", string(data), want) 137 | } 138 | }) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package ycat 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | // WriteStream is a writtable stream of values 9 | type WriteStream interface { 10 | Push(RawValue) bool 11 | } 12 | 13 | // ReadStream is a readable stream of values 14 | type ReadStream interface { 15 | Next() (RawValue, bool) 16 | } 17 | 18 | // Stream is a readable/writable stream of values 19 | type Stream interface { 20 | ReadStream 21 | WriteStream 22 | } 23 | 24 | //StreamTask represents a task to run on a Stream 25 | type StreamTask interface { 26 | Run(s Stream) error 27 | } 28 | 29 | // StreamFunc is a StreamTask callback 30 | type StreamFunc func(s Stream) error 31 | 32 | //Run implements StreamTask for StramFunc 33 | func (f StreamFunc) Run(s Stream) error { return f(s) } 34 | 35 | // Consumer consumes values from a readable stream 36 | type Consumer interface { 37 | Consume(s ReadStream) error 38 | } 39 | 40 | // ConsumerFunc is a Consumer callback 41 | type ConsumerFunc func(s ReadStream) error 42 | 43 | // Consume implements Consumer 44 | func (f ConsumerFunc) Consume(s ReadStream) error { return f(s) } 45 | 46 | // Run implements StreamTask 47 | func (f ConsumerFunc) Run(s Stream) error { return f(s) } 48 | 49 | // Producer generates values for a WriteStream 50 | type Producer interface { 51 | Produce(s WriteStream) error 52 | } 53 | 54 | // ProducerFunc is a Producer callback 55 | type ProducerFunc func(s WriteStream) error 56 | 57 | // Produce implements Producer for ProducerFunc 58 | func (f ProducerFunc) Produce(s WriteStream) error { return f(s) } 59 | 60 | // Run implements StreamTask for ProducerFunc 61 | func (f ProducerFunc) Run(s Stream) error { 62 | if Drain(s) { 63 | return f(s) 64 | } 65 | return nil 66 | } 67 | 68 | // Producers is a sequence of Producers 69 | type Producers []Producer 70 | 71 | // Run implements StreamTask for Producers 72 | func (tasks Producers) Run(s Stream) error { return tasks.Produce(s) } 73 | 74 | // Produce implements Producer for Producers 75 | func (tasks Producers) Produce(s WriteStream) error { 76 | for _, task := range tasks { 77 | if err := task.Produce(s); err != nil { 78 | return err 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | type stream struct { 85 | done <-chan struct{} 86 | src <-chan RawValue 87 | out chan<- RawValue 88 | } 89 | 90 | // Next implements ReadStream 91 | func (s *stream) Next() (v RawValue, ok bool) { 92 | select { 93 | case v, ok = <-s.src: 94 | // println("s.value", ok) 95 | case <-s.done: 96 | // println("next s.done", ok) 97 | } 98 | return 99 | } 100 | 101 | // Push implements WriteStream 102 | func (s *stream) Push(v RawValue) bool { 103 | select { 104 | case s.out <- v: 105 | return true 106 | case <-s.done: 107 | return false 108 | } 109 | } 110 | 111 | // NullStream is a Producer that pushes a null 112 | type NullStream struct{} 113 | 114 | // Produce implements Producer for NullStream 115 | func (NullStream) Produce(s WriteStream) error { 116 | s.Push("null") 117 | return nil 118 | } 119 | 120 | type Debug string 121 | 122 | func (d Debug) Run(s Stream) (err error) { 123 | logger := log.New(os.Stderr, string(d), 0) 124 | for { 125 | v, ok := s.Next() 126 | if !ok { 127 | logger.Println("EOF") 128 | return nil 129 | } 130 | logger.Println("Value", v) 131 | if !s.Push(v) { 132 | logger.Println("Push end") 133 | return nil 134 | } 135 | } 136 | } 137 | 138 | // ToArray concatenates stream values to an array 139 | type ToArray struct{} 140 | 141 | // Run implements StreamTask for ToArray 142 | func (ToArray) Run(s Stream) (err error) { 143 | var values []RawValue 144 | for { 145 | v, ok := s.Next() 146 | if ok { 147 | values = append(values, v) 148 | } else { 149 | break 150 | } 151 | } 152 | if values != nil { 153 | s.Push(RawValueArray(values...)) 154 | } 155 | return 156 | } 157 | 158 | // Drain is a helper that drains all values from a stream 159 | func Drain(s Stream) bool { 160 | for { 161 | v, ok := s.Next() 162 | if !ok { 163 | return true 164 | } 165 | if !s.Push(v) { 166 | return false 167 | } 168 | } 169 | 170 | } 171 | 172 | // type DrainTask struct{} 173 | 174 | // func (DrainTask) Run(s Stream) error { 175 | // Drain(s) 176 | // return nil 177 | // } 178 | // func (DrainTask) Init(ctx context.Context) int { 179 | // return 0 180 | // } 181 | 182 | // type drainStream struct { 183 | // Stream 184 | // Drained bool 185 | // } 186 | 187 | // func DrainStream(s Stream) WriteStream { 188 | // return &drainStream{s, false} 189 | // } 190 | 191 | // func (s *drainStream) Push(v RawValue) bool { 192 | // if !s.Drained { 193 | // s.Drained = true 194 | // if !Drain(s.Stream) { 195 | // return false 196 | // } 197 | // } 198 | // return s.Stream.Push(v) 199 | // } 200 | -------------------------------------------------------------------------------- /ycat_std.go: -------------------------------------------------------------------------------- 1 | // Code generated by ycat; DO NOT EDIT. 2 | package ycat 3 | const ycatStdLib = "// Usefull functions\n// Completely inefficient :) should be ported to nativeFuncs\nlocal result(input, arr) =\n if std.isString(input) && std.isArray(arr) then std.join('', arr) else arr\n ;\n\nlocal has(x, y) = \n local t = std.type(x);\n if t == 'array' then\n std.count(x, y) > 0\n else if t == 'object' then\n std.objectHas(x, y)\n else if t == 'string' then\n std.length(std.findSubstr(y, x)) > 0\n else\n false\n ;\n\nlocal skipFunc(x) = if std.type(x) == 'function' then x else function(y) y == x;\nlocal trimFunc(cutset) =\n if std.isString(cutset) then\n local cs = std.stringChars(cutset);\n function (c) std.count(cs, c) > 0\n else if std.isArray(cutset) then\n function (c) std.count(cutset, c) > 0\n else if std.isFunction(cutset) then\n cutset\n else if std.isObject(cutset) then\n function (c) std.objectHas(cutset, c)\n else if std.isNumber(cutset) then\n function (c) std.codepoint(c) == cutset\n else\n function (c) false\n ;\nstd + {\n local _ = self\n , len:: std.length\n , has:: has\n , get(obj, key, v=null)::\n if std.isObject(obj) && std.objectHas(obj, key) then obj[key] else v\n , sum(arr)::\n local add(total, n) = total + n;\n std.foldl(add, arr, 0)\n , avg(arr)::\n local n = std.length(arr);\n if n > 0 then _.sum(arr)/n else 0\n , skipWhile(pred, arr)::\n local func = skipFunc(pred);\n local skip = function(acc, x) {\n skip:: if acc.skip then func(x) else false,\n out:: if self.skip then [] else acc.out + [x],\n };\n result(arr, std.foldl(skip, arr, {skip:: true, out:: []}).out)\n , takeWhile(pred, arr)::\n local func = skipFunc(pred);\n local take = function(acc, x) {\n ok:: if acc.ok then func(x) else false,\n out:: if self.ok then acc.out + [x] else acc.out,\n };\n result(arr, std.foldl(take, arr, {ok:: true, out:: []}).out)\n , indexOf(arr, x)::\n local fn(y) = x != y;\n local n = std.length(_.takeWhile(fn, arr));\n if n == std.length(arr) then -1 else n\n , not(func):: function(x) if func(x) then false else true\n , takeUntil(pred, arr):: _.takeWhile(_.not(skipFunc(pred)), arr)\n , skipUntil(pred, arr):: _.skipWhile(_.not(skipFunc(pred)), arr)\n , trunc(arr, size):: // Truncate array\n local sz = std.min(size, std.length(arr));\n result(arr, std.makeArray(sz, function(i) arr[i]))\n , rev(arr):: // Reverse array\n local size = std.length(arr);\n local n = size - 1;\n result(arr, std.makeArray(size, function(i) arr[n-i]))\n , ascii:: {\n local inRange(min, max) =\n local _min = std.codepoint(min);\n local _max = std.codepoint(max);\n function (c) _min <= std.codepoint(c) && std.codepoint(c) <= _max\n , isLower:: inRange('a', 'z')\n , isUpper:: inRange('A', 'Z')\n , isDigit:: inRange('0', '9')\n , space:: \" \\n\\t\\r\"\n , isAlpha(c):: _.ascii.isLower(c) || _.ascii.isUpper(c)\n , isAlnum(c):: _.ascii.isLower(c) || _.ascii.isUpper(c) || _.ascii.isDigit(c)\n , isSpace(c):: c == \" \" || c == \"\\n\" || c == \"\\t\" || c == \"\\r\"\n }\n , squeeze(s, cutset)::\n local tr = trimFunc(cutset);\n local fn(acc, c) =\n local n = std.length(acc) - 1;\n if tr(c) && n >= 0 && tr(acc[n]) then\n acc\n else\n acc + [c];\n local ss = std.foldl(fn, s, []);\n result(s, ss)\n\n , normalize(s):: // Trim and consolidate sequential whitespace to ' '\n local toSpace(c) = if _.ascii.isSpace(c) then ' ' else c;\n local ls = _.skipWhile(\" \", _.map(toSpace, s));\n local rs = _.skipWhile(\" \", _.rev(ls));\n local ss = _.squeeze(_.rev(rs), \" \");\n result(s, ss)\n\n , trimLeft(s, cutset=_.ascii.space):: // Trim left side of a string\n local tr = trimFunc(cutset);\n _.skipWhile(tr, s)\n\n , trimRight(s, cutset=_.ascii.space):: // Trim right side of a string\n local tr = trimFunc(cutset);\n local rs = _.skipWhile(tr, _.rev(s));\n local ls = _.rev(rs);\n result(s, ls)\n , trim(s, cutset=_.ascii.space):: // Trim both sides of a string\n local tr = trimFunc(cutset);\n local rs = _.skipWhile(tr, _.rev(s));\n local ls = _.skipWhile(tr, _.rev(rs));\n result(s, ls)\n , k8s:: {\n maxNameSize:: 253\n , trunc(name)::\n if std.length(name) > _.k8s.maxNameSize then\n result(name, _.trunc(name, _.k8s.maxNameSize))\n else\n name\n , namespace(res, ns, override=true)::\n local n = _.k8s.name(ns);\n if override then\n res + {metadata: {namespace: n}}\n else\n {metadata+: {namespace: n}} + res\n , name(s):: // convert string to kubernetes name\n local fn(c) =\n if _.ascii.isLower(c) then c\n else if _.ascii.isDigit(c) then c\n else if _.ascii.isUpper(c) then std.asciiLower(c)\n else '-';\n local cs = std.map(fn, s);\n local rs = _.skipWhile('-', _.rev(cs)); // trim - from end\n local ls = _.skipUntil(_.ascii.isLower, _.rev(rs)); // trim -,0-9 from start\n local name = _.squeeze(ls, \"-\"); // squeeze sequential '-'\n result(s, _.k8s.trunc(name))\n }\n\n}" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ycat 2 | 3 | Command line processor for YAML/JSON files using [Jsonnet](https://jsonnet.org/) 4 | 5 | ## Usage 6 | ``` 7 | ycat - command line YAML/JSON processor 8 | 9 | USAGE: 10 | ycat [OPTIONS] [INPUT...] 11 | ycat [OPTIONS] [PIPELINE...] 12 | 13 | OPTIONS: 14 | -o, --out {json|j|yaml|y} Set output format 15 | -h, --help Show help and exit 16 | 17 | INPUT: 18 | [FILE...] Read values from file(s) 19 | -y, --yaml [FILE...] Read YAML values from file(s) 20 | -j, --json [FILE...] Read JSON values from file(s) 21 | -n, --null Inject a null value 22 | -a, --array Merge values to array 23 | 24 | PIPELINE: 25 | [INPUT...] [ENV...] EVAL 26 | 27 | ENV: 28 | -v, --var = Bind Jsonnet variable to code 29 | == Bind Jsonnet variable to a string value 30 | -i, --import = Import file into a local Jsonnet variable 31 | --input-var Change the name of the input value variable (default x) 32 | --max-stack Jsonnet VM max stack size (default 500) 33 | 34 | EVAL: 35 |