Merge lp:~niemeyer/pyjuju/go-formula-config-validation into lp:pyjuju
- go-formula-config-validation
- Merge into trunk
Proposed by
Gustavo Niemeyer
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Kapil Thangavelu | ||||
Approved revision: | 16 | ||||
Merge reported by: | Gustavo Niemeyer | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~niemeyer/pyjuju/go-formula-config-validation | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
1654 lines (+1511/-0) 25 files modified
formula/Makefile (+25/-0) formula/config.go (+126/-0) formula/config_test.go (+153/-0) formula/export_test.go (+11/-0) formula/formula.go (+33/-0) formula/formula_test.go (+59/-0) formula/meta.go (+170/-0) formula/meta_test.go (+135/-0) formula/testrepo/dummy/.ignored (+1/-0) formula/testrepo/dummy/config.yaml (+5/-0) formula/testrepo/dummy/metadata.yaml (+7/-0) formula/testrepo/dummy/src/hello.c (+7/-0) formula/testrepo/mysql/metadata.yaml (+7/-0) formula/testrepo/mysql2/metadata.yaml (+11/-0) formula/testrepo/new/metadata.yaml (+7/-0) formula/testrepo/old/metadata.yaml (+7/-0) formula/testrepo/riak/metadata.yaml (+13/-0) formula/testrepo/varnish/metadata.yaml (+7/-0) formula/testrepo/varnish2/hooks/install (+3/-0) formula/testrepo/varnish2/metadata.yaml (+7/-0) formula/testrepo/wordpress/config.yaml (+3/-0) formula/testrepo/wordpress/metadata.yaml (+21/-0) schema/Makefile (+23/-0) schema/schema.go (+377/-0) schema/schema_test.go (+293/-0) |
||||
To merge this branch: | bzr merge lp:~niemeyer/pyjuju/go-formula-config-validation | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu (community) | Approve | ||
Review via email: mp+74311@code.launchpad.net |
Commit message
Description of the change
Formula config in Go port needs validation support
It currently implements parsing of the file, but misses
support for validating values.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'formula' |
2 | === added file 'formula/Makefile' |
3 | --- formula/Makefile 1970-01-01 00:00:00 +0000 |
4 | +++ formula/Makefile 2011-09-06 21:44:45 +0000 |
5 | @@ -0,0 +1,25 @@ |
6 | +include $(GOROOT)/src/Make.inc |
7 | + |
8 | +all: package |
9 | + |
10 | +TARG=launchpad.net/ensemble/go/formula |
11 | + |
12 | +GOFILES=\ |
13 | + config.go\ |
14 | + formula.go\ |
15 | + meta.go\ |
16 | + |
17 | +GOFMT=gofmt |
18 | +BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go)) |
19 | + |
20 | +gofmt: $(BADFMT) |
21 | + @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done |
22 | + |
23 | +ifneq ($(BADFMT),) |
24 | +ifneq ($(MAKECMDGOALS),gofmt) |
25 | +$(warning WARNING: make gofmt: $(BADFMT)) |
26 | +endif |
27 | +endif |
28 | + |
29 | +include $(GOROOT)/src/Make.pkg |
30 | + |
31 | |
32 | === added file 'formula/config.go' |
33 | --- formula/config.go 1970-01-01 00:00:00 +0000 |
34 | +++ formula/config.go 2011-09-06 21:44:45 +0000 |
35 | @@ -0,0 +1,126 @@ |
36 | +package formula |
37 | + |
38 | +import ( |
39 | + "fmt" |
40 | + "io/ioutil" |
41 | + "launchpad.net/ensemble/go/schema" |
42 | + "launchpad.net/goyaml" |
43 | + "os" |
44 | + "strconv" |
45 | +) |
46 | + |
47 | +// Option represents a single configuration option that is declared |
48 | +// as supported by a formula in its config.yaml file. |
49 | +type Option struct { |
50 | + Title string |
51 | + Description string |
52 | + Type string |
53 | + Default interface{} |
54 | +} |
55 | + |
56 | +// Config represents the supported configuration options for a formula, |
57 | +// as declared in its config.yaml file. |
58 | +type Config struct { |
59 | + Options map[string]Option |
60 | +} |
61 | + |
62 | +// ReadConfig reads a config.yaml file and returns its representation. |
63 | +func ReadConfig(path string) (config *Config, err os.Error) { |
64 | + data, err := ioutil.ReadFile(path) |
65 | + if err != nil { |
66 | + return |
67 | + } |
68 | + config, err = ParseConfig(data) |
69 | + if err != nil { |
70 | + err = os.NewError(fmt.Sprintf("%s: %s", path, err)) |
71 | + } |
72 | + return |
73 | +} |
74 | + |
75 | +// ParseConfig parses the content of a config.yaml file and returns |
76 | +// its representation. |
77 | +func ParseConfig(data []byte) (config *Config, err os.Error) { |
78 | + raw := make(map[interface{}]interface{}) |
79 | + err = goyaml.Unmarshal(data, raw) |
80 | + if err != nil { |
81 | + return |
82 | + } |
83 | + v, err := configSchema.Coerce(raw, nil) |
84 | + if err != nil { |
85 | + return |
86 | + } |
87 | + config = &Config{} |
88 | + config.Options = make(map[string]Option) |
89 | + m := v.(schema.MapType) |
90 | + for name, infov := range m["options"].(schema.MapType) { |
91 | + opt := infov.(schema.MapType) |
92 | + optTitle, _ := opt["title"].(string) |
93 | + optType, _ := opt["type"].(string) |
94 | + optDescr, _ := opt["description"].(string) |
95 | + optDefault, _ := opt["default"] |
96 | + config.Options[name.(string)] = Option{ |
97 | + Title: optTitle, |
98 | + Type: optType, |
99 | + Description: optDescr, |
100 | + Default: optDefault, |
101 | + } |
102 | + } |
103 | + return |
104 | +} |
105 | + |
106 | +// Validate processes the values in the input map according to the |
107 | +// configuration in config, doing the following operations: |
108 | +// |
109 | +// - Values are converted from strings to the types defined |
110 | +// - Options with default values are introduced for missing keys |
111 | +// - Unknown keys and badly typed values are reported as errors |
112 | +// |
113 | +func (c *Config) Validate(values map[string]string) (processed map[string]interface{}, err os.Error) { |
114 | + out := make(map[string]interface{}) |
115 | + for k, v := range values { |
116 | + opt, ok := c.Options[k] |
117 | + if !ok { |
118 | + return nil, os.NewError(fmt.Sprintf("Unknown configuration option: %q", k)) |
119 | + } |
120 | + switch opt.Type { |
121 | + case "string": |
122 | + out[k] = v |
123 | + case "int": |
124 | + i, err := strconv.Atoi64(v) |
125 | + if err != nil { |
126 | + return nil, os.NewError(fmt.Sprintf("Value for %q is not an int: %q", k, v)) |
127 | + } |
128 | + out[k] = i |
129 | + case "float": |
130 | + f, err := strconv.Atof64(v) |
131 | + if err != nil { |
132 | + return nil, os.NewError(fmt.Sprintf("Value for %q is not a float: %q", k, v)) |
133 | + } |
134 | + out[k] = f |
135 | + default: |
136 | + panic(fmt.Sprintf("Internal error: option type %q is unknown to Validate", opt.Type)) |
137 | + } |
138 | + } |
139 | + for k, opt := range c.Options { |
140 | + if _, ok := out[k]; !ok && opt.Default != nil { |
141 | + out[k] = opt.Default |
142 | + } |
143 | + } |
144 | + return out, nil |
145 | +} |
146 | + |
147 | +var optionSchema = schema.FieldMap( |
148 | + schema.Fields{ |
149 | + "type": schema.OneOf(schema.Const("string"), schema.Const("int"), schema.Const("float")), |
150 | + "default": schema.OneOf(schema.String(), schema.Int(), schema.Float()), |
151 | + "description": schema.String(), |
152 | + }, |
153 | + schema.Optional{"default", "description"}, |
154 | +) |
155 | + |
156 | +var configSchema = schema.FieldMap( |
157 | + schema.Fields{ |
158 | + "options": schema.Map(schema.String(), optionSchema), |
159 | + }, |
160 | + nil, |
161 | +) |
162 | |
163 | === added file 'formula/config_test.go' |
164 | --- formula/config_test.go 1970-01-01 00:00:00 +0000 |
165 | +++ formula/config_test.go 2011-09-06 21:44:45 +0000 |
166 | @@ -0,0 +1,153 @@ |
167 | +package formula_test |
168 | + |
169 | +import ( |
170 | + "io/ioutil" |
171 | + . "launchpad.net/gocheck" |
172 | + "launchpad.net/ensemble/go/formula" |
173 | + "path/filepath" |
174 | +) |
175 | + |
176 | +var sampleConfig = ` |
177 | +options: |
178 | + title: |
179 | + default: My Title |
180 | + description: A descriptive title used for the service. |
181 | + type: string |
182 | + outlook: |
183 | + description: No default outlook. |
184 | + type: string |
185 | + username: |
186 | + default: admin001 |
187 | + description: The name of the initial account (given admin permissions). |
188 | + type: string |
189 | + skill-level: |
190 | + description: A number indicating skill. |
191 | + type: int |
192 | + agility-ratio: |
193 | + description: A number from 0 to 1 indicating agility. |
194 | + type: float |
195 | +` |
196 | + |
197 | +func repoConfig(name string) (path string) { |
198 | + return filepath.Join("testrepo", name, "config.yaml") |
199 | +} |
200 | + |
201 | +func assertDummyConfig(c *C, config *formula.Config) { |
202 | + c.Assert(config.Options["title"], Equals, |
203 | + formula.Option{ |
204 | + Default: "My Title", |
205 | + Description: "A descriptive title used for the service.", |
206 | + Type: "string", |
207 | + }, |
208 | + ) |
209 | +} |
210 | + |
211 | +func (s *S) TestReadConfig(c *C) { |
212 | + config, err := formula.ReadConfig(repoConfig("dummy")) |
213 | + c.Assert(err, IsNil) |
214 | + assertDummyConfig(c, config) |
215 | +} |
216 | + |
217 | +func (s *S) TestParseConfig(c *C) { |
218 | + data, err := ioutil.ReadFile(repoConfig("dummy")) |
219 | + c.Assert(err, IsNil) |
220 | + |
221 | + config, err := formula.ParseConfig(data) |
222 | + c.Assert(err, IsNil) |
223 | + assertDummyConfig(c, config) |
224 | +} |
225 | + |
226 | +func (s *S) TestConfigErrorWithPath(c *C) { |
227 | + path := filepath.Join(c.MkDir(), "mymeta.yaml") |
228 | + |
229 | + _, err := formula.ReadConfig(path) |
230 | + c.Assert(err, Matches, `.*/.*/mymeta\.yaml.*no such file.*`) |
231 | + |
232 | + data := `options: {t: {type: foo}}` |
233 | + err = ioutil.WriteFile(path, []byte(data), 0644) |
234 | + c.Assert(err, IsNil) |
235 | + |
236 | + _, err = formula.ReadConfig(path) |
237 | + c.Assert(err, Matches, `/.*/mymeta\.yaml: options.t.type: unsupported value`) |
238 | +} |
239 | + |
240 | +func (s *S) TestParseSample(c *C) { |
241 | + config, err := formula.ParseConfig([]byte(sampleConfig)) |
242 | + c.Assert(err, IsNil) |
243 | + |
244 | + opt := config.Options |
245 | + c.Assert(opt["title"], Equals, |
246 | + formula.Option{ |
247 | + Default: "My Title", |
248 | + Description: "A descriptive title used for the service.", |
249 | + Type: "string", |
250 | + }, |
251 | + ) |
252 | + c.Assert(opt["outlook"], Equals, |
253 | + formula.Option{ |
254 | + Description: "No default outlook.", |
255 | + Type: "string", |
256 | + }, |
257 | + ) |
258 | + c.Assert(opt["username"], Equals, |
259 | + formula.Option{ |
260 | + Default: "admin001", |
261 | + Description: "The name of the initial account (given admin permissions).", |
262 | + Type: "string", |
263 | + }, |
264 | + ) |
265 | + c.Assert(opt["skill-level"], Equals, |
266 | + formula.Option{ |
267 | + Description: "A number indicating skill.", |
268 | + Type: "int", |
269 | + }, |
270 | + ) |
271 | +} |
272 | + |
273 | +func (s *S) TestValidate(c *C) { |
274 | + config, err := formula.ParseConfig([]byte(sampleConfig)) |
275 | + c.Assert(err, IsNil) |
276 | + |
277 | + input := map[string]string{ |
278 | + "title": "Helpful Title", |
279 | + "outlook": "Peachy", |
280 | + } |
281 | + |
282 | + // This should include an overridden value, a default and a new value. |
283 | + expected := map[string]interface{}{ |
284 | + "title": "Helpful Title", |
285 | + "outlook": "Peachy", |
286 | + "username": "admin001", |
287 | + } |
288 | + |
289 | + output, err := config.Validate(input) |
290 | + c.Assert(err, IsNil) |
291 | + c.Assert(output, Equals, expected) |
292 | + |
293 | + // Check whether float conversion is working. |
294 | + input["agility-ratio"] = "0.5" |
295 | + input["skill-level"] = "7" |
296 | + expected["agility-ratio"] = 0.5 |
297 | + expected["skill-level"] = int64(7) |
298 | + output, err = config.Validate(input) |
299 | + c.Assert(err, IsNil) |
300 | + c.Assert(output, Equals, expected) |
301 | + |
302 | + // Check whether float errors are caught. |
303 | + input["agility-ratio"] = "foo" |
304 | + output, err = config.Validate(input) |
305 | + c.Assert(err, Matches, `Value for "agility-ratio" is not a float: "foo"`) |
306 | + input["agility-ratio"] = "0.5" |
307 | + |
308 | + // Check whether int errors are caught. |
309 | + input["skill-level"] = "foo" |
310 | + output, err = config.Validate(input) |
311 | + c.Assert(err, Matches, `Value for "skill-level" is not an int: "foo"`) |
312 | + input["skill-level"] = "7" |
313 | + |
314 | + // Now try to set a value outside the expected. |
315 | + input["bad"] = "value" |
316 | + output, err = config.Validate(input) |
317 | + c.Assert(output, IsNil) |
318 | + c.Assert(err, Matches, `Unknown configuration option: "bad"`) |
319 | +} |
320 | |
321 | === added file 'formula/export_test.go' |
322 | --- formula/export_test.go 1970-01-01 00:00:00 +0000 |
323 | +++ formula/export_test.go 2011-09-06 21:44:45 +0000 |
324 | @@ -0,0 +1,11 @@ |
325 | +package formula |
326 | + |
327 | +import ( |
328 | + "launchpad.net/ensemble/go/schema" |
329 | +) |
330 | + |
331 | +// Export meaningful bits for tests only. |
332 | + |
333 | +func IfaceExpander(limit interface{}) schema.Checker { |
334 | + return ifaceExpander(limit) |
335 | +} |
336 | |
337 | === added file 'formula/formula.go' |
338 | --- formula/formula.go 1970-01-01 00:00:00 +0000 |
339 | +++ formula/formula.go 2011-09-06 21:44:45 +0000 |
340 | @@ -0,0 +1,33 @@ |
341 | +package formula |
342 | + |
343 | +import ( |
344 | + "fmt" |
345 | + "os" |
346 | + "strconv" |
347 | + "strings" |
348 | +) |
349 | + |
350 | +func errorf(format string, args ...interface{}) os.Error { |
351 | + return os.NewError(fmt.Sprintf(format, args...)) |
352 | +} |
353 | + |
354 | +// ParseId splits a formula identifier into its constituting parts. |
355 | +func ParseId(id string) (namespace string, name string, rev int, err os.Error) { |
356 | + colon := strings.Index(id, ":") |
357 | + if colon == -1 { |
358 | + err = errorf("Missing formula namespace: %q", id) |
359 | + return |
360 | + } |
361 | + dash := strings.LastIndex(id, "-") |
362 | + if dash != -1 { |
363 | + rev, err = strconv.Atoi(id[dash+1:]) |
364 | + } |
365 | + if dash == -1 || err != nil { |
366 | + err = errorf("Missing formula revision: %q", id) |
367 | + return |
368 | + } |
369 | + namespace = id[:colon] |
370 | + name = id[colon+1 : dash] |
371 | + return |
372 | +} |
373 | + |
374 | |
375 | === added file 'formula/formula_test.go' |
376 | --- formula/formula_test.go 1970-01-01 00:00:00 +0000 |
377 | +++ formula/formula_test.go 2011-09-06 21:44:45 +0000 |
378 | @@ -0,0 +1,59 @@ |
379 | +package formula_test |
380 | + |
381 | +import ( |
382 | + "io/ioutil" |
383 | + "testing" |
384 | + . "launchpad.net/gocheck" |
385 | + "launchpad.net/ensemble/go/formula" |
386 | + "launchpad.net/goyaml" |
387 | +) |
388 | + |
389 | + |
390 | +func Test(t *testing.T) { |
391 | + TestingT(t) |
392 | +} |
393 | + |
394 | +type S struct{} |
395 | + |
396 | +var _ = Suite(&S{}) |
397 | + |
398 | +func (s *S) TestParseId(c *C) { |
399 | + namespace, name, rev, err := formula.ParseId("local:mysql-21") |
400 | + c.Assert(err, IsNil) |
401 | + c.Assert(namespace, Equals, "local") |
402 | + c.Assert(name, Equals, "mysql") |
403 | + c.Assert(rev, Equals, 21) |
404 | + |
405 | + namespace, name, rev, err = formula.ParseId("local:mysql-cluster-21") |
406 | + c.Assert(err, IsNil) |
407 | + c.Assert(namespace, Equals, "local") |
408 | + c.Assert(name, Equals, "mysql-cluster") |
409 | + c.Assert(rev, Equals, 21) |
410 | + |
411 | + _, _, _, err = formula.ParseId("foo") |
412 | + c.Assert(err, Matches, `Missing formula namespace: "foo"`) |
413 | + |
414 | + _, _, _, err = formula.ParseId("local:foo-x") |
415 | + c.Assert(err, Matches, `Missing formula revision: "local:foo-x"`) |
416 | +} |
417 | + |
418 | +func ReadYaml(path string) map[interface{}]interface{} { |
419 | + data, err := ioutil.ReadFile(path) |
420 | + if err != nil { |
421 | + panic(err) |
422 | + } |
423 | + m := make(map[interface{}]interface{}) |
424 | + err = goyaml.Unmarshal(data, m) |
425 | + if err != nil { |
426 | + panic(err) |
427 | + } |
428 | + return m |
429 | +} |
430 | + |
431 | +func DumpYaml(v interface{}) []byte { |
432 | + data, err := goyaml.Marshal(v) |
433 | + if err != nil { |
434 | + panic(err) |
435 | + } |
436 | + return data |
437 | +} |
438 | |
439 | === added file 'formula/meta.go' |
440 | --- formula/meta.go 1970-01-01 00:00:00 +0000 |
441 | +++ formula/meta.go 2011-09-06 21:44:45 +0000 |
442 | @@ -0,0 +1,170 @@ |
443 | +package formula |
444 | + |
445 | +import ( |
446 | + "fmt" |
447 | + "io/ioutil" |
448 | + "launchpad.net/ensemble/go/schema" |
449 | + "launchpad.net/goyaml" |
450 | + "os" |
451 | +) |
452 | + |
453 | +// Relation represents a single relation defined in the formula |
454 | +// metadata.yaml file. |
455 | +type Relation struct { |
456 | + Interface string |
457 | + Optional bool |
458 | + Limit int |
459 | +} |
460 | + |
461 | +// Meta represents all the known content that may be defined |
462 | +// within a formula's metadata.yaml file. |
463 | +type Meta struct { |
464 | + Name string |
465 | + Revision int |
466 | + Summary string |
467 | + Description string |
468 | + Provides map[string]Relation |
469 | + Requires map[string]Relation |
470 | + Peers map[string]Relation |
471 | +} |
472 | + |
473 | +// ReadMeta reads a metadata.yaml file and returns its representation. |
474 | +func ReadMeta(path string) (meta *Meta, err os.Error) { |
475 | + data, err := ioutil.ReadFile(path) |
476 | + if err != nil { |
477 | + return |
478 | + } |
479 | + meta, err = ParseMeta(data) |
480 | + if err != nil { |
481 | + err = os.NewError(fmt.Sprintf("%s: %s", path, err)) |
482 | + } |
483 | + return |
484 | +} |
485 | + |
486 | +// ParseMeta parses the data of a metadata.yaml file and returns |
487 | +// its representation. |
488 | +func ParseMeta(data []byte) (meta *Meta, err os.Error) { |
489 | + raw := make(map[interface{}]interface{}) |
490 | + err = goyaml.Unmarshal(data, raw) |
491 | + if err != nil { |
492 | + return |
493 | + } |
494 | + v, err := formulaSchema.Coerce(raw, nil) |
495 | + if err != nil { |
496 | + return |
497 | + } |
498 | + m := v.(schema.MapType) |
499 | + meta = &Meta{} |
500 | + meta.Name = m["name"].(string) |
501 | + // Schema decodes as int64, but the int range should be good |
502 | + // enough for revisions. |
503 | + meta.Revision = int(m["revision"].(int64)) |
504 | + meta.Summary = m["summary"].(string) |
505 | + meta.Description = m["description"].(string) |
506 | + meta.Provides = parseRelations(m["provides"]) |
507 | + meta.Requires = parseRelations(m["requires"]) |
508 | + meta.Peers = parseRelations(m["peers"]) |
509 | + return |
510 | +} |
511 | + |
512 | +func parseRelations(relations interface{}) map[string]Relation { |
513 | + if relations == nil { |
514 | + return nil |
515 | + } |
516 | + result := make(map[string]Relation) |
517 | + for name, rel := range relations.(schema.MapType) { |
518 | + relMap := rel.(schema.MapType) |
519 | + relation := Relation{} |
520 | + relation.Interface = relMap["interface"].(string) |
521 | + relation.Optional = relMap["optional"].(bool) |
522 | + if relMap["limit"] != nil { |
523 | + // Schema defaults to int64, but we know |
524 | + // the int range should be more than enough. |
525 | + relation.Limit = int(relMap["limit"].(int64)) |
526 | + } |
527 | + result[name.(string)] = relation |
528 | + } |
529 | + return result |
530 | +} |
531 | + |
532 | +// Schema coercer that expands the interface shorthand notation. |
533 | +// A consistent format is easier to work with than considering the |
534 | +// potential difference everywhere. |
535 | +// |
536 | +// Supports the following variants:: |
537 | +// |
538 | +// provides: |
539 | +// server: riak |
540 | +// admin: http |
541 | +// foobar: |
542 | +// interface: blah |
543 | +// |
544 | +// provides: |
545 | +// server: |
546 | +// interface: mysql |
547 | +// limit: |
548 | +// optional: false |
549 | +// |
550 | +// In all input cases, the output is the fully specified interface |
551 | +// representation as seen in the mysql interface description above. |
552 | +func ifaceExpander(limit interface{}) schema.Checker { |
553 | + return ifaceExpC{limit} |
554 | +} |
555 | + |
556 | +type ifaceExpC struct { |
557 | + limit interface{} |
558 | +} |
559 | + |
560 | +var ( |
561 | + stringC = schema.String() |
562 | + mapC = schema.Map(schema.String(), schema.Any()) |
563 | +) |
564 | + |
565 | +func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err os.Error) { |
566 | + s, err := stringC.Coerce(v, path) |
567 | + if err == nil { |
568 | + newv = schema.MapType{ |
569 | + "interface": s, |
570 | + "limit": c.limit, |
571 | + "optional": false, |
572 | + } |
573 | + return |
574 | + } |
575 | + |
576 | + // Optional values are context-sensitive and/or have |
577 | + // defaults, which is different than what KeyDict can |
578 | + // readily support. So just do it here first, then |
579 | + // coerce to the real schema. |
580 | + v, err = mapC.Coerce(v, path) |
581 | + if err != nil { |
582 | + return |
583 | + } |
584 | + m := v.(schema.MapType) |
585 | + if _, ok := m["limit"]; !ok { |
586 | + m["limit"] = c.limit |
587 | + } |
588 | + if _, ok := m["optional"]; !ok { |
589 | + m["optional"] = false |
590 | + } |
591 | + return ifaceSchema.Coerce(m, path) |
592 | +} |
593 | + |
594 | +var ifaceSchema = schema.FieldMap(schema.Fields{ |
595 | + "interface": schema.String(), |
596 | + "limit": schema.OneOf(schema.Const(nil), schema.Int()), |
597 | + "optional": schema.Bool(), |
598 | +}, nil) |
599 | + |
600 | +var formulaSchema = schema.FieldMap( |
601 | + schema.Fields{ |
602 | + "ensemble": schema.Const("formula"), |
603 | + "name": schema.String(), |
604 | + "revision": schema.Int(), |
605 | + "summary": schema.String(), |
606 | + "description": schema.String(), |
607 | + "peers": schema.Map(schema.String(), ifaceExpander(1)), |
608 | + "provides": schema.Map(schema.String(), ifaceExpander(nil)), |
609 | + "requires": schema.Map(schema.String(), ifaceExpander(1)), |
610 | + }, |
611 | + schema.Optional{"provides", "requires", "peers"}, |
612 | +) |
613 | |
614 | === added file 'formula/meta_test.go' |
615 | --- formula/meta_test.go 1970-01-01 00:00:00 +0000 |
616 | +++ formula/meta_test.go 2011-09-06 21:44:45 +0000 |
617 | @@ -0,0 +1,135 @@ |
618 | +package formula_test |
619 | + |
620 | +import ( |
621 | + "io/ioutil" |
622 | + . "launchpad.net/gocheck" |
623 | + "launchpad.net/ensemble/go/formula" |
624 | + "launchpad.net/ensemble/go/schema" |
625 | + "path/filepath" |
626 | +) |
627 | + |
628 | +func repoMeta(name string) (path string) { |
629 | + return filepath.Join("testrepo", name, "metadata.yaml") |
630 | +} |
631 | + |
632 | +func (s *S) TestReadMeta(c *C) { |
633 | + meta, err := formula.ReadMeta(repoMeta("dummy")) |
634 | + c.Assert(err, IsNil) |
635 | + c.Assert(meta.Name, Equals, "dummy") |
636 | + c.Assert(meta.Revision, Equals, 1) |
637 | + c.Assert(meta.Summary, Equals, "That's a dummy formula.") |
638 | + c.Assert(meta.Description, Equals, |
639 | + "This is a longer description which\npotentially contains multiple lines.\n") |
640 | +} |
641 | + |
642 | +func (s *S) TestParseMeta(c *C) { |
643 | + data, err := ioutil.ReadFile(repoMeta("dummy")) |
644 | + c.Assert(err, IsNil) |
645 | + |
646 | + meta, err := formula.ParseMeta(data) |
647 | + c.Assert(err, IsNil) |
648 | + c.Assert(meta.Name, Equals, "dummy") |
649 | + c.Assert(meta.Revision, Equals, 1) |
650 | + c.Assert(meta.Summary, Equals, "That's a dummy formula.") |
651 | + c.Assert(meta.Description, Equals, |
652 | + "This is a longer description which\npotentially contains multiple lines.\n") |
653 | +} |
654 | + |
655 | +func (s *S) TestMetaHeader(c *C) { |
656 | + yaml := ReadYaml(repoMeta("dummy")) |
657 | + yaml["ensemble"] = "foo" |
658 | + data := DumpYaml(yaml) |
659 | + |
660 | + _, err := formula.ParseMeta(data) |
661 | + c.Assert(err, Matches, `ensemble: expected "formula", got "foo"`) |
662 | +} |
663 | + |
664 | +func (s *S) TestMetaErrorWithPath(c *C) { |
665 | + yaml := ReadYaml(repoMeta("dummy")) |
666 | + yaml["ensemble"] = "foo" |
667 | + data := DumpYaml(yaml) |
668 | + |
669 | + path := filepath.Join(c.MkDir(), "mymeta.yaml") |
670 | + |
671 | + _, err := formula.ReadMeta(path) |
672 | + c.Assert(err, Matches, `.*/.*/mymeta\.yaml.*no such file.*`) |
673 | + |
674 | + err = ioutil.WriteFile(path, data, 0644) |
675 | + c.Assert(err, IsNil) |
676 | + |
677 | + _, err = formula.ReadMeta(path) |
678 | + c.Assert(err, Matches, `/.*/mymeta\.yaml: ensemble: expected "formula", got "foo"`) |
679 | +} |
680 | + |
681 | +func (s *S) TestParseMetaRelations(c *C) { |
682 | + meta, err := formula.ReadMeta(repoMeta("mysql")) |
683 | + c.Assert(err, IsNil) |
684 | + c.Assert(meta.Provides["server"], Equals, formula.Relation{Interface: "mysql"}) |
685 | + c.Assert(meta.Requires, IsNil) |
686 | + c.Assert(meta.Peers, IsNil) |
687 | + |
688 | + meta, err = formula.ReadMeta(repoMeta("riak")) |
689 | + c.Assert(err, IsNil) |
690 | + c.Assert(meta.Provides["endpoint"], Equals, formula.Relation{Interface: "http"}) |
691 | + c.Assert(meta.Provides["admin"], Equals, formula.Relation{Interface: "http"}) |
692 | + c.Assert(meta.Peers["ring"], Equals, formula.Relation{Interface: "riak", Limit: 1}) |
693 | + c.Assert(meta.Requires, IsNil) |
694 | + |
695 | + meta, err = formula.ReadMeta(repoMeta("wordpress")) |
696 | + c.Assert(err, IsNil) |
697 | + c.Assert(meta.Provides["url"], Equals, formula.Relation{Interface: "http"}) |
698 | + c.Assert(meta.Requires["db"], Equals, formula.Relation{Interface: "mysql", Limit: 1}) |
699 | + c.Assert(meta.Requires["cache"], Equals, formula.Relation{Interface: "varnish", Limit: 2, Optional: true}) |
700 | + c.Assert(meta.Peers, IsNil) |
701 | + |
702 | +} |
703 | + |
704 | +// Test rewriting of a given interface specification into long form. |
705 | +// |
706 | +// InterfaceExpander uses `coerce` to do one of two things: |
707 | +// |
708 | +// - Rewrite shorthand to the long form used for actual storage |
709 | +// - Fills in defaults, including a configurable `limit` |
710 | +// |
711 | +// This test ensures test coverage on each of these branches, along |
712 | +// with ensuring the conversion object properly raises SchemaError |
713 | +// exceptions on invalid data. |
714 | +func (s *S) TestIfaceExpander(c *C) { |
715 | + e := formula.IfaceExpander(nil) |
716 | + |
717 | + path := []string{"<pa", "th>"} |
718 | + |
719 | + // Shorthand is properly rewritten |
720 | + v, err := e.Coerce("http", path) |
721 | + c.Assert(err, IsNil) |
722 | + c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false}) |
723 | + |
724 | + // Defaults are properly applied |
725 | + v, err = e.Coerce(schema.MapType{"interface": "http"}, path) |
726 | + c.Assert(err, IsNil) |
727 | + c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false}) |
728 | + |
729 | + v, err = e.Coerce(schema.MapType{"interface": "http", "limit": 2}, path) |
730 | + c.Assert(err, IsNil) |
731 | + c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(2), "optional": false}) |
732 | + |
733 | + v, err = e.Coerce(schema.MapType{"interface": "http", "optional": true}, path) |
734 | + c.Assert(err, IsNil) |
735 | + c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": true}) |
736 | + |
737 | + // Invalid data raises an error. |
738 | + v, err = e.Coerce(42, path) |
739 | + c.Assert(err, Matches, "<path>: expected map, got 42") |
740 | + |
741 | + v, err = e.Coerce(schema.MapType{"interface": "http", "optional": nil}, path) |
742 | + c.Assert(err, Matches, "<path>.optional: expected bool, got nothing") |
743 | + |
744 | + v, err = e.Coerce(schema.MapType{"interface": "http", "limit": "none, really"}, path) |
745 | + c.Assert(err, Matches, "<path>.limit: unsupported value") |
746 | + |
747 | + // Can change default limit |
748 | + e = formula.IfaceExpander(1) |
749 | + v, err = e.Coerce(schema.MapType{"interface": "http"}, path) |
750 | + c.Assert(err, IsNil) |
751 | + c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(1), "optional": false}) |
752 | +} |
753 | |
754 | === added directory 'formula/testrepo' |
755 | === added directory 'formula/testrepo/dummy' |
756 | === added file 'formula/testrepo/dummy/.ignored' |
757 | --- formula/testrepo/dummy/.ignored 1970-01-01 00:00:00 +0000 |
758 | +++ formula/testrepo/dummy/.ignored 2011-09-06 21:44:45 +0000 |
759 | @@ -0,0 +1,1 @@ |
760 | +# |
761 | \ No newline at end of file |
762 | |
763 | === added directory 'formula/testrepo/dummy/build' |
764 | === added file 'formula/testrepo/dummy/build/ignored' |
765 | === added file 'formula/testrepo/dummy/config.yaml' |
766 | --- formula/testrepo/dummy/config.yaml 1970-01-01 00:00:00 +0000 |
767 | +++ formula/testrepo/dummy/config.yaml 2011-09-06 21:44:45 +0000 |
768 | @@ -0,0 +1,5 @@ |
769 | +options: |
770 | + title: {default: My Title, description: A descriptive title used for the service., type: string} |
771 | + outlook: {description: No default outlook., type: string} |
772 | + username: {default: admin001, description: The name of the initial account (given admin permissions)., type: string} |
773 | + skill-level: {description: A number indicating skill., type: int} |
774 | |
775 | === added directory 'formula/testrepo/dummy/empty' |
776 | === added file 'formula/testrepo/dummy/metadata.yaml' |
777 | --- formula/testrepo/dummy/metadata.yaml 1970-01-01 00:00:00 +0000 |
778 | +++ formula/testrepo/dummy/metadata.yaml 2011-09-06 21:44:45 +0000 |
779 | @@ -0,0 +1,7 @@ |
780 | +ensemble: formula |
781 | +name: dummy |
782 | +revision: 1 |
783 | +summary: "That's a dummy formula." |
784 | +description: | |
785 | + This is a longer description which |
786 | + potentially contains multiple lines. |
787 | |
788 | === added directory 'formula/testrepo/dummy/src' |
789 | === added file 'formula/testrepo/dummy/src/hello.c' |
790 | --- formula/testrepo/dummy/src/hello.c 1970-01-01 00:00:00 +0000 |
791 | +++ formula/testrepo/dummy/src/hello.c 2011-09-06 21:44:45 +0000 |
792 | @@ -0,0 +1,7 @@ |
793 | +#include <stdio.h> |
794 | + |
795 | +main() |
796 | +{ |
797 | + printf ("Hello World!\n"); |
798 | + return 0; |
799 | +} |
800 | |
801 | === added directory 'formula/testrepo/mysql' |
802 | === added file 'formula/testrepo/mysql/metadata.yaml' |
803 | --- formula/testrepo/mysql/metadata.yaml 1970-01-01 00:00:00 +0000 |
804 | +++ formula/testrepo/mysql/metadata.yaml 2011-09-06 21:44:45 +0000 |
805 | @@ -0,0 +1,7 @@ |
806 | +ensemble: formula |
807 | +name: mysql |
808 | +revision: 1 |
809 | +summary: "Database engine" |
810 | +description: "A pretty popular database" |
811 | +provides: |
812 | + server: mysql |
813 | |
814 | === added directory 'formula/testrepo/mysql2' |
815 | === added file 'formula/testrepo/mysql2/metadata.yaml' |
816 | --- formula/testrepo/mysql2/metadata.yaml 1970-01-01 00:00:00 +0000 |
817 | +++ formula/testrepo/mysql2/metadata.yaml 2011-09-06 21:44:45 +0000 |
818 | @@ -0,0 +1,11 @@ |
819 | +ensemble: formula |
820 | +name: mysql2 |
821 | +revision: 1 |
822 | +summary: "Database engine" |
823 | +description: "A pretty popular database" |
824 | +provides: |
825 | + prod: |
826 | + interface: mysql |
827 | + dev: |
828 | + interface: mysql |
829 | + limit: 2 |
830 | |
831 | === added directory 'formula/testrepo/new' |
832 | === added file 'formula/testrepo/new/metadata.yaml' |
833 | --- formula/testrepo/new/metadata.yaml 1970-01-01 00:00:00 +0000 |
834 | +++ formula/testrepo/new/metadata.yaml 2011-09-06 21:44:45 +0000 |
835 | @@ -0,0 +1,7 @@ |
836 | +ensemble: formula |
837 | +name: sample |
838 | +revision: 2 |
839 | +summary: "That's a sample formula." |
840 | +description: | |
841 | + This is a longer description which |
842 | + potentially contains multiple lines. |
843 | |
844 | === added directory 'formula/testrepo/old' |
845 | === added file 'formula/testrepo/old/metadata.yaml' |
846 | --- formula/testrepo/old/metadata.yaml 1970-01-01 00:00:00 +0000 |
847 | +++ formula/testrepo/old/metadata.yaml 2011-09-06 21:44:45 +0000 |
848 | @@ -0,0 +1,7 @@ |
849 | +ensemble: formula |
850 | +name: sample |
851 | +revision: 1 |
852 | +summary: "That's a sample formula." |
853 | +description: | |
854 | + This is a longer description which |
855 | + potentially contains multiple lines. |
856 | |
857 | === added directory 'formula/testrepo/riak' |
858 | === added file 'formula/testrepo/riak/metadata.yaml' |
859 | --- formula/testrepo/riak/metadata.yaml 1970-01-01 00:00:00 +0000 |
860 | +++ formula/testrepo/riak/metadata.yaml 2011-09-06 21:44:45 +0000 |
861 | @@ -0,0 +1,13 @@ |
862 | +ensemble: formula |
863 | +name: riak |
864 | +revision: 7 |
865 | +summary: "K/V storage engine" |
866 | +description: "Scalable K/V Store in Erlang with Clocks :-)" |
867 | +provides: |
868 | + endpoint: |
869 | + interface: http |
870 | + admin: |
871 | + interface: http |
872 | +peers: |
873 | + ring: |
874 | + interface: riak |
875 | |
876 | === added directory 'formula/testrepo/varnish' |
877 | === added file 'formula/testrepo/varnish/metadata.yaml' |
878 | --- formula/testrepo/varnish/metadata.yaml 1970-01-01 00:00:00 +0000 |
879 | +++ formula/testrepo/varnish/metadata.yaml 2011-09-06 21:44:45 +0000 |
880 | @@ -0,0 +1,7 @@ |
881 | +ensemble: formula |
882 | +name: varnish |
883 | +revision: 1 |
884 | +summary: "Database engine" |
885 | +description: "Another popular database" |
886 | +provides: |
887 | + webcache: varnish |
888 | |
889 | === added directory 'formula/testrepo/varnish2' |
890 | === added directory 'formula/testrepo/varnish2/hooks' |
891 | === added file 'formula/testrepo/varnish2/hooks/install' |
892 | --- formula/testrepo/varnish2/hooks/install 1970-01-01 00:00:00 +0000 |
893 | +++ formula/testrepo/varnish2/hooks/install 2011-09-06 21:44:45 +0000 |
894 | @@ -0,0 +1,3 @@ |
895 | +#!/bin/bash |
896 | + |
897 | +echo hello world |
898 | \ No newline at end of file |
899 | |
900 | === added file 'formula/testrepo/varnish2/metadata.yaml' |
901 | --- formula/testrepo/varnish2/metadata.yaml 1970-01-01 00:00:00 +0000 |
902 | +++ formula/testrepo/varnish2/metadata.yaml 2011-09-06 21:44:45 +0000 |
903 | @@ -0,0 +1,7 @@ |
904 | +ensemble: formula |
905 | +name: varnish |
906 | +revision: 1 |
907 | +summary: "Database engine" |
908 | +description: "Another popular database" |
909 | +provides: |
910 | + webcache: varnish |
911 | |
912 | === added directory 'formula/testrepo/wordpress' |
913 | === added file 'formula/testrepo/wordpress/config.yaml' |
914 | --- formula/testrepo/wordpress/config.yaml 1970-01-01 00:00:00 +0000 |
915 | +++ formula/testrepo/wordpress/config.yaml 2011-09-06 21:44:45 +0000 |
916 | @@ -0,0 +1,3 @@ |
917 | +options: |
918 | + blog-title: {default: My Title, description: A descriptive title used for the blog., type: string} |
919 | + |
920 | |
921 | === added file 'formula/testrepo/wordpress/metadata.yaml' |
922 | --- formula/testrepo/wordpress/metadata.yaml 1970-01-01 00:00:00 +0000 |
923 | +++ formula/testrepo/wordpress/metadata.yaml 2011-09-06 21:44:45 +0000 |
924 | @@ -0,0 +1,21 @@ |
925 | +ensemble: formula |
926 | +name: wordpress |
927 | +revision: 3 |
928 | +summary: "Blog engine" |
929 | +description: "A pretty popular blog engine" |
930 | +provides: |
931 | + url: |
932 | + interface: http |
933 | + limit: |
934 | + optional: false |
935 | +requires: |
936 | + db: |
937 | + interface: mysql |
938 | + limit: 1 |
939 | + optional: false |
940 | + cache: |
941 | + interface: varnish |
942 | + limit: 2 |
943 | + optional: true |
944 | + |
945 | + |
946 | |
947 | === added directory 'schema' |
948 | === added file 'schema/Makefile' |
949 | --- schema/Makefile 1970-01-01 00:00:00 +0000 |
950 | +++ schema/Makefile 2011-09-06 21:44:45 +0000 |
951 | @@ -0,0 +1,23 @@ |
952 | +include $(GOROOT)/src/Make.inc |
953 | + |
954 | +all: package |
955 | + |
956 | +TARG=launchpad.net/ensemble/go/schema |
957 | + |
958 | +GOFILES=\ |
959 | + schema.go\ |
960 | + |
961 | +GOFMT=gofmt |
962 | +BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go)) |
963 | + |
964 | +gofmt: $(BADFMT) |
965 | + @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done |
966 | + |
967 | +ifneq ($(BADFMT),) |
968 | +ifneq ($(MAKECMDGOALS),gofmt) |
969 | +$(warning WARNING: make gofmt: $(BADFMT)) |
970 | +endif |
971 | +endif |
972 | + |
973 | +include $(GOROOT)/src/Make.pkg |
974 | + |
975 | |
976 | === added file 'schema/schema.go' |
977 | --- schema/schema.go 1970-01-01 00:00:00 +0000 |
978 | +++ schema/schema.go 2011-09-06 21:44:45 +0000 |
979 | @@ -0,0 +1,377 @@ |
980 | +package schema |
981 | + |
982 | +import ( |
983 | + "fmt" |
984 | + "os" |
985 | + "reflect" |
986 | + "regexp" |
987 | + "strconv" |
988 | + "strings" |
989 | +) |
990 | + |
991 | +// All map types used in the schema package are of type MapType. |
992 | +type MapType map[interface{}]interface{} |
993 | + |
994 | +// All the slice types generated in the schema package are of type ListType. |
995 | +type ListType []interface{} |
996 | + |
997 | +// The Coerce method of the Checker interface is called recursively when |
998 | +// v is being validated. If err is nil, newv is used as the new value |
999 | +// at the recursion point. If err is non-nil, v is taken as invalid and |
1000 | +// may be either ignored or error out depending on where in the schema |
1001 | +// checking process the error happened. Checkers like OneOf may continue |
1002 | +// with an alternative, for instance. |
1003 | +type Checker interface { |
1004 | + Coerce(v interface{}, path []string) (newv interface{}, err os.Error) |
1005 | +} |
1006 | + |
1007 | +type error struct { |
1008 | + want string |
1009 | + got interface{} |
1010 | + path []string |
1011 | +} |
1012 | + |
1013 | +func (e error) String() string { |
1014 | + var path string |
1015 | + if e.path[0] == "." { |
1016 | + path = strings.Join(e.path[1:], "") |
1017 | + } else { |
1018 | + path = strings.Join(e.path, "") |
1019 | + } |
1020 | + if e.want == "" { |
1021 | + return fmt.Sprintf("%s: unsupported value", path) |
1022 | + } |
1023 | + if e.got == nil { |
1024 | + return fmt.Sprintf("%s: expected %s, got nothing", path, e.want) |
1025 | + } |
1026 | + return fmt.Sprintf("%s: expected %s, got %#v", path, e.want, e.got) |
1027 | +} |
1028 | + |
1029 | +// Any returns a Checker that succeeds with any input value and |
1030 | +// results in the value itself unprocessed. |
1031 | +func Any() Checker { |
1032 | + return anyC{} |
1033 | +} |
1034 | + |
1035 | +type anyC struct{} |
1036 | + |
1037 | +func (c anyC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1038 | + return v, nil |
1039 | +} |
1040 | + |
1041 | + |
1042 | +// Const returns a Checker that only succeeds if the input matches |
1043 | +// value exactly. The value is compared with reflect.DeepEqual. |
1044 | +func Const(value interface{}) Checker { |
1045 | + return constC{value} |
1046 | +} |
1047 | + |
1048 | +type constC struct { |
1049 | + value interface{} |
1050 | +} |
1051 | + |
1052 | +func (c constC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1053 | + if reflect.DeepEqual(v, c.value) { |
1054 | + return v, nil |
1055 | + } |
1056 | + return nil, error{fmt.Sprintf("%#v", c.value), v, path} |
1057 | +} |
1058 | + |
1059 | +// OneOf returns a Checker that attempts to Coerce the value with each |
1060 | +// of the provided checkers. The value returned by the first checker |
1061 | +// that succeeds will be returned by the OneOf checker itself. If no |
1062 | +// checker succeeds, OneOf will return an error on coercion. |
1063 | +func OneOf(options ...Checker) Checker { |
1064 | + return oneOfC{options} |
1065 | +} |
1066 | + |
1067 | +type oneOfC struct { |
1068 | + options []Checker |
1069 | +} |
1070 | + |
1071 | +func (c oneOfC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1072 | + for _, o := range c.options { |
1073 | + newv, err := o.Coerce(v, path) |
1074 | + if err == nil { |
1075 | + return newv, nil |
1076 | + } |
1077 | + } |
1078 | + return nil, error{path: path} |
1079 | +} |
1080 | + |
1081 | +// Bool returns a Checker that accepts boolean values only. |
1082 | +func Bool() Checker { |
1083 | + return boolC{} |
1084 | +} |
1085 | + |
1086 | +type boolC struct{} |
1087 | + |
1088 | +func (c boolC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1089 | + if v != nil && reflect.TypeOf(v).Kind() == reflect.Bool { |
1090 | + return v, nil |
1091 | + } |
1092 | + return nil, error{"bool", v, path} |
1093 | +} |
1094 | + |
1095 | +// Int returns a Checker that accepts any integer value, and returns |
1096 | +// the same value consistently typed as an int64. |
1097 | +func Int() Checker { |
1098 | + return intC{} |
1099 | +} |
1100 | + |
1101 | +type intC struct{} |
1102 | + |
1103 | +func (c intC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1104 | + if v == nil { |
1105 | + return nil, error{"int", v, path} |
1106 | + } |
1107 | + switch reflect.TypeOf(v).Kind() { |
1108 | + case reflect.Int: |
1109 | + case reflect.Int8: |
1110 | + case reflect.Int16: |
1111 | + case reflect.Int32: |
1112 | + case reflect.Int64: |
1113 | + default: |
1114 | + return nil, error{"int", v, path} |
1115 | + } |
1116 | + return reflect.ValueOf(v).Int(), nil |
1117 | +} |
1118 | + |
1119 | +// Int returns a Checker that accepts any float value, and returns |
1120 | +// the same value consistently typed as a float64. |
1121 | +func Float() Checker { |
1122 | + return floatC{} |
1123 | +} |
1124 | + |
1125 | +type floatC struct{} |
1126 | + |
1127 | +func (c floatC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1128 | + if v == nil { |
1129 | + return nil, error{"float", v, path} |
1130 | + } |
1131 | + switch reflect.TypeOf(v).Kind() { |
1132 | + case reflect.Float32: |
1133 | + case reflect.Float64: |
1134 | + default: |
1135 | + return nil, error{"float", v, path} |
1136 | + } |
1137 | + return reflect.ValueOf(v).Float(), nil |
1138 | +} |
1139 | + |
1140 | + |
1141 | +// String returns a Checker that accepts a string value only and returns |
1142 | +// it unprocessed. |
1143 | +func String() Checker { |
1144 | + return stringC{} |
1145 | +} |
1146 | + |
1147 | +type stringC struct{} |
1148 | + |
1149 | +func (c stringC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1150 | + if v != nil && reflect.TypeOf(v).Kind() == reflect.String { |
1151 | + return reflect.ValueOf(v).String(), nil |
1152 | + } |
1153 | + return nil, error{"string", v, path} |
1154 | +} |
1155 | + |
1156 | +func SimpleRegexp() Checker { |
1157 | + return sregexpC{} |
1158 | +} |
1159 | + |
1160 | +type sregexpC struct{} |
1161 | + |
1162 | +func (c sregexpC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1163 | + // XXX The regexp package happens to be extremely simple right now. |
1164 | + // Once exp/regexp goes mainstream, we'll have to update this |
1165 | + // logic to use a more widely accepted regexp subset. |
1166 | + if v != nil && reflect.TypeOf(v).Kind() == reflect.String { |
1167 | + s := reflect.ValueOf(v).String() |
1168 | + _, err := regexp.Compile(s) |
1169 | + if err != nil { |
1170 | + return nil, error{"valid regexp", s, path} |
1171 | + } |
1172 | + return v, nil |
1173 | + } |
1174 | + return nil, error{"regexp string", v, path} |
1175 | +} |
1176 | + |
1177 | +// List returns a Checker that accepts a slice value with values |
1178 | +// that are processed with the elem checker. If any element of the |
1179 | +// provided slice value fails to be processed, processing will stop |
1180 | +// and return with the obtained error. |
1181 | +// |
1182 | +// The coerced output value has type schema.ListType. |
1183 | +func List(elem Checker) Checker { |
1184 | + return listC{elem} |
1185 | +} |
1186 | + |
1187 | +type listC struct { |
1188 | + elem Checker |
1189 | +} |
1190 | + |
1191 | +func (c listC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1192 | + rv := reflect.ValueOf(v) |
1193 | + if rv.Kind() != reflect.Slice { |
1194 | + return nil, error{"list", v, path} |
1195 | + } |
1196 | + |
1197 | + path = append(path, "[", "?", "]") |
1198 | + |
1199 | + l := rv.Len() |
1200 | + out := make(ListType, 0, l) |
1201 | + for i := 0; i != l; i++ { |
1202 | + path[len(path)-2] = strconv.Itoa(i) |
1203 | + elem, err := c.elem.Coerce(rv.Index(i).Interface(), path) |
1204 | + if err != nil { |
1205 | + return nil, err |
1206 | + } |
1207 | + out = append(out, elem) |
1208 | + } |
1209 | + return out, nil |
1210 | +} |
1211 | + |
1212 | +// Map returns a Checker that accepts a map value. Every key and value |
1213 | +// in the map are processed with the respective checker, and if any |
1214 | +// value fails to be coerced, processing stops and returns with the |
1215 | +// underlying error. |
1216 | +// |
1217 | +// The coerced output value has type schema.MapType. |
1218 | +func Map(key Checker, value Checker) Checker { |
1219 | + return mapC{key, value} |
1220 | +} |
1221 | + |
1222 | +type mapC struct { |
1223 | + key Checker |
1224 | + value Checker |
1225 | +} |
1226 | + |
1227 | +func (c mapC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1228 | + rv := reflect.ValueOf(v) |
1229 | + if rv.Kind() != reflect.Map { |
1230 | + return nil, error{"map", v, path} |
1231 | + } |
1232 | + |
1233 | + vpath := append(path, ".", "?") |
1234 | + |
1235 | + l := rv.Len() |
1236 | + out := make(MapType, l) |
1237 | + keys := rv.MapKeys() |
1238 | + for i := 0; i != l; i++ { |
1239 | + k := keys[i] |
1240 | + newk, err := c.key.Coerce(k.Interface(), path) |
1241 | + if err != nil { |
1242 | + return nil, err |
1243 | + } |
1244 | + vpath[len(vpath)-1] = fmt.Sprint(k.Interface()) |
1245 | + newv, err := c.value.Coerce(rv.MapIndex(k).Interface(), vpath) |
1246 | + if err != nil { |
1247 | + return nil, err |
1248 | + } |
1249 | + out[newk] = newv |
1250 | + } |
1251 | + return out, nil |
1252 | +} |
1253 | + |
1254 | +type Fields map[string]Checker |
1255 | +type Optional []string |
1256 | + |
1257 | +// FieldMap returns a Checker that accepts a map value with defined |
1258 | +// string keys. Every key has an independent checker associated, |
1259 | +// and processing will only succeed if all the values succeed |
1260 | +// individually. If a field fails to be processed, processing stops |
1261 | +// and returns with the underlying error. |
1262 | +// |
1263 | +// The coerced output value has type schema.MapType. |
1264 | +func FieldMap(fields Fields, optional Optional) Checker { |
1265 | + return fieldMapC{fields, optional} |
1266 | +} |
1267 | + |
1268 | +type fieldMapC struct { |
1269 | + fields Fields |
1270 | + optional []string |
1271 | +} |
1272 | + |
1273 | +func (c fieldMapC) isOptional(key string) bool { |
1274 | + for _, k := range c.optional { |
1275 | + if k == key { |
1276 | + return true |
1277 | + } |
1278 | + } |
1279 | + return false |
1280 | +} |
1281 | + |
1282 | +func (c fieldMapC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1283 | + rv := reflect.ValueOf(v) |
1284 | + if rv.Kind() != reflect.Map { |
1285 | + return nil, error{"map", v, path} |
1286 | + } |
1287 | + |
1288 | + vpath := append(path, ".", "?") |
1289 | + |
1290 | + l := rv.Len() |
1291 | + out := make(MapType, l) |
1292 | + for k, checker := range c.fields { |
1293 | + vpath[len(vpath)-1] = k |
1294 | + var value interface{} |
1295 | + valuev := rv.MapIndex(reflect.ValueOf(k)) |
1296 | + if valuev.IsValid() { |
1297 | + value = valuev.Interface() |
1298 | + } else if c.isOptional(k) { |
1299 | + continue |
1300 | + } |
1301 | + newv, err := checker.Coerce(value, vpath) |
1302 | + if err != nil { |
1303 | + return nil, err |
1304 | + } |
1305 | + out[k] = newv |
1306 | + } |
1307 | + return out, nil |
1308 | +} |
1309 | + |
1310 | +// FieldMapSet returns a Checker that accepts a map value checked |
1311 | +// against one of several FieldMap checkers. The actual checker |
1312 | +// used is the first one whose checker associated with the selector |
1313 | +// field processes the map correctly. If no checker processes |
1314 | +// the selector value correctly, an error is returned. |
1315 | +// |
1316 | +// The coerced output value has type schema.MapType. |
1317 | +func FieldMapSet(selector string, maps []Checker) Checker { |
1318 | + fmaps := make([]fieldMapC, len(maps)) |
1319 | + for i, m := range maps { |
1320 | + if fmap, ok := m.(fieldMapC); ok { |
1321 | + if checker, _ := fmap.fields[selector]; checker == nil { |
1322 | + panic("FieldMapSet has a FieldMap with a missing selector") |
1323 | + } |
1324 | + fmaps[i] = fmap |
1325 | + } else { |
1326 | + panic("FieldMapSet got a non-FieldMap checker") |
1327 | + } |
1328 | + } |
1329 | + return mapSetC{selector, fmaps} |
1330 | +} |
1331 | + |
1332 | +type mapSetC struct { |
1333 | + selector string |
1334 | + fmaps []fieldMapC |
1335 | +} |
1336 | + |
1337 | +func (c mapSetC) Coerce(v interface{}, path []string) (interface{}, os.Error) { |
1338 | + rv := reflect.ValueOf(v) |
1339 | + if rv.Kind() != reflect.Map { |
1340 | + return nil, error{"map", v, path} |
1341 | + } |
1342 | + |
1343 | + var selector interface{} |
1344 | + selectorv := rv.MapIndex(reflect.ValueOf(c.selector)) |
1345 | + if selectorv.IsValid() { |
1346 | + selector = selectorv.Interface() |
1347 | + for _, fmap := range c.fmaps { |
1348 | + _, err := fmap.fields[c.selector].Coerce(selector, path) |
1349 | + if err != nil { |
1350 | + continue |
1351 | + } |
1352 | + return fmap.Coerce(v, path) |
1353 | + } |
1354 | + } |
1355 | + return nil, error{"supported selector", selector, append(path, ".", c.selector)} |
1356 | +} |
1357 | |
1358 | === added file 'schema/schema_test.go' |
1359 | --- schema/schema_test.go 1970-01-01 00:00:00 +0000 |
1360 | +++ schema/schema_test.go 2011-09-06 21:44:45 +0000 |
1361 | @@ -0,0 +1,293 @@ |
1362 | +package schema_test |
1363 | + |
1364 | +import ( |
1365 | + "testing" |
1366 | + . "launchpad.net/gocheck" |
1367 | + "launchpad.net/ensemble/go/schema" |
1368 | + "os" |
1369 | +) |
1370 | + |
1371 | +func Test(t *testing.T) { |
1372 | + TestingT(t) |
1373 | +} |
1374 | + |
1375 | +type S struct{} |
1376 | + |
1377 | +var _ = Suite(&S{}) |
1378 | + |
1379 | +type Dummy struct{} |
1380 | + |
1381 | +func (d *Dummy) Coerce(value interface{}, path []string) (coerced interface{}, err os.Error) { |
1382 | + return "i-am-dummy", nil |
1383 | +} |
1384 | + |
1385 | +var aPath = []string{"<pa", "th>"} |
1386 | + |
1387 | +func (s *S) TestConst(c *C) { |
1388 | + sch := schema.Const("foo") |
1389 | + |
1390 | + out, err := sch.Coerce("foo", aPath) |
1391 | + c.Assert(err, IsNil) |
1392 | + c.Assert(out, Equals, "foo") |
1393 | + |
1394 | + out, err = sch.Coerce(42, aPath) |
1395 | + c.Assert(out, IsNil) |
1396 | + c.Assert(err, Matches, `<path>: expected "foo", got 42`) |
1397 | + |
1398 | + out, err = sch.Coerce(nil, aPath) |
1399 | + c.Assert(out, IsNil) |
1400 | + c.Assert(err, Matches, `<path>: expected "foo", got nothing`) |
1401 | +} |
1402 | + |
1403 | +func (s *S) TestAny(c *C) { |
1404 | + sch := schema.Any() |
1405 | + |
1406 | + out, err := sch.Coerce("foo", aPath) |
1407 | + c.Assert(err, IsNil) |
1408 | + c.Assert(out, Equals, "foo") |
1409 | + |
1410 | + out, err = sch.Coerce(nil, aPath) |
1411 | + c.Assert(err, IsNil) |
1412 | + c.Assert(out, Equals, nil) |
1413 | +} |
1414 | + |
1415 | +func (s *S) TestOneOf(c *C) { |
1416 | + sch := schema.OneOf(schema.Const("foo"), schema.Const(42)) |
1417 | + |
1418 | + out, err := sch.Coerce("foo", aPath) |
1419 | + c.Assert(err, IsNil) |
1420 | + c.Assert(out, Equals, "foo") |
1421 | + |
1422 | + out, err = sch.Coerce(42, aPath) |
1423 | + c.Assert(err, IsNil) |
1424 | + c.Assert(out, Equals, 42) |
1425 | + |
1426 | + out, err = sch.Coerce("bar", aPath) |
1427 | + c.Assert(out, IsNil) |
1428 | + c.Assert(err, Matches, `<path>: unsupported value`) |
1429 | +} |
1430 | + |
1431 | +func (s *S) TestBool(c *C) { |
1432 | + sch := schema.Bool() |
1433 | + |
1434 | + out, err := sch.Coerce(true, aPath) |
1435 | + c.Assert(err, IsNil) |
1436 | + c.Assert(out, Equals, true) |
1437 | + |
1438 | + out, err = sch.Coerce(false, aPath) |
1439 | + c.Assert(err, IsNil) |
1440 | + c.Assert(out, Equals, false) |
1441 | + |
1442 | + out, err = sch.Coerce(1, aPath) |
1443 | + c.Assert(out, IsNil) |
1444 | + c.Assert(err, Matches, "<path>: expected bool, got 1") |
1445 | + |
1446 | + out, err = sch.Coerce(nil, aPath) |
1447 | + c.Assert(out, IsNil) |
1448 | + c.Assert(err, Matches, "<path>: expected bool, got nothing") |
1449 | +} |
1450 | + |
1451 | +func (s *S) TestInt(c *C) { |
1452 | + sch := schema.Int() |
1453 | + |
1454 | + out, err := sch.Coerce(42, aPath) |
1455 | + c.Assert(err, IsNil) |
1456 | + c.Assert(out, Equals, int64(42)) |
1457 | + |
1458 | + out, err = sch.Coerce(int8(42), aPath) |
1459 | + c.Assert(err, IsNil) |
1460 | + c.Assert(out, Equals, int64(42)) |
1461 | + |
1462 | + out, err = sch.Coerce(true, aPath) |
1463 | + c.Assert(out, IsNil) |
1464 | + c.Assert(err, Matches, "<path>: expected int, got true") |
1465 | + |
1466 | + out, err = sch.Coerce(nil, aPath) |
1467 | + c.Assert(out, IsNil) |
1468 | + c.Assert(err, Matches, "<path>: expected int, got nothing") |
1469 | +} |
1470 | + |
1471 | +func (s *S) TestFloat(c *C) { |
1472 | + sch := schema.Float() |
1473 | + |
1474 | + out, err := sch.Coerce(float32(1.0), aPath) |
1475 | + c.Assert(err, IsNil) |
1476 | + c.Assert(out, Equals, float64(1.0)) |
1477 | + |
1478 | + out, err = sch.Coerce(float64(1.0), aPath) |
1479 | + c.Assert(err, IsNil) |
1480 | + c.Assert(out, Equals, float64(1.0)) |
1481 | + |
1482 | + out, err = sch.Coerce(true, aPath) |
1483 | + c.Assert(out, IsNil) |
1484 | + c.Assert(err, Matches, "<path>: expected float, got true") |
1485 | + |
1486 | + out, err = sch.Coerce(nil, aPath) |
1487 | + c.Assert(out, IsNil) |
1488 | + c.Assert(err, Matches, "<path>: expected float, got nothing") |
1489 | +} |
1490 | + |
1491 | +func (s *S) TestString(c *C) { |
1492 | + sch := schema.String() |
1493 | + |
1494 | + out, err := sch.Coerce("foo", aPath) |
1495 | + c.Assert(err, IsNil) |
1496 | + c.Assert(out, Equals, "foo") |
1497 | + |
1498 | + out, err = sch.Coerce(true, aPath) |
1499 | + c.Assert(out, IsNil) |
1500 | + c.Assert(err, Matches, "<path>: expected string, got true") |
1501 | + |
1502 | + out, err = sch.Coerce(nil, aPath) |
1503 | + c.Assert(out, IsNil) |
1504 | + c.Assert(err, Matches, "<path>: expected string, got nothing") |
1505 | +} |
1506 | + |
1507 | +func (s *S) TestSimpleRegexp(c *C) { |
1508 | + sch := schema.SimpleRegexp() |
1509 | + out, err := sch.Coerce("[0-9]+", aPath) |
1510 | + c.Assert(err, IsNil) |
1511 | + c.Assert(out, Equals, "[0-9]+") |
1512 | + |
1513 | + out, err = sch.Coerce(1, aPath) |
1514 | + c.Assert(out, IsNil) |
1515 | + c.Assert(err, Matches, "<path>: expected regexp string, got 1") |
1516 | + |
1517 | + out, err = sch.Coerce("[", aPath) |
1518 | + c.Assert(out, IsNil) |
1519 | + c.Assert(err, Matches, `<path>: expected valid regexp, got "\["`) |
1520 | + |
1521 | + out, err = sch.Coerce(nil, aPath) |
1522 | + c.Assert(out, IsNil) |
1523 | + c.Assert(err, Matches, `<path>: expected regexp string, got nothing`) |
1524 | +} |
1525 | + |
1526 | +func (s *S) TestList(c *C) { |
1527 | + sch := schema.List(schema.Int()) |
1528 | + out, err := sch.Coerce([]int8{1, 2}, aPath) |
1529 | + c.Assert(err, IsNil) |
1530 | + c.Assert(out, Equals, schema.ListType{int64(1), int64(2)}) |
1531 | + |
1532 | + out, err = sch.Coerce(42, aPath) |
1533 | + c.Assert(out, IsNil) |
1534 | + c.Assert(err, Matches, "<path>: expected list, got 42") |
1535 | + |
1536 | + out, err = sch.Coerce(nil, aPath) |
1537 | + c.Assert(out, IsNil) |
1538 | + c.Assert(err, Matches, "<path>: expected list, got nothing") |
1539 | + |
1540 | + out, err = sch.Coerce([]interface{}{1, true}, aPath) |
1541 | + c.Assert(out, IsNil) |
1542 | + c.Assert(err, Matches, `<path>\[1\]: expected int, got true`) |
1543 | +} |
1544 | + |
1545 | +func (s *S) TestMap(c *C) { |
1546 | + sch := schema.Map(schema.String(), schema.Int()) |
1547 | + out, err := sch.Coerce(map[string]interface{}{"a": 1, "b": int8(2)}, aPath) |
1548 | + c.Assert(err, IsNil) |
1549 | + c.Assert(out, Equals, schema.MapType{"a": int64(1), "b": int64(2)}) |
1550 | + |
1551 | + out, err = sch.Coerce(42, aPath) |
1552 | + c.Assert(out, IsNil) |
1553 | + c.Assert(err, Matches, "<path>: expected map, got 42") |
1554 | + |
1555 | + out, err = sch.Coerce(nil, aPath) |
1556 | + c.Assert(out, IsNil) |
1557 | + c.Assert(err, Matches, "<path>: expected map, got nothing") |
1558 | + |
1559 | + out, err = sch.Coerce(map[int]int{1: 1}, aPath) |
1560 | + c.Assert(out, IsNil) |
1561 | + c.Assert(err, Matches, "<path>: expected string, got 1") |
1562 | + |
1563 | + out, err = sch.Coerce(map[string]bool{"a": true}, aPath) |
1564 | + c.Assert(out, IsNil) |
1565 | + c.Assert(err, Matches, `<path>\.a: expected int, got true`) |
1566 | + |
1567 | + // First path entry shouldn't have dots in an error message. |
1568 | + out, err = sch.Coerce(map[string]bool{"a": true}, nil) |
1569 | + c.Assert(out, IsNil) |
1570 | + c.Assert(err, Matches, `a: expected int, got true`) |
1571 | +} |
1572 | + |
1573 | +func (s *S) TestFieldMap(c *C) { |
1574 | + fields := schema.Fields{ |
1575 | + "a": schema.Const("A"), |
1576 | + "b": schema.Const("B"), |
1577 | + } |
1578 | + sch := schema.FieldMap(fields, schema.Optional{"b"}) |
1579 | + |
1580 | + out, err := sch.Coerce(map[string]interface{}{"a": "A", "b": "B"}, aPath) |
1581 | + c.Assert(err, IsNil) |
1582 | + c.Assert(out, Equals, schema.MapType{"a": "A", "b": "B"}) |
1583 | + |
1584 | + out, err = sch.Coerce(42, aPath) |
1585 | + c.Assert(out, IsNil) |
1586 | + c.Assert(err, Matches, "<path>: expected map, got 42") |
1587 | + |
1588 | + out, err = sch.Coerce(nil, aPath) |
1589 | + c.Assert(out, IsNil) |
1590 | + c.Assert(err, Matches, "<path>: expected map, got nothing") |
1591 | + |
1592 | + out, err = sch.Coerce(map[string]interface{}{"a": "A", "b": "C"}, aPath) |
1593 | + c.Assert(out, IsNil) |
1594 | + c.Assert(err, Matches, `<path>\.b: expected "B", got "C"`) |
1595 | + |
1596 | + out, err = sch.Coerce(map[string]interface{}{"b": "B"}, aPath) |
1597 | + c.Assert(out, IsNil) |
1598 | + c.Assert(err, Matches, `<path>\.a: expected "A", got nothing`) |
1599 | + |
1600 | + // b is optional |
1601 | + out, err = sch.Coerce(map[string]interface{}{"a": "A"}, aPath) |
1602 | + c.Assert(err, IsNil) |
1603 | + c.Assert(out, Equals, schema.MapType{"a": "A"}) |
1604 | + |
1605 | + // First path entry shouldn't have dots in an error message. |
1606 | + out, err = sch.Coerce(map[string]bool{"a": true}, nil) |
1607 | + c.Assert(out, IsNil) |
1608 | + c.Assert(err, Matches, `a: expected "A", got true`) |
1609 | +} |
1610 | + |
1611 | +func (s *S) TestSchemaMap(c *C) { |
1612 | + fields1 := schema.FieldMap(schema.Fields{ |
1613 | + "type": schema.Const(1), |
1614 | + "a": schema.Const(2), |
1615 | + }, nil) |
1616 | + fields2 := schema.FieldMap(schema.Fields{ |
1617 | + "type": schema.Const(3), |
1618 | + "b": schema.Const(4), |
1619 | + }, nil) |
1620 | + sch := schema.FieldMapSet("type", []schema.Checker{fields1, fields2}) |
1621 | + |
1622 | + out, err := sch.Coerce(map[string]int{"type": 1, "a": 2}, aPath) |
1623 | + c.Assert(err, IsNil) |
1624 | + c.Assert(out, Equals, schema.MapType{"type": 1, "a": 2}) |
1625 | + |
1626 | + out, err = sch.Coerce(map[string]int{"type": 3, "b": 4}, aPath) |
1627 | + c.Assert(err, IsNil) |
1628 | + c.Assert(out, Equals, schema.MapType{"type": 3, "b": 4}) |
1629 | + |
1630 | + out, err = sch.Coerce(map[string]int{}, aPath) |
1631 | + c.Assert(out, IsNil) |
1632 | + c.Assert(err, Matches, `<path>\.type: expected supported selector, got nothing`) |
1633 | + |
1634 | + out, err = sch.Coerce(map[string]int{"type": 2}, aPath) |
1635 | + c.Assert(out, IsNil) |
1636 | + c.Assert(err, Matches, `<path>\.type: expected supported selector, got 2`) |
1637 | + |
1638 | + out, err = sch.Coerce(map[string]int{"type": 3, "b": 5}, aPath) |
1639 | + c.Assert(out, IsNil) |
1640 | + c.Assert(err, Matches, `<path>\.b: expected 4, got 5`) |
1641 | + |
1642 | + out, err = sch.Coerce(42, aPath) |
1643 | + c.Assert(out, IsNil) |
1644 | + c.Assert(err, Matches, `<path>: expected map, got 42`) |
1645 | + |
1646 | + out, err = sch.Coerce(nil, aPath) |
1647 | + c.Assert(out, IsNil) |
1648 | + c.Assert(err, Matches, `<path>: expected map, got nothing`) |
1649 | + |
1650 | + // First path entry shouldn't have dots in an error message. |
1651 | + out, err = sch.Coerce(map[string]int{"a": 1}, nil) |
1652 | + c.Assert(out, IsNil) |
1653 | + c.Assert(err, Matches, `type: expected supported selector, got nothing`) |
1654 | +} |
Looks good, +1