compositor 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +104 -30
- data/lib/compositor/base.rb +7 -1
- data/lib/compositor/version.rb +1 -1
- data/spec/compositor/base_spec.rb +31 -0
- metadata +4 -3
data/README.md
CHANGED
@@ -3,10 +3,12 @@ Compositor
|
|
3
3
|
|
4
4
|
[](http://travis-ci.org/wanelo/compositor)
|
5
5
|
|
6
|
-
Composite
|
7
|
-
JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together, converting to
|
8
|
-
a
|
6
|
+
A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently
|
7
|
+
JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to
|
8
|
+
a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.
|
9
9
|
|
10
|
+
The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance
|
11
|
+
is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.
|
10
12
|
|
11
13
|
## Installation
|
12
14
|
|
@@ -25,21 +27,22 @@ Or install it yourself as:
|
|
25
27
|
## Usage
|
26
28
|
|
27
29
|
For each model that needs a hash/json representation you need to create a ruby class that subclasses ```Composite::Leaf```,
|
28
|
-
adds some custom state that's important for rendering that object in addition to ```
|
29
|
-
method
|
30
|
+
adds some custom state that's important for rendering that object in addition to ```context```, implements a proper constructor
|
31
|
+
(see example), and finally implement the main rendering ```#to_hash``` method (used as the "operation" in the Composite pattern
|
32
|
+
terminology).
|
30
33
|
|
31
|
-
The ```
|
32
|
-
|
33
|
-
Outside of Rails application, ```view_context``` can be any other object holding application helpers or state. All
|
34
|
-
subclasses of ```Compositor::Leaf``` inherit view_context reference, and can use it to construct Hash representations.
|
34
|
+
The ```context``` variable is a reference to an object holding necessary helpers for generating JSON, for example
|
35
|
+
Rails Controllers expose a ```view_context``` instance, which contains helper methods necessary to generate application URLs.
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
Outside of Rails application, ```context``` can be any other object holding application helpers or state. All
|
38
|
+
subclasses of ```Compositor::Leaf``` such as ```UserCompositor``` inherit ```context``` attribute and accessors, and so can
|
39
|
+
use the context in generating URLs, or calling any other application helpers.
|
39
40
|
|
40
|
-
```
|
41
|
-
|
41
|
+
We recommend that you place your Compositor classes in eg ```app/compositors/*``` directory, which defines one
|
42
|
+
(or more than one) compositor(s) per model class you will be rendering.
|
42
43
|
|
44
|
+
```ruby
|
45
|
+
# File: app/compositors/user_compositor.rb
|
43
46
|
class UserCompositor < Compositor::Leaf
|
44
47
|
attr_accessor :user
|
45
48
|
|
@@ -55,27 +58,75 @@ class UserCompositor < Compositor::Leaf
|
|
55
58
|
location: user.location,
|
56
59
|
bio: user.bio,
|
57
60
|
url: user.url,
|
58
|
-
image_url: context.image_path(user.avatar),
|
61
|
+
image_url: context.image_path(user.avatar), # using context to generate URL path from routes
|
59
62
|
...
|
60
63
|
}
|
61
64
|
end
|
62
65
|
end
|
63
66
|
```
|
64
67
|
|
65
|
-
|
66
|
-
important attributes.
|
68
|
+
You could create this class directly, as in
|
67
69
|
|
68
|
-
|
69
|
-
Composite::Map or Composite::List to create a Hash or an Array as the top-level JSON data structure.
|
70
|
+
```ruby
|
70
71
|
|
71
|
-
|
72
|
-
|
72
|
+
uc = UserCompositor.new(view_context, user, {})
|
73
|
+
uc.to_hash # => returns a Hash representation
|
74
|
+
uc.to_json # => calls to_hash, and then renders JSON
|
75
|
+
```
|
73
76
|
|
74
|
-
|
75
|
-
|
77
|
+
But constructing trees of objects that represent a complex API responses requires a lot more than that, such as
|
78
|
+
constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each
|
79
|
+
inner Hash comes with a "root" element, such as ```:product => { :id => 1, ... }``` where ```:product``` is the root
|
80
|
+
element.
|
81
|
+
|
82
|
+
So here is how to create a list of users in this way, but explicitly declaring classes:
|
76
83
|
|
77
84
|
```ruby
|
85
|
+
compositor = Compositor::Map.new(view_context,
|
86
|
+
:collection => @users.map{|user| UserCompositor.new(view_context, user, { :root => true }),
|
87
|
+
:root => :users
|
88
|
+
```
|
89
|
+
|
90
|
+
When calling ```to_hash``` on the top level compositor, we get:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
:users => {
|
94
|
+
:user => {
|
95
|
+
id: 1234,
|
96
|
+
username: "kigster",
|
97
|
+
location: "San Francisco",
|
98
|
+
bio: "",
|
99
|
+
url: "",
|
100
|
+
image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"
|
101
|
+
},
|
102
|
+
:user => {
|
103
|
+
id: 1235,
|
104
|
+
username: "johnny",
|
105
|
+
location: "Sunnyvale",
|
106
|
+
bio: "",
|
107
|
+
url: "",
|
108
|
+
image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"
|
109
|
+
}
|
110
|
+
}
|
111
|
+
```
|
112
|
+
|
113
|
+
So this is how you can assemple multiple compositors together without the DSL.
|
114
|
+
|
115
|
+
But the real power of this gem is in the additional DSL class, that dramatically simplifies definition
|
116
|
+
of complex responses, as described below.
|
117
|
+
|
118
|
+
### Using the DSL
|
78
119
|
|
120
|
+
```UserCompositor``` class, when defined, automatically adds a ```user``` method to the DSL class, which effictively
|
121
|
+
instantiates the new UserCompositor instance, passing the context into it automatically.
|
122
|
+
|
123
|
+
Using built-in ```Compositor::Map``` and ```Compositor::List``` we can construct multiple objects into a larger
|
124
|
+
hierarchy.
|
125
|
+
|
126
|
+
In the example below, an application is assumed to define ```StoreCompositor```and ```ProductCompositor``` classes
|
127
|
+
similar to ```UserCompositor```, but wrapping ```Store``` and ```Product``` ActiveRecord models.
|
128
|
+
|
129
|
+
```ruby
|
79
130
|
compositor = Compositor::DSL.create(context) do
|
80
131
|
map do
|
81
132
|
store @store, root: :store
|
@@ -86,8 +137,8 @@ that similar to ```UserCompositor``` return hash representations of each model o
|
|
86
137
|
end
|
87
138
|
end
|
88
139
|
|
140
|
+
# now we can call to_hash or to_json on the compositor:
|
89
141
|
puts compositor.to_hash # =>
|
90
|
-
|
91
142
|
{
|
92
143
|
:store => {
|
93
144
|
id: 12354,
|
@@ -104,16 +155,39 @@ that similar to ```UserCompositor``` return hash representations of each model o
|
|
104
155
|
url: "",
|
105
156
|
image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"
|
106
157
|
},
|
107
|
-
:products =>
|
108
|
-
|
109
|
-
|
158
|
+
:products => [
|
159
|
+
{ id: 1234, :name => "Awesome Product", ... },
|
160
|
+
{ id: 4325, :name => "Another Awesome Product", ... }
|
110
161
|
}
|
111
162
|
}
|
112
163
|
```
|
113
164
|
|
114
|
-
|
115
|
-
|
116
|
-
|
165
|
+
Inside the list definition above, ```@products``` is a collection of Products, ```ActiveRecord``` objects,
|
166
|
+
and the block maps each to a Compositor using ```product()``` method, registered by ProductCompositor.
|
167
|
+
|
168
|
+
### Instance Variables
|
169
|
+
|
170
|
+
One thing to note, is that when ```Compositor::DSL``` is used, the gem copies all instance variables
|
171
|
+
from the ```context``` into the DSL instance, so in the above example instance variable ```@user```
|
172
|
+
was defined on ```view_context``` (by Rails, which copies them from the Controller instance), and
|
173
|
+
so became automatically available inside DSL. Note that all instance variables must be
|
174
|
+
defined *before* the DSL instance is created.
|
175
|
+
|
176
|
+
### Method Name Collisions in the DSL
|
177
|
+
|
178
|
+
Because DSL uses only the last word of the class name (eg, ```user``` for a class named ```MyModule::UserCompositor```),
|
179
|
+
there is a possibility of name collisions. In order to prevent that, Compositor will detect if a DSL method is already
|
180
|
+
defined and throw exception if another class tries to redefine it.
|
181
|
+
|
182
|
+
If you prefer to have your own ```Compositor``` class hierarchy, or just compositors that should not be added to the
|
183
|
+
DSL, you can name the classes starting with ```Abstract```, such as ```MyModule::AbstractCompositor```.
|
184
|
+
|
185
|
+
### Performance
|
186
|
+
|
187
|
+
Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses
|
188
|
+
with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating
|
189
|
+
multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API
|
190
|
+
request is completely reasonable.
|
117
191
|
|
118
192
|
## Contributing
|
119
193
|
|
data/lib/compositor/base.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module Compositor
|
2
|
+
class MethodAlreadyDefinedError < RuntimeError; end
|
3
|
+
|
2
4
|
class Base
|
3
5
|
attr_reader :attrs
|
4
6
|
attr_accessor :root, :context
|
@@ -41,7 +43,11 @@ module Compositor
|
|
41
43
|
|
42
44
|
def self.inherited(subclass)
|
43
45
|
method_name = root_class_name(subclass)
|
44
|
-
unless method_name.eql?("base")
|
46
|
+
unless method_name.eql?("base") || method_name.start_with?("abstract")
|
47
|
+
# check if it's already defined
|
48
|
+
if Compositor::DSL.instance_methods.include?(method_name.to_sym)
|
49
|
+
raise MethodAlreadyDefinedError.new("Method #{method_name} is already defined on the DSL class.")
|
50
|
+
end
|
45
51
|
Compositor::DSL.send(:define_method, method_name) do |*args, &block|
|
46
52
|
subclass.
|
47
53
|
new(@context, *args).
|
data/lib/compositor/version.rb
CHANGED
@@ -9,5 +9,36 @@ describe Compositor::Base do
|
|
9
9
|
it "returns the underscored class name with compositor stripped out" do
|
10
10
|
Compositor::Base.root_class_name(DslStringCompositor).should == "dsl_string"
|
11
11
|
end
|
12
|
+
|
13
|
+
it "raises exception when two subclasses that clash on the same name are defined" do
|
14
|
+
block = lambda {
|
15
|
+
# first class
|
16
|
+
class UserCompositor < Compositor::Leaf
|
17
|
+
end
|
18
|
+
|
19
|
+
# 2nd class
|
20
|
+
module ::Foo
|
21
|
+
class UserCompositor < Compositor::Leaf
|
22
|
+
end
|
23
|
+
end
|
24
|
+
}
|
25
|
+
expect { block.call }.to raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not add DSL when class name begins with Abstract" do
|
29
|
+
block = lambda {
|
30
|
+
class AbstractUserCompositor < Compositor::Leaf
|
31
|
+
end
|
32
|
+
# Normally this would cause an exception, but since they both
|
33
|
+
# start with Abstract, neither is added to the DSL.
|
34
|
+
module ::Foo
|
35
|
+
class AbstractUserCompositor < Compositor::Leaf
|
36
|
+
end
|
37
|
+
end
|
38
|
+
}
|
39
|
+
expect { block.call }.not_to raise_error
|
40
|
+
Compositor::DSL.instance_methods.should_not include(:abstract_user)
|
41
|
+
end
|
42
|
+
|
12
43
|
end
|
13
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: compositor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: oj
|
@@ -136,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
136
|
version: '0'
|
137
137
|
requirements: []
|
138
138
|
rubyforge_project:
|
139
|
-
rubygems_version: 1.8.
|
139
|
+
rubygems_version: 1.8.24
|
140
140
|
signing_key:
|
141
141
|
specification_version: 3
|
142
142
|
summary: Composite design pattern with a convenient DSL for building JSON/Hashes of
|
@@ -151,3 +151,4 @@ test_files:
|
|
151
151
|
- spec/compositor/performance_spec.rb
|
152
152
|
- spec/spec_helper.rb
|
153
153
|
- spec/support/sample_dsl.rb
|
154
|
+
has_rdoc:
|