compositor 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,10 +3,12 @@ Compositor
3
3
 
4
4
  [![Build status](https://secure.travis-ci.org/wanelo/compositor.png)](http://travis-ci.org/wanelo/compositor)
5
5
 
6
- Composite 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, converting to
8
- a hash and then JSON.
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 ```view_context```, and implement the ```#to_hash```
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 ```view_context``` variable is a reference to an object holding necessary helpers for generating JSON, for example
32
- view_context is automatically available inside Rails controllers, and contains helper methods necessary to generate application URLs.
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
- We recommend you place your Compositor classes in eg ```app/compositors/*``` directory, that has one compositor
37
- class per model class you will be rendering. Example below would be ```app/compositors/user.rb```, a compositor class
38
- wrapping ```User``` model.
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
- ```ruby
41
- # The actual class name "User" is converted into a DSL method named "user", shown later.
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
- This small class automatically registers "user" DSL method, which receives a user object and any other
66
- important attributes.
68
+ You could create this class directly, as in
67
69
 
68
- Then this class can be merged with other similar "leaf" classes, or another "composite" class, such as
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
- Once the tree of composite objects has been setup, calling #to_hash on the top level object quickly
72
- generates hash by walking the tree and merging everything together.
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
- In the example below, application defines also ```StoreCompositor```, ```ProductCompositor``` classes
75
- that similar to ```UserCompositor``` return hash representations of each model object.
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
- [ id: 1234, :name => "Awesome Product", ... ],
109
- [ id: 4325, :name => "Another Awesome Product", ... ]
158
+ :products => [
159
+ { id: 1234, :name => "Awesome Product", ... },
160
+ { id: 4325, :name => "Another Awesome Product", ... }
110
161
  }
111
162
  }
112
163
  ```
113
164
 
114
- The context is an object that can contain helpers, instance variables, or anything that can be used
115
- within leaves. For example, you can pass in the view_context from the controller to get access to any
116
- Rails routes or helpers.
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
 
@@ -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") # check if it's already defined
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).
@@ -1,3 +1,3 @@
1
1
  module Compositor
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
@@ -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.3
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-05-31 00:00:00.000000000 Z
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.25
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: