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 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: