Merge lp:~jameinel/juju-core/typed-registry into lp:~go-bot/juju-core/trunk

Proposed by John A Meinel
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 2739
Proposed branch: lp:~jameinel/juju-core/typed-registry
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 298 lines (+278/-0)
4 files modified
utils/registry/export_test.go (+8/-0)
utils/registry/package_test.go (+14/-0)
utils/registry/registry.go (+105/-0)
utils/registry/registry_test.go (+151/-0)
To merge this branch: bzr merge lp:~jameinel/juju-core/typed-registry
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+219161@code.launchpad.net

Commit message

utils/registry: TypedNameVersion registry

As part of getting API versioning in place, I was putting together some
Registry's in a couple of places. I realized I wanted 2 things that were
essentially identical code, just tracking slightly different object
types. So I put together an abstraction that still enforces the Type,
but is otherwise treats the input and output as a generic interface{}.

https://codereview.appspot.com/99180044/

Description of the change

utils/registry: TypedNameVersion registry

As part of getting API versioning in place, I was putting together some
Registry's in a couple of places. I realized I wanted 2 things that were
essentially identical code, just tracking slightly different object
types. So I put together an abstraction that still enforces the Type,
but is otherwise treats the input and output as a generic interface{}.

I'm not stuck on the naming, though I went with
registry.TypedNameVersion as the handle for this thing. I could just as
easily calling simply registry.Registry and the fact that it enforces
Types and uses Name+Version as the key can just be in the docs.

I'm not 100% sure that we'll strictly need this, as the code is evolving
to where I may only have 1 registry. However, it still seemed like a
useful bit of functionality, and it was easily split out from the other
work as a self-contained and well tested bit of work.

If we prefer, I can wait to land this until it is clearly necessary
(rather than just having a single Registry that has a fixed object
type). But if we ever have >=2 registries with different types, I think
we'll want something like this.

https://codereview.appspot.com/99180044/

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Reviewers: mp+219161_code.launchpad.net,

Message:
Please take a look.

Description:
utils/registry: TypedNameVersion registry

As part of getting API versioning in place, I was putting together some
Registry's in a couple of places. I realized I wanted 2 things that were
essentially identical code, just tracking slightly different object
types. So I put together an abstraction that still enforces the Type,
but is otherwise treats the input and output as a generic interface{}.

I'm not stuck on the naming, though I went with
registry.TypedNameVersion as the handle for this thing. I could just as
easily calling simply registry.Registry and the fact that it enforces
Types and uses Name+Version as the key can just be in the docs.

I'm not 100% sure that we'll strictly need this, as the code is evolving
to where I may only have 1 registry. However, it still seemed like a
useful bit of functionality, and it was easily split out from the other
work as a self-contained and well tested bit of work.

If we prefer, I can wait to land this until it is clearly necessary
(rather than just having a single Registry that has a fixed object
type). But if we ever have >=2 registries with different types, I think
we'll want something like this.

https://code.launchpad.net/~jameinel/juju-core/typed-registry/+merge/219161

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/99180044/

Affected files (+280, -0 lines):
   A [revision details]
   A utils/registry/export_test.go
   A utils/registry/package_test.go
   A utils/registry/registry.go
   A utils/registry/registry_test.go

Revision history for this message
William Reade (fwereade) wrote :

I'm also not sure that we'll end up using exactly this, but LGTM all the
same; just please remember to drop it if it turns out that we don't.

https://codereview.appspot.com/99180044/

Revision history for this message
John A Meinel (jameinel) wrote :
Revision history for this message
Go Bot (go-bot) wrote :

The attempt to merge lp:~jameinel/juju-core/typed-registry into lp:juju-core failed. Below is the output from the failed tests.

utils/registry/registry.go:11:2: cannot find package "launchpad.net/juju-core/errors" in any of:
 /usr/lib/go/src/pkg/launchpad.net/juju-core/errors (from $GOROOT)
 /home/tarmac/trees/src/launchpad.net/juju-core/errors (from $GOPATH)
mongod: no process found

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Saw this in passing - it looks reasonable, although with all the talk of
"versions" and "facades" it seems as if it it might fit better as a
subpackage of or within the rpc package.

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go
File utils/registry/registry.go (right):

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode16
utils/registry/registry.go:16: // defined when the registry was created.
It will be cast during Register so
I'm not sure I see the use case for the conversion logic. Wouldn't it be
simpler just to stipulate that the same type must always be registered
for the same name?

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode21
utils/registry/registry.go:21: versions map[string]Versions
Another possibility might be to hold a single map with a struct{name,
version} key. Then Get is only a single map lookup and List could be
simpler too.

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode38
utils/registry/registry.go:38: // Versions maps concrete versions of the
objects.
a) this doc comment doesn't say what it maps from.
b) it looks as if the Versions type doesn't actually need to be
exported.

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode41
utils/registry/registry.go:41: // Register records the factory that can
be used to produce an instance of the
This comment doesn't seem to make sense in terms of the functionality of
this package. There's nothing factory-like going on here, although it's
certainly possible to store functions in the registry; similarly
"facade" doesn't really have any meaning here.

// Register records the object associated with the given name and
version.
// The object must be of the same type passed to NewTypedNameVersion.

?

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode52
utils/registry/registry.go:52: if r.versions == nil {
How can this happen, assuming NewTypedNameVersion has been used?

https://codereview.appspot.com/99180044/diff/20001/utils/registry/registry.go#newcode97
utils/registry/registry.go:97: // facade is not found, it returns
error.NotFound
I'd prefer to see a bool return here to make it clear that
there's only one kind of error (and it's closer to the map
interface too, which is essentially what this type is)

https://codereview.appspot.com/99180044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'utils/registry'
2=== added file 'utils/registry/export_test.go'
3--- utils/registry/export_test.go 1970-01-01 00:00:00 +0000
4+++ utils/registry/export_test.go 2014-05-16 11:49:09 +0000
5@@ -0,0 +1,8 @@
6+// Copyright 2012, 2013 Canonical Ltd.
7+// Licensed under the AGPLv3, see LICENCE file for details.
8+
9+package registry
10+
11+var (
12+ DescriptionFromVersions = descriptionFromVersions
13+)
14
15=== added file 'utils/registry/package_test.go'
16--- utils/registry/package_test.go 1970-01-01 00:00:00 +0000
17+++ utils/registry/package_test.go 2014-05-16 11:49:09 +0000
18@@ -0,0 +1,14 @@
19+// Copyright 2014 Canonical Ltd.
20+// Licensed under the AGPLv3, see LICENCE file for details.
21+
22+package registry_test
23+
24+import (
25+ stdtesting "testing"
26+
27+ gc "launchpad.net/gocheck"
28+)
29+
30+func TestAll(t *stdtesting.T) {
31+ gc.TestingT(t)
32+}
33
34=== added file 'utils/registry/registry.go'
35--- utils/registry/registry.go 1970-01-01 00:00:00 +0000
36+++ utils/registry/registry.go 2014-05-16 11:49:09 +0000
37@@ -0,0 +1,105 @@
38+// Copyright 2014 Canonical Ltd.
39+// Licensed under the AGPLv3, see LICENCE file for details.
40+
41+package registry
42+
43+import (
44+ "fmt"
45+ "reflect"
46+ "sort"
47+
48+ "github.com/juju/errors"
49+)
50+
51+// TypedNameVersion is a registry that will allow you to register objects based
52+// on a name and version pair. The objects must be convertible to the Type
53+// defined when the registry was created. It will be cast during Register so
54+// you can be sure all objects returned from Get() are safe to TypeAssert to
55+// that type.
56+type TypedNameVersion struct {
57+ requiredType reflect.Type
58+ versions map[string]Versions
59+}
60+
61+// NewTypedNameVersion creates a place to register your objects
62+func NewTypedNameVersion(requiredType reflect.Type) *TypedNameVersion {
63+ return &TypedNameVersion{
64+ requiredType: requiredType,
65+ versions: make(map[string]Versions),
66+ }
67+}
68+
69+// Description gives the name and available versions in a registry.
70+type Description struct {
71+ Name string
72+ Versions []int
73+}
74+
75+// Versions maps concrete versions of the objects.
76+type Versions map[int]interface{}
77+
78+// Register records the factory that can be used to produce an instance of the
79+// facade at the supplied version.
80+// If the object being registered doesn't Implement the required Type, then an
81+// error is returned.
82+// An error is also returned if an object is already registered with the given
83+// name and version.
84+func (r *TypedNameVersion) Register(name string, version int, obj interface{}) error {
85+ if !reflect.TypeOf(obj).ConvertibleTo(r.requiredType) {
86+ return fmt.Errorf("object of type %T cannot be converted to type %s.%s", obj, r.requiredType.PkgPath(), r.requiredType.Name())
87+ }
88+ obj = reflect.ValueOf(obj).Convert(r.requiredType).Interface()
89+ if r.versions == nil {
90+ r.versions = make(map[string]Versions, 1)
91+ }
92+ if versions, ok := r.versions[name]; ok {
93+ if _, ok := versions[version]; ok {
94+ fullname := fmt.Sprintf("%s(%d)", name, version)
95+ return fmt.Errorf("object %q already registered", fullname)
96+ }
97+ versions[version] = obj
98+ } else {
99+ r.versions[name] = Versions{version: obj}
100+ }
101+ return nil
102+}
103+
104+// descriptionFromVersions aggregates the information in a Versions map into a
105+// more friendly form for List()
106+func descriptionFromVersions(name string, versions Versions) Description {
107+ intVersions := make([]int, 0, len(versions))
108+ for version := range versions {
109+ intVersions = append(intVersions, version)
110+ }
111+ sort.Ints(intVersions)
112+ return Description{
113+ Name: name,
114+ Versions: intVersions,
115+ }
116+}
117+
118+// List returns a slice describing each of the registered Facades.
119+func (r *TypedNameVersion) List() []Description {
120+ names := make([]string, 0, len(r.versions))
121+ for name := range r.versions {
122+ names = append(names, name)
123+ }
124+ sort.Strings(names)
125+ descriptions := make([]Description, len(r.versions))
126+ for i, name := range names {
127+ versions := r.versions[name]
128+ descriptions[i] = descriptionFromVersions(name, versions)
129+ }
130+ return descriptions
131+}
132+
133+// Get returns the object for a single name and version. If the requested
134+// facade is not found, it returns error.NotFound
135+func (r *TypedNameVersion) Get(name string, version int) (interface{}, error) {
136+ if versions, ok := r.versions[name]; ok {
137+ if factory, ok := versions[version]; ok {
138+ return factory, nil
139+ }
140+ }
141+ return nil, errors.NotFoundf("%s(%d)", name, version)
142+}
143
144=== added file 'utils/registry/registry_test.go'
145--- utils/registry/registry_test.go 1970-01-01 00:00:00 +0000
146+++ utils/registry/registry_test.go 2014-05-16 11:49:09 +0000
147@@ -0,0 +1,151 @@
148+// Copyright 2014 Canonical Ltd.
149+// Licensed under the AGPLv3, see LICENCE file for details.
150+
151+package registry_test
152+
153+import (
154+ "reflect"
155+
156+ jc "github.com/juju/testing/checkers"
157+ gc "launchpad.net/gocheck"
158+
159+ "github.com/juju/errors"
160+ "launchpad.net/juju-core/testing/testbase"
161+ "launchpad.net/juju-core/utils/registry"
162+)
163+
164+type registrySuite struct {
165+ testbase.LoggingSuite
166+}
167+
168+var _ = gc.Suite(&registrySuite{})
169+
170+type Factory func() (interface{}, error)
171+
172+func nilFactory() (interface{}, error) {
173+ return nil, nil
174+}
175+
176+var factoryType = reflect.TypeOf((*Factory)(nil)).Elem()
177+
178+type testFacade struct {
179+ version string
180+ called bool
181+}
182+
183+type stringVal struct {
184+ value string
185+}
186+
187+func (t *testFacade) TestMethod() stringVal {
188+ t.called = true
189+ return stringVal{"called " + t.version}
190+}
191+
192+func (s *registrySuite) TestDescriptionFromVersions(c *gc.C) {
193+ versions := registry.Versions{0: nilFactory}
194+ c.Check(registry.DescriptionFromVersions("name", versions),
195+ gc.DeepEquals,
196+ registry.Description{
197+ Name: "name",
198+ Versions: []int{0},
199+ })
200+ versions[2] = nilFactory
201+ c.Check(registry.DescriptionFromVersions("name", versions),
202+ gc.DeepEquals,
203+ registry.Description{
204+ Name: "name",
205+ Versions: []int{0, 2},
206+ })
207+}
208+
209+func (s *registrySuite) TestDescriptionFromVersionsAreSorted(c *gc.C) {
210+ versions := registry.Versions{
211+ 10: nilFactory,
212+ 5: nilFactory,
213+ 0: nilFactory,
214+ 18: nilFactory,
215+ 6: nilFactory,
216+ 4: nilFactory,
217+ }
218+ c.Check(registry.DescriptionFromVersions("name", versions),
219+ gc.DeepEquals,
220+ registry.Description{
221+ Name: "name",
222+ Versions: []int{0, 4, 5, 6, 10, 18},
223+ })
224+}
225+
226+func (s *registrySuite) TestRegisterAndList(c *gc.C) {
227+ r := registry.NewTypedNameVersion(factoryType)
228+ c.Assert(r.Register("name", 0, nilFactory), gc.IsNil)
229+ c.Check(r.List(), gc.DeepEquals, []registry.Description{
230+ {Name: "name", Versions: []int{0}},
231+ })
232+}
233+
234+func (s *registrySuite) TestRegisterAndListMultiple(c *gc.C) {
235+ r := registry.NewTypedNameVersion(factoryType)
236+ c.Assert(r.Register("other", 0, nilFactory), gc.IsNil)
237+ c.Assert(r.Register("name", 0, nilFactory), gc.IsNil)
238+ c.Assert(r.Register("third", 2, nilFactory), gc.IsNil)
239+ c.Check(r.List(), gc.DeepEquals, []registry.Description{
240+ {Name: "name", Versions: []int{0}},
241+ {Name: "other", Versions: []int{0}},
242+ {Name: "third", Versions: []int{2}},
243+ })
244+}
245+
246+func (s *registrySuite) TestRegisterWrongType(c *gc.C) {
247+ r := registry.NewTypedNameVersion(factoryType)
248+ err := r.Register("other", 0, "notAFactory")
249+ c.Check(err, gc.ErrorMatches, `object of type string cannot be converted to type .*registry_test.Factory`)
250+}
251+
252+func (s *registrySuite) TestRegisterAlreadyPresent(c *gc.C) {
253+ r := registry.NewTypedNameVersion(factoryType)
254+ err := r.Register("name", 0, func() (interface{}, error) {
255+ return "orig", nil
256+ })
257+ c.Assert(err, gc.IsNil)
258+ err = r.Register("name", 0, func() (interface{}, error) {
259+ return "broken", nil
260+ })
261+ c.Check(err, gc.ErrorMatches, `object "name\(0\)" already registered`)
262+ f, err := r.Get("name", 0)
263+ c.Assert(err, gc.IsNil)
264+ val, err := f.(Factory)()
265+ c.Assert(err, gc.IsNil)
266+ c.Check(val, gc.Equals, "orig")
267+}
268+
269+func (s *registrySuite) TestGet(c *gc.C) {
270+ r := registry.NewTypedNameVersion(factoryType)
271+ customFactory := func() (interface{}, error) {
272+ return 10, nil
273+ }
274+ c.Assert(r.Register("name", 0, customFactory), gc.IsNil)
275+ f, err := r.Get("name", 0)
276+ c.Assert(err, gc.IsNil)
277+ c.Assert(f, gc.NotNil)
278+ res, err := f.(Factory)()
279+ c.Assert(err, gc.IsNil)
280+ c.Check(res, gc.Equals, 10)
281+}
282+
283+func (s *registrySuite) TestGetUnknown(c *gc.C) {
284+ r := registry.NewTypedNameVersion(factoryType)
285+ f, err := r.Get("name", 0)
286+ c.Check(err, jc.Satisfies, errors.IsNotFound)
287+ c.Check(err, gc.ErrorMatches, `name\(0\) not found`)
288+ c.Check(f, gc.IsNil)
289+}
290+
291+func (s *registrySuite) TestGetUnknownVersion(c *gc.C) {
292+ r := registry.NewTypedNameVersion(factoryType)
293+ c.Assert(r.Register("name", 0, nilFactory), gc.IsNil)
294+ f, err := r.Get("name", 1)
295+ c.Check(err, jc.Satisfies, errors.IsNotFound)
296+ c.Check(err, gc.ErrorMatches, `name\(1\) not found`)
297+ c.Check(f, gc.IsNil)
298+}

Subscribers

People subscribed via source and target branches

to status/vote changes: