Merge lp:~niemeyer/pyjuju/go-formula-config-validation into lp:pyjuju

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
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Review via email: mp+74311@code.launchpad.net

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.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Looks good, +1

review: Approve

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

Subscribers

People subscribed via source and target branches

to status/vote changes: