-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmain.go
244 lines (206 loc) · 6.69 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"os"
"strings"
"github.com/imdario/mergo"
jsone "github.com/taskcluster/json-e"
// Quick hack of ghodss YAML to expose a new method
yaml_ghodss "github.com/wryun/yaml-1"
yaml_v2 "gopkg.in/yaml.v2"
)
const description = `rjsone is a simple wrapper around the JSON-e templating language.
See: https://taskcluster.github.io/json-e/
Context is usually provided by a list of arguments. By default,
these are interpreted as files. Data is loaded as YAML/JSON by default
and merged into the main context. If the 'filename' begins with a +,
the rest of the argument is interpreted as a raw string rather than
reading the file. For example:
rjsone -t template.yaml context.yaml '+{"foo": 1}'
When duplicate keys are found, later entries replace earlier at the
top level only unless the -d flag is passed to perform deep merging.
You can specify a particular context key to load a YAML/JSON file into
using keyname:filename.yaml. You can also use keyname:.. to indicate
that subsequent entries without keys should be loaded as a list element
into that key. If you instead use keyname:..., metadata information is
loaded as well and each list element is an object containing {filename,
basename, content}.
When loading the context, the default input format is YAML but you can
also use JSON, plain text, and kv (key value pairs, space separated,
as used by bazel and many unix tools). To specify the format, rather
than using a : you use :format:. For example:
:yaml:ctx.yaml :kv:ctx.kv :json:ctx.json mykey:text:ctx.txt
Note that you must specify a key name under which to load the plain text
file, since it cannot define keys (i.e. is a plain text string). Also,
although the default format is yaml, the default format with :: is
text. So the following equivalencies hold:
mykey::context.txt == mykey:text:context.txt
context.yaml == :context.yaml == :yaml:context.yaml
A common pattern, therefore, is to provide plain text arguments to
the template:
rjsone -t template.yaml env::+production context.yaml
For complex applications, single argument functions can be added by
prefixing the filename with a - (or a -- for raw string input). For
example:
b64decode::--'base64 -d'
This adds a base64 decode function to the context which accepts two
arguments as input, an array (command line arguments) and string (stdin),
and outputs a string. In your template, you would use this function by
like b64decode([], 'Zm9vCg=='). As with before, you can use format
specifiers (:- is yaml on both sides for the default behaviour, and
you can explicitly specify kv/json/text/yaml between both :: and
--).
`
type arguments struct {
yaml bool
indentation int
templateFile string
verbose bool
deepMerge bool
outputFile string
contexts []context
}
type content interface {
load() (interface{}, error)
metadata() map[string]interface{}
}
func main() {
var args arguments
flag.Usage = func() {
fmt.Fprint(flag.CommandLine.Output(), description)
fmt.Fprintf(flag.CommandLine.Output(), "\nUsage: %s [options] [context ...]\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprint(flag.CommandLine.Output(), "\n")
}
flag.StringVar(&args.templateFile, "t", "-", "file to use for template (- is stdin)")
flag.BoolVar(&args.yaml, "y", false, "output YAML rather than JSON (always reads YAML/JSON)")
flag.BoolVar(&args.verbose, "v", false, "show information about processing on stderr")
flag.BoolVar(&args.deepMerge, "d", false, "performs a deep merge of contexts")
flag.StringVar(&args.outputFile, "o", "-", "output to a file (default is -, which is stdout)")
flag.IntVar(&args.indentation, "i", 2, "indentation of JSON output; 0 means no pretty-printing")
flag.Parse()
args.contexts = parseContexts(flag.Args())
logger := log.New(os.Stderr, "", 0)
if err := run(logger, args); err != nil {
fmt.Fprintf(flag.CommandLine.Output(), "Fatal error: %s\n", err)
os.Exit(2)
}
}
func run(l *log.Logger, args arguments) (finalError error) {
closeWithError := func(c io.Closer) {
if err := c.Close(); err != nil && finalError == nil {
finalError = err
}
}
context, err := loadContext(args.contexts, args.deepMerge)
if err != nil {
return err
}
if args.verbose {
l.Println("Calculated context:")
output, err := yaml_ghodss.Marshal(context)
if err != nil {
return err
}
l.Println(string(output))
}
var input io.ReadCloser
if args.templateFile == "-" {
input = os.Stdin
} else {
input, err = os.Open(args.templateFile)
if err != nil {
return err
}
defer closeWithError(input)
}
var out io.WriteCloser
if args.outputFile == "-" {
out = os.Stdout
} else {
out, err = os.Create(args.outputFile)
if err != nil {
return err
}
defer closeWithError(out)
}
var encoder *yaml_v2.Encoder
if args.yaml {
encoder = yaml_v2.NewEncoder(out)
defer closeWithError(encoder)
}
decoder := yaml_v2.NewDecoder(input)
for {
// json-e wants types as output by json, so we have to reach
// into the annoying ghodss/yaml code to do the type conversion.
// We can't use it directly (trivially), because it doesn't have
// multi-document support.
var passthroughTemplate interface{}
err := decoder.Decode(&passthroughTemplate)
if err == io.EOF {
return nil
}
if err != nil {
return err
}
var template interface{}
err = yaml_ghodss.YAMLTypesToJSONTypes(passthroughTemplate, &template)
if err != nil {
return err
}
output, err := jsone.Render(template, context)
if err != nil {
return err
}
if args.yaml {
err = encoder.Encode(output)
if err != nil {
return err
}
} else {
var byteOutput []byte
if args.indentation == 0 {
byteOutput, err = json.Marshal(output)
} else {
byteOutput, err = json.MarshalIndent(output, "", strings.Repeat(" ", args.indentation))
// MarshalIndent, sadly, doesn't add a newline at the end. Which I think it should.
byteOutput = append(byteOutput, 0x0a)
}
if err != nil {
return err
}
_, err = out.Write(byteOutput)
if err != nil {
return err
}
}
}
}
func loadContext(contexts []context, deepMerge bool) (map[string]interface{}, error) {
finalContext := make(map[string]interface{})
for _, context := range contexts {
untypedNewContext, err := context.eval()
if err != nil {
return nil, err
}
newContext, ok := untypedNewContext.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("context %s had no top level keys: %q", context.original, untypedNewContext)
}
if deepMerge {
err = mergo.Merge(&finalContext, newContext, mergo.WithOverride)
if err != nil {
return nil, err
}
} else {
for k, v := range newContext {
finalContext[k] = v
}
}
}
return finalContext, nil
}