brainstem 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +41 -32
- data/README.md +136 -73
- data/brainstem.gemspec +1 -1
- data/lib/brainstem.rb +0 -2
- data/lib/brainstem/association_field.rb +6 -0
- data/lib/brainstem/controller_methods.rb +1 -1
- data/lib/brainstem/presenter.rb +12 -4
- data/lib/brainstem/presenter_collection.rb +26 -5
- data/lib/brainstem/test_helpers.rb +81 -0
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/controller_methods_spec.rb +21 -16
- data/spec/brainstem/presenter_collection_spec.rb +205 -172
- data/spec/brainstem/presenter_spec.rb +70 -47
- data/spec/brainstem_spec.rb +5 -5
- data/spec/spec_helpers/db.rb +2 -1
- metadata +13 -38
- data/pkg/brainstem-0.0.2.gem +0 -0
- data/pkg/brainstem-0.2.1.gem +0 -0
- data/pkg/brainstem-0.2.2.gem +0 -0
- data/pkg/brainstem-0.2.gem +0 -0
checksums.yaml
ADDED
@@ -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=
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
data/Gemfile.lock
CHANGED
@@ -1,42 +1,51 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
brainstem (0.2.
|
5
|
-
activerecord (>= 3.
|
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 (
|
11
|
-
activesupport (=
|
12
|
-
builder (~> 3.
|
13
|
-
activerecord (
|
14
|
-
activemodel (=
|
15
|
-
activesupport (=
|
16
|
-
arel (~>
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
rspec-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
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&
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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.
|
data/brainstem.gemspec
CHANGED
@@ -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.
|
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
|
data/lib/brainstem.rb
CHANGED
@@ -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?
|
data/lib/brainstem/presenter.rb
CHANGED
@@ -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
|
-
|
159
|
-
reflection
|
160
|
-
|
161
|
-
|
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)
|