brainstem 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NDQ4OWNhYzY3M2FjZDI3NTAwMGY3MjhmNTc4NTFmZTY5NmU0MDc4YQ==
5
+ data.tar.gz: !binary |-
6
+ MjNhMGMzZjdlYTE0MzZkZjJmNzNhN2VkZDUwYzJmOWMyMGI4ZDRkZQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZmI4MDIwN2JjODg0YWMxYzFjODBjYWY4ZDdmMDgzNzM4YWFjNGQyY2Q4NTg4
10
+ ZGI2NDljNzdkNjBkYzNmNjkzZWUyNjIzZjBiNjc4OWUxMGUzZDFhYjY3Yzhj
11
+ MzM2YTA5ZmY1OTM1MmU0MGU5NDM0ZTk4OGU1NDdhY2RlZTE4Mjg=
12
+ data.tar.gz: !binary |-
13
+ OWExMGE5Nzk5ODk2NzBjMjJhNzhjZWM4NTRiNjE4MWY5NDk0MDExNTNiZTk4
14
+ ZmYzNzkwNzE0Mzc1MTIzYmQ1Y2ZhYjYzOTU1MGQ0MjI3N2YwMTNjODFlNGFm
15
+ OTI5NDkwMjQzNDQwNDI1N2JiMWFjNGFmZjMxZGI3YjAzMGVlZGU=
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ + **0.2.5** - _07/22/2014_
4
+
5
+ + `Brainstem::Presenter#load_associations!` now:
6
+ + polymorphic 'belongs_to' association is represented as a hash which includes:
7
+ + id: The id of the polymorphic object
8
+ + key: The table name for the class of the polymorphic object
9
+
3
10
  + **0.2.4** - _01/9/2014_
4
11
 
5
12
  + `Brainstem::ControllerMethods#present_object` now simulates an only request (by providing the `only` parameter to Brainstem) when attempting to present a single model.
@@ -1,42 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainstem (0.2.4)
5
- activerecord (>= 3.0)
4
+ brainstem (0.2.5)
5
+ activerecord (>= 3.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (3.2.13)
11
- activesupport (= 3.2.13)
12
- builder (~> 3.0.0)
13
- activerecord (3.2.13)
14
- activemodel (= 3.2.13)
15
- activesupport (= 3.2.13)
16
- arel (~> 3.0.2)
17
- tzinfo (~> 0.3.29)
18
- activesupport (3.2.13)
19
- i18n (= 0.6.1)
20
- multi_json (~> 1.0)
21
- arel (3.0.2)
22
- builder (3.0.4)
23
- diff-lcs (1.2.2)
24
- i18n (0.6.1)
25
- multi_json (1.7.2)
26
- rake (10.0.4)
27
- redcarpet (2.2.2)
28
- rr (1.0.5)
29
- rspec (2.13.0)
30
- rspec-core (~> 2.13.0)
31
- rspec-expectations (~> 2.13.0)
32
- rspec-mocks (~> 2.13.0)
33
- rspec-core (2.13.1)
34
- rspec-expectations (2.13.0)
35
- diff-lcs (>= 1.1.3, < 2.0)
36
- rspec-mocks (2.13.1)
37
- sqlite3 (1.3.7)
38
- tzinfo (0.3.37)
39
- yard (0.8.5.2)
10
+ activemodel (4.1.6)
11
+ activesupport (= 4.1.6)
12
+ builder (~> 3.1)
13
+ activerecord (4.1.6)
14
+ activemodel (= 4.1.6)
15
+ activesupport (= 4.1.6)
16
+ arel (~> 5.0.0)
17
+ activesupport (4.1.6)
18
+ i18n (~> 0.6, >= 0.6.9)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.1)
22
+ tzinfo (~> 1.1)
23
+ arel (5.0.1.20140414130214)
24
+ builder (3.2.2)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.6.11)
27
+ json (1.8.1)
28
+ minitest (5.4.2)
29
+ rake (10.3.2)
30
+ redcarpet (3.1.2)
31
+ rr (1.1.2)
32
+ rspec (3.1.0)
33
+ rspec-core (~> 3.1.0)
34
+ rspec-expectations (~> 3.1.0)
35
+ rspec-mocks (~> 3.1.0)
36
+ rspec-core (3.1.6)
37
+ rspec-support (~> 3.1.0)
38
+ rspec-expectations (3.1.2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.1.0)
41
+ rspec-mocks (3.1.3)
42
+ rspec-support (~> 3.1.0)
43
+ rspec-support (3.1.2)
44
+ sqlite3 (1.3.9)
45
+ thread_safe (0.3.4)
46
+ tzinfo (1.2.2)
47
+ thread_safe (~> 0.1)
48
+ yard (0.8.7.4)
40
49
 
41
50
  PLATFORMS
42
51
  ruby
data/README.md CHANGED
@@ -26,68 +26,75 @@ Add this line to your application's Gemfile:
26
26
 
27
27
  Create a class that inherits from Brainstem::Presenter, named after the model that you want to present, and preferrably versioned in a module. For example:
28
28
 
29
- module Api
30
- module V1
31
- class WidgetPresenter < Brainstem::Presenter
32
- presents "Widget"
33
-
34
- # Available sort orders to expose through the API
35
- sort_order :updated_at, "widgets.updated_at"
36
- sort_order :created_at, "widgets.created_at"
37
-
38
- # Default sort order to apply
39
- default_sort_order "updated_at:desc"
40
-
41
- # Optional filter that delegates to the Widget model :popular scope,
42
- # which should take one argument of true or false.
43
- filter :popular
44
-
45
- # Optional filter that applies a lambda.
46
- filter :location_name do |scope, location_name|
47
- scope.joins(:locations).where("locations.name = ?", location_name)
48
- end
49
-
50
- # Filter with an overridable default that runs on all requests.
51
- filter :include_legacy_widgets, :default => false do |scope, bool|
52
- bool ? scope : scope.without_legacy_widgets
53
- end
54
-
55
- # Return a ruby hash that can be converted to JSON
56
- def present(widget)
57
- {
58
- :name => widget.name,
59
- :legacy => widget.legacy?,
60
- :updated_at => widget.updated_at,
61
- :created_at => widget.created_at,
62
- # Associations can be included by request
63
- :features => association(:features),
64
- :location => association(:location)
65
- }
66
- end
67
- end
68
- end
69
- end
70
-
71
- Once you've created a presenter like the one above, pass requests through from your controller.
29
+ ```ruby
30
+ module Api
31
+ module V1
32
+ class WidgetPresenter < Brainstem::Presenter
33
+ presents "Widget"
34
+
35
+ # Available sort orders to expose through the API
36
+ sort_order :updated_at, "widgets.updated_at"
37
+ sort_order :created_at, "widgets.created_at"
38
+
39
+ # Default sort order to apply
40
+ default_sort_order "updated_at:desc"
41
+
42
+ # Optional filter that delegates to the Widget model :popular scope,
43
+ # which should take one argument of true or false.
44
+ filter :popular
45
+
46
+ # Optional filter that applies a lambda.
47
+ filter :location_name do |scope, location_name|
48
+ scope.joins(:locations).where("locations.name = ?", location_name)
49
+ end
72
50
 
73
- class Api::WidgetsController < ActionController::Base
74
- include Brainstem::ControllerMethods
51
+ # Filter with an overridable default that runs on all requests.
52
+ filter :include_legacy_widgets, :default => false do |scope, bool|
53
+ bool ? scope : scope.without_legacy_widgets
54
+ end
75
55
 
76
- def index
77
- render :json => present("widgets") { Widgets.visible_to(current_user) }
56
+ # Return a ruby hash that can be converted to JSON
57
+ def present(widget)
58
+ {
59
+ :name => widget.name,
60
+ :legacy => widget.legacy?,
61
+ :updated_at => widget.updated_at,
62
+ :created_at => widget.created_at,
63
+ # Associations can be included by request
64
+ :features => association(:features),
65
+ :location => association(:location)
66
+ }
78
67
  end
79
68
  end
69
+ end
70
+ end
71
+ ```
72
+
73
+ Once you've created a presenter like the one above, pass requests through from your controller.
74
+
75
+ ```ruby
76
+ class Api::WidgetsController < ActionController::Base
77
+ include Brainstem::ControllerMethods
78
+
79
+ def index
80
+ render :json => present("widgets") { Widgets.visible_to(current_user) }
81
+ end
82
+ end
83
+ ```
80
84
 
81
85
  The scope passed to `present` could contain any starting conditions that you'd like. Requests can have includes, filters, and sort orders.
82
86
 
83
- GET /api/widgets.json?include=features&sort_order=popularity:desc&location_name=san+francisco
87
+ GET /api/widgets.json?include=features&order=popularity:desc&location_name=san+francisco
88
+
89
+ Additionally, requests can have a 'pretty' parameter. When set to true, responses will be pretty-printed.
90
+ Do this by adding `&pretty=true` to the above example.
84
91
 
85
92
  Responses will look like the following:
86
93
 
87
94
  {
88
95
  # Total number of results that matched the query.
89
96
  count: 5,
90
-
97
+
91
98
  # A lookup table to top-level keys. Necessary
92
99
  # because some objects can have associations of
93
100
  # the same type as themselves.
@@ -95,7 +102,7 @@ Responses will look like the following:
95
102
  { key: "widgets", id: "2" },
96
103
  { key: "widgets", id: "10" }
97
104
  ],
98
-
105
+
99
106
  # Serialized models with any requested associations, keyed by ID.
100
107
 
101
108
  widgets: {
@@ -106,7 +113,7 @@ Responses will look like the following:
106
113
  popularity: 85,
107
114
  location_id: "2"
108
115
  },
109
-
116
+
110
117
  "2": {
111
118
  id: "2",
112
119
  name: "flubber",
@@ -125,26 +132,42 @@ Responses will look like the following:
125
132
 
126
133
  You may want to setup an initializer in `config/initializers/brainstem.rb` like the following:
127
134
 
128
- Brainstem.default_namespace = :v1
129
-
130
- module Api
131
- module V1
132
- module Helper
133
- def current_user
134
- # However you get your current user.
135
- end
136
- end
137
- end
138
- end
139
- Brainstem::Presenter.helper(Api::V1::Helper)
140
-
141
- require 'api/v1/widget_presenter'
142
- require 'api/v1/feature_presenter'
143
- require 'api/v1/location_presenter'
144
- # ...
145
-
146
- # Or you could do something like this:
147
- # Dir[Rails.root.join("lib/api/v1/*_presenter.rb").to_s].each { |p| require p }
135
+ ```ruby
136
+ Brainstem.default_namespace = :v1
137
+
138
+ module Api
139
+ module V1
140
+ module Helper
141
+ def current_user
142
+ # However you get your current user.
143
+ end
144
+ end
145
+ end
146
+ end
147
+ Brainstem::Presenter.helper(Api::V1::Helper)
148
+
149
+ require 'api/v1/widget_presenter'
150
+ require 'api/v1/feature_presenter'
151
+ require 'api/v1/location_presenter'
152
+ # ...
153
+
154
+ # Or you could do something like this:
155
+ # Dir[Rails.root.join("lib/api/v1/*_presenter.rb").to_s].each { |p| require p }
156
+ ```
157
+
158
+ ### A note on Rails 4 Style Scopes
159
+
160
+ In Rails 3 it was acceptable to write scopes like this: `scope :popular, where(:popular => true)`. This was deprecated in Rails 4 in preference of scopes that include a callable object: `scope :popular, lambda { where(:popular) => true }`.
161
+
162
+ If your scope does not take any parameters, this can cause a problem with Brainstem if you use a filter that delegates to that scope in your presenter. (e.g., `filter :popular`). The preferable way to handle this is to write a Brainstem scope that delegates to your model scope:
163
+
164
+ ```ruby
165
+ filter :popular do |scope|
166
+ scope.popular
167
+ end
168
+ ```
169
+
170
+ --
148
171
 
149
172
  For more detailed examples, please see the documentation for methods on {Brainstem::Presenter} and our detailed [Rails example application](https://github.com/mavenlink/brainstem-demo-rails).
150
173
 
@@ -158,16 +181,56 @@ APIs presented with Brainstem are just JSON APIs, so they can be consumed with j
158
181
  results: [
159
182
  { key: "widgets", id: "2" }, { key: "widgets", id: "10" }
160
183
  ],
161
-
184
+
162
185
  widgets: {
163
186
  "10": {
164
187
  id: "10",
165
188
  name: "disco ball",
166
189
 
167
190
 
168
-
169
191
  Brainstem returns objects as top-level hashes and provides a `results` array of `key` and `id` objects for finding the returned data in those hashes. The reason that we use the `results` array is two-fold: 1st) it provides order outside of the serialized objects so that we can provide objects keyed by ID, and 2nd) it allows for polymorphic responses and for objects that have associations of their own type (like posts and replies or tasks and sub-tasks).
170
192
 
193
+ ### Test helpers
194
+
195
+ Brainstem includes some spec helpers for controller specs. In order to use them, you need to include Brainstem in your controller specs by adding the following to `spec/support/brainstem.rb` or in your `spec/spec_helper.rb`:
196
+
197
+ ```ruby
198
+ require 'brainstem/test_helpers'
199
+
200
+ RSpec.configure do |config|
201
+ config.include Brainstem::TestHelpers, type: :controller
202
+ end
203
+ ```
204
+
205
+ Now you are ready to use the `brainstem_data` method.
206
+
207
+ ```ruby
208
+ # Assume user is the model and name is an attribute
209
+
210
+ # Selecting an item from a collection by it's id
211
+ expect(brainstem_data.users.by_id(235).name).to eq('name')
212
+
213
+ # Getting an array of all ids of in a collection without map
214
+ expect(brainstem_data.users.ids).to include(1)
215
+
216
+ # Accessing the keys of a collection
217
+ expect(brainstem_data.users.first.keys).to =~ %w(id name email address)
218
+
219
+ # Using standard array methods on a collection to get by index
220
+ expect(brainstem_data.users.first.name).to eq('name')
221
+ expect(brainstem_data.users[2].name).to eq('name')
222
+ ```
223
+
224
+ An alternate syntax for readability might be:
225
+
226
+ ```ruby
227
+ describe 'brainstem_data' do
228
+ subject { brainstem_data }
229
+
230
+ its('users.ids') { should include(1) }
231
+ end
232
+ ```
233
+
171
234
  ### Brainstem and Backbone.js
172
235
 
173
236
  If you're already using Backbone.js, integrating with a Brainstem API is super simple. Just use the [Brainstem.js](https://github.com/mavenlink/brainstem-js) gem (or its JavaScript contents) to access your relational Brainstem API from JavaScript.
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.require_paths = ["lib"]
18
18
  gem.version = Brainstem::VERSION
19
19
 
20
- gem.add_dependency "activerecord", ">= 3.0"
20
+ gem.add_dependency "activerecord", ">= 3.2"
21
21
 
22
22
  gem.add_development_dependency "rake"
23
23
  gem.add_development_dependency "redcarpet" # for markdown in yard
@@ -5,7 +5,6 @@ require "brainstem/controller_methods"
5
5
 
6
6
  # The Brainstem module itself contains a +default_namespace+ class attribute and a few helpers that make managing +PresenterCollections+ and their corresponding namespaces easier.
7
7
  module Brainstem
8
-
9
8
  # Sets {default_namespace} to a new value.
10
9
  # @param [String] namespace
11
10
  # @return [String] the new default namespace
@@ -59,5 +58,4 @@ module Brainstem
59
58
  def self.logger=(logger)
60
59
  @logger = logger
61
60
  end
62
-
63
61
  end
@@ -6,6 +6,11 @@ module Brainstem
6
6
  # @return [String] The name of the method that is being proxied.
7
7
  attr_reader :method_name
8
8
 
9
+ # @!attribute [rw] ignore_type
10
+ # @return [Boolean] When true, polymorphic associations will be treated like normal ones,
11
+ # skipping the _ref structure and simply using [json_name]_id.
12
+ attr_accessor :ignore_type
13
+
9
14
  # @!attribute [rw] json_name
10
15
  # @return [String] The name of the top-level JSON key for objects provided by this association.
11
16
  attr_accessor :json_name
@@ -25,6 +30,7 @@ module Brainstem
25
30
  options = args.last.is_a?(Hash) ? args.pop : {}
26
31
  method_name = args.first.to_sym if args.first.is_a?(String) || args.first.is_a?(Symbol)
27
32
  @json_name = options[:json_name]
33
+ @ignore_type = options[:ignore_type] || false
28
34
  @restrict_to_only = options[:restrict_to_only] || false
29
35
  if block_given?
30
36
  raise ArgumentError, "options[:json_name] is required when using a block" unless options[:json_name]
@@ -27,7 +27,7 @@ module Brainstem
27
27
  # only required if the name cannot be inferred.
28
28
  # @return (see PresenterCollection#presenting)
29
29
  def present_object(objects, options = {})
30
- options.merge!(:params => params)
30
+ options.merge!(:params => params, :apply_default_filters => false)
31
31
 
32
32
  if objects.is_a?(ActiveRecord::Relation) || objects.is_a?(Array)
33
33
  raise ActiveRecord::RecordNotFound if objects.empty?
@@ -155,10 +155,18 @@ module Brainstem
155
155
  id_attr = value.method_name ? "#{value.method_name}_id" : nil
156
156
 
157
157
  if id_attr && model.class.columns_hash.has_key?(id_attr)
158
- struct["#{key}_id".to_sym] = to_s_except_nil(model.send(id_attr))
159
- reflection = value.method_name && model.reflections[value.method_name.to_sym]
160
- if reflection && reflection.options[:polymorphic]
161
- struct["#{key.to_s.singularize}_type".to_sym] = model.send("#{value.method_name}_type")
158
+ reflection = value.method_name && model.class.reflections[value.method_name.to_sym]
159
+ if reflection && reflection.options[:polymorphic] && !value.ignore_type
160
+ struct["#{key.to_s.singularize}_ref".to_sym] = begin
161
+ if (id_attr = model.send(id_attr)).present?
162
+ {
163
+ :id => to_s_except_nil(id_attr),
164
+ :key => model.send("#{value.method_name}_type").try(:constantize).try(:table_name)
165
+ }
166
+ end
167
+ end
168
+ else
169
+ struct["#{key}_id".to_sym] = to_s_except_nil(model.send(id_attr))
162
170
  end
163
171
  elsif associations.include?(key.to_s)
164
172
  result = value.call(model)