brainstem 0.2.6.1 → 1.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/CHANGELOG.md +16 -2
- data/Gemfile.lock +51 -36
- data/README.md +531 -110
- data/brainstem.gemspec +6 -2
- data/lib/brainstem.rb +25 -9
- data/lib/brainstem/concerns/controller_param_management.rb +22 -0
- data/lib/brainstem/concerns/error_presentation.rb +58 -0
- data/lib/brainstem/concerns/inheritable_configuration.rb +29 -0
- data/lib/brainstem/concerns/lookup.rb +30 -0
- data/lib/brainstem/concerns/presenter_dsl.rb +111 -0
- data/lib/brainstem/controller_methods.rb +17 -8
- data/lib/brainstem/dsl/association.rb +55 -0
- data/lib/brainstem/dsl/associations_block.rb +12 -0
- data/lib/brainstem/dsl/base_block.rb +31 -0
- data/lib/brainstem/dsl/conditional.rb +25 -0
- data/lib/brainstem/dsl/conditionals_block.rb +15 -0
- data/lib/brainstem/dsl/configuration.rb +112 -0
- data/lib/brainstem/dsl/field.rb +68 -0
- data/lib/brainstem/dsl/fields_block.rb +25 -0
- data/lib/brainstem/preloader.rb +98 -0
- data/lib/brainstem/presenter.rb +325 -134
- data/lib/brainstem/presenter_collection.rb +82 -286
- data/lib/brainstem/presenter_validator.rb +96 -0
- data/lib/brainstem/query_strategies/README.md +107 -0
- data/lib/brainstem/query_strategies/base_strategy.rb +62 -0
- data/lib/brainstem/query_strategies/filter_and_search.rb +50 -0
- data/lib/brainstem/query_strategies/filter_or_search.rb +103 -0
- data/lib/brainstem/test_helpers.rb +5 -1
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/concerns/controller_param_management_spec.rb +42 -0
- data/spec/brainstem/concerns/error_presentation_spec.rb +113 -0
- data/spec/brainstem/concerns/inheritable_configuration_spec.rb +210 -0
- data/spec/brainstem/concerns/presenter_dsl_spec.rb +412 -0
- data/spec/brainstem/controller_methods_spec.rb +15 -27
- data/spec/brainstem/dsl/association_spec.rb +123 -0
- data/spec/brainstem/dsl/conditional_spec.rb +93 -0
- data/spec/brainstem/dsl/configuration_spec.rb +1 -0
- data/spec/brainstem/dsl/field_spec.rb +212 -0
- data/spec/brainstem/preloader_spec.rb +137 -0
- data/spec/brainstem/presenter_collection_spec.rb +565 -244
- data/spec/brainstem/presenter_spec.rb +726 -167
- data/spec/brainstem/presenter_validator_spec.rb +209 -0
- data/spec/brainstem/query_strategies/filter_and_search_spec.rb +46 -0
- data/spec/brainstem/query_strategies/filter_or_search_spec.rb +45 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/spec_helpers/db.rb +32 -65
- data/spec/spec_helpers/presenters.rb +124 -29
- data/spec/spec_helpers/rr.rb +11 -0
- data/spec/spec_helpers/schema.rb +115 -0
- metadata +126 -30
- data/lib/brainstem/association_field.rb +0 -53
- data/lib/brainstem/engine.rb +0 -4
- data/pkg/brainstem-0.2.5.gem +0 -0
- data/pkg/brainstem-0.2.6.gem +0 -0
- data/spec/spec_helpers/cleanup.rb +0 -23
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZDY4MmYyNjBlZTNlMDJmMmU3Yjk1MDZhYjJhNTY0MDQ3NmY3MjRmMg==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a8bd5fc16e1e1886466bd5d575c4ea7217a7bf0d
|
4
|
+
data.tar.gz: 0cc3b8df6885253815b47108273153e67105bfd2
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
NzhmYzViODBmN2M2NmFkODk4NTdiM2U4YzBhNTZjODQ1NTllZmUxOWUzZWM3
|
11
|
-
MzFiMmEzMDViNGJkNjkwMmM0NzE1MTg0N2JhYjJkMzVmYzM1NTk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
Mzk5ZmNlMmQ4ZGMxODI0OTY0ZjU1Y2FlYjQ1MDAwOWJiZjk0MDNlMzJhNjVi
|
14
|
-
N2RmMjkwZTUzZjJmM2UyOWE3OWQ1MTMwMzBhMWEyMjZjNjY1MDgwNGY5MTJj
|
15
|
-
YmI2NDBjZTA1N2EzM2Q3ZjFkZWRkNzk3NGYyZDBjZjdjNzY5MGM=
|
6
|
+
metadata.gz: 3aa58b60f630f1f89875a7afce69e51faf6745c2b8a1f93495d57f754a208b17d80736aba4fd2ca2e08612bd41e570441e6faf19bca5cb70204a0e7678d2c1ad
|
7
|
+
data.tar.gz: 9b090b4841129d96d5581ee116eba190e40cfcd214fd1893845e8019a563f8c90c9214c298ba40e7e6047cb8501093162c2fce74b2517a18492f5752be796167
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
+ **1.0.0.pre.1** - _03/07/2017_
|
4
|
+
- Implemented new presenter DSL.
|
5
|
+
- Added controller helpers for presenting errors.
|
6
|
+
- Added support for optional fields.
|
7
|
+
- Added support for filtering in conjunction with your search implementation.
|
8
|
+
- Added support for defining lookup caches for dynamic fields.
|
9
|
+
- Fixed: documentation for default filters.
|
10
|
+
- Fixed: ambiguity of `brainstem_key` for presenters that present multiple classes.
|
11
|
+
- Fixed: non-deterministic order when sorting records with identical sortable fields (`updated_at`, for instance).
|
12
|
+
|
13
|
+
+ **1.0.0.pre** - _10/5/2015_
|
14
|
+
|
15
|
+
+ Complete rewrite of the Presenter DSL allowing for introspection and (soon) automatic API documentation.
|
16
|
+
|
3
17
|
+ **0.2.5** - _07/22/2014_
|
4
18
|
|
5
19
|
+ `Brainstem::Presenter#load_associations!` now:
|
@@ -14,7 +28,7 @@
|
|
14
28
|
+ **0.2.3** - _11/21/2013_
|
15
29
|
|
16
30
|
+ `Brainstem::ControllerMethods#present_object` now runs the default filters that are defined in the presenter.
|
17
|
-
|
31
|
+
|
18
32
|
+ `Brainstem.presenter_collection` now takes two optional options:
|
19
33
|
+ `raise_on_empty` - Boolean that defaults to false and when set to true will raise an exception (default: `ActiveRecord::RecordNotFound`) when the result set is empty.
|
20
|
-
+ `empty_error_class` - Exception class to raise when `raise_on_empty` is true.
|
34
|
+
+ `empty_error_class` - Exception class to raise when `raise_on_empty` is true.
|
data/Gemfile.lock
CHANGED
@@ -1,60 +1,75 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
brainstem (0.
|
5
|
-
activerecord (>=
|
4
|
+
brainstem (1.0.0.pre.1)
|
5
|
+
activerecord (>= 4.1)
|
6
|
+
activesupport (>= 4.1)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
10
|
-
activemodel (
|
11
|
-
activesupport (=
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
activemodel (5.0.2)
|
12
|
+
activesupport (= 5.0.2)
|
13
|
+
activerecord (5.0.2)
|
14
|
+
activemodel (= 5.0.2)
|
15
|
+
activesupport (= 5.0.2)
|
16
|
+
arel (~> 7.0)
|
17
|
+
activesupport (5.0.2)
|
18
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
19
|
i18n (~> 0.7)
|
19
|
-
json (~> 1.7, >= 1.7.7)
|
20
20
|
minitest (~> 5.1)
|
21
|
-
thread_safe (~> 0.3, >= 0.3.4)
|
22
21
|
tzinfo (~> 1.1)
|
23
|
-
arel (
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
22
|
+
arel (7.1.4)
|
23
|
+
coderay (1.1.1)
|
24
|
+
concurrent-ruby (1.0.5)
|
25
|
+
database_cleaner (1.5.3)
|
26
|
+
diff-lcs (1.3)
|
27
|
+
i18n (0.8.1)
|
28
|
+
method_source (0.8.2)
|
29
|
+
minitest (5.10.1)
|
30
|
+
pry (0.10.4)
|
31
|
+
coderay (~> 1.1.0)
|
32
|
+
method_source (~> 0.8.1)
|
33
|
+
slop (~> 3.4)
|
34
|
+
pry-nav (0.2.4)
|
35
|
+
pry (>= 0.9.10, < 0.11.0)
|
36
|
+
rake (12.0.0)
|
37
|
+
redcarpet (3.4.0)
|
38
|
+
rr (1.2.0)
|
39
|
+
rspec (3.5.0)
|
40
|
+
rspec-core (~> 3.5.0)
|
41
|
+
rspec-expectations (~> 3.5.0)
|
42
|
+
rspec-mocks (~> 3.5.0)
|
43
|
+
rspec-core (3.5.4)
|
44
|
+
rspec-support (~> 3.5.0)
|
45
|
+
rspec-expectations (3.5.0)
|
39
46
|
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
-
rspec-support (~> 3.
|
41
|
-
rspec-mocks (3.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
rspec-support (~> 3.5.0)
|
48
|
+
rspec-mocks (3.5.0)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.5.0)
|
51
|
+
rspec-support (3.5.0)
|
52
|
+
slop (3.6.0)
|
53
|
+
sqlite3 (1.3.13)
|
54
|
+
thread_safe (0.3.6)
|
46
55
|
tzinfo (1.2.2)
|
47
56
|
thread_safe (~> 0.1)
|
48
|
-
yard (0.8
|
57
|
+
yard (0.9.8)
|
49
58
|
|
50
59
|
PLATFORMS
|
51
60
|
ruby
|
52
61
|
|
53
62
|
DEPENDENCIES
|
54
63
|
brainstem!
|
64
|
+
database_cleaner
|
65
|
+
pry
|
66
|
+
pry-nav
|
55
67
|
rake
|
56
68
|
redcarpet
|
57
69
|
rr
|
58
|
-
rspec
|
70
|
+
rspec (~> 3.5)
|
59
71
|
sqlite3
|
60
72
|
yard
|
73
|
+
|
74
|
+
BUNDLED WITH
|
75
|
+
1.13.6
|
data/README.md
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
If you're upgrading from an older version of Brainstem, please see [Upgrading From The Pre 1.0 Brainstem](https://github.com/mavenlink/brainstem#upgrading-from-the-pre-10-brainstem) and the rest of this README.
|
2
|
+
|
1
3
|
# Brainstem
|
2
4
|
|
5
|
+
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mavenlink/brainstem?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
6
|
+
|
3
7
|
[![Build Status](https://travis-ci.org/mavenlink/brainstem.png)](https://travis-ci.org/mavenlink/brainstem)
|
4
8
|
|
5
|
-
Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles
|
9
|
+
Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles
|
10
|
+
converting ActiveRecord objects into structured JSON and a set of API abstractions that allow users to request sorts,
|
11
|
+
filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.
|
6
12
|
|
7
13
|
## Why Brainstem?
|
8
14
|
|
@@ -10,11 +16,12 @@ Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a
|
|
10
16
|
* Version your Presenters for consistency as your API evolves.
|
11
17
|
* Expose end-user selectable filters and sorts.
|
12
18
|
* Whitelist your existing scopes to act as API filters for your users.
|
13
|
-
* Allow users to side-load multiple objects, with their associations, in a single request, reducing the number of
|
19
|
+
* Allow users to side-load multiple objects, with their associations, in a single request, reducing the number of
|
20
|
+
requests needed to get the job done. This is especially helpful for building speedy mobile applications.
|
14
21
|
* Prevent data duplication by pulling associations into top-level hashes, easily indexable by ID.
|
15
|
-
* Easy integration with Backbone.js. "It's like Ember Data for Backbone.js!"
|
22
|
+
* Easy integration with Backbone.js via [brainstem-js](https://github.com/mavenlink/brainstem-js). "It's like Ember Data for Backbone.js!"
|
16
23
|
|
17
|
-
|
24
|
+
[Watch our talk about Brainstem from RailsConf 2013](http://www.confreaks.com/videos/2457-railsconf2013-introducing-brainstem-your-companion-for-rich-rails-apis)
|
18
25
|
|
19
26
|
## Installation
|
20
27
|
|
@@ -24,13 +31,16 @@ Add this line to your application's Gemfile:
|
|
24
31
|
|
25
32
|
## Usage
|
26
33
|
|
27
|
-
|
34
|
+
### Make a Presenter
|
35
|
+
|
36
|
+
Create a class that inherits from Brainstem::Presenter, named after the model that you want to present, and preferrably
|
37
|
+
versioned in a module. For example `lib/api/v1/widget_presenter.rb`:
|
28
38
|
|
29
39
|
```ruby
|
30
40
|
module Api
|
31
41
|
module V1
|
32
42
|
class WidgetPresenter < Brainstem::Presenter
|
33
|
-
presents
|
43
|
+
presents Widget
|
34
44
|
|
35
45
|
# Available sort orders to expose through the API
|
36
46
|
sort_order :updated_at, "widgets.updated_at"
|
@@ -39,140 +49,244 @@ module Api
|
|
39
49
|
# Default sort order to apply
|
40
50
|
default_sort_order "updated_at:desc"
|
41
51
|
|
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
52
|
# Optional filter that applies a lambda.
|
47
53
|
filter :location_name do |scope, location_name|
|
48
54
|
scope.joins(:locations).where("locations.name = ?", location_name)
|
49
55
|
end
|
50
56
|
|
51
|
-
# Filter with an overridable default
|
52
|
-
|
57
|
+
# Filter with an overridable default. This will run on every request,
|
58
|
+
# passing in `bool` as `false` unless a user has specified otherwise.
|
59
|
+
filter :include_legacy_widgets, default: false do |scope, bool|
|
53
60
|
bool ? scope : scope.without_legacy_widgets
|
54
61
|
end
|
55
62
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
63
|
+
# The top-level JSON key in which these presented records will be returned.
|
64
|
+
# This is optional and defaults to the model's table name.
|
65
|
+
brainstem_key :widgets
|
66
|
+
|
67
|
+
# Specify the fields to be present in the returned JSON.
|
68
|
+
fields do
|
69
|
+
field :name, :string, "the Widget's name"
|
70
|
+
field :legacy, :boolean, "true for legacy Widgets, false otherwise", via: :legacy?
|
71
|
+
field :longform_description, :string, "feature-length description of this Widget", optional: true
|
72
|
+
field :updated_at, :datetime, "the time of this Widget's last update"
|
73
|
+
field :created_at, :datetime, "the time at which this Widget was created"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Associations can be included by providing include=association_name in the URL.
|
77
|
+
# IDs for belongs_to associations will be returned for free if they're native
|
78
|
+
# columns on the model, otherwise the user must explicitly request associations
|
79
|
+
# to avoid unnecessary loads.
|
80
|
+
associations do
|
81
|
+
association :features, Feature, "features associated with this Widget"
|
82
|
+
association :location, Location, "the location of this Widget"
|
67
83
|
end
|
68
84
|
end
|
69
85
|
end
|
70
86
|
end
|
71
87
|
```
|
72
88
|
|
73
|
-
|
89
|
+
### Setup your Controller
|
90
|
+
|
91
|
+
Once you've created a presenter like the one above, pass requests through from your Controller.
|
74
92
|
|
75
93
|
```ruby
|
76
94
|
class Api::WidgetsController < ActionController::Base
|
77
95
|
include Brainstem::ControllerMethods
|
78
96
|
|
79
97
|
def index
|
80
|
-
render :
|
98
|
+
render json: brainstem_present("widgets") { Widgets.visible_to(current_user) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def show
|
102
|
+
widget = Widget.find(params[:id])
|
103
|
+
render json: brainstem_present_object(widget)
|
104
|
+
end
|
105
|
+
|
106
|
+
def create
|
107
|
+
# Note: you are in charge of sanitizing params[brainstem_model_name], likely with strong parameters.
|
108
|
+
widget = Widget.new(params[brainstem_model_name])
|
109
|
+
if widget.save
|
110
|
+
render json: brainstem_present_object(widget)
|
111
|
+
else
|
112
|
+
render json: brainstem_model_error(widget), status: :unprocessable_entity
|
113
|
+
end
|
81
114
|
end
|
82
115
|
end
|
83
116
|
```
|
84
117
|
|
85
|
-
The
|
118
|
+
The `Brainstem::ControllerMethods` concern provides:
|
119
|
+
* `brainstem_model_name` which is inferred from your controller name or settable with `self.brainstem_model_name = :thing`.
|
120
|
+
* `brainstem_present` and `brainstem_present_object` for presenting a scope of models or a single model.
|
121
|
+
* `brainstem_model_error` and `brainstem_system_error` for presenting model and system error messages.
|
86
122
|
|
87
|
-
|
123
|
+
### Controller Best Practices
|
88
124
|
|
89
|
-
|
125
|
+
We recommend that your base API controller look something like the following.
|
90
126
|
|
91
|
-
|
92
|
-
|
93
|
-
|
127
|
+
```ruby
|
128
|
+
module Api
|
129
|
+
module V1
|
130
|
+
class ApiController < ApplicationController
|
131
|
+
include Brainstem::ControllerMethods
|
94
132
|
|
95
|
-
|
96
|
-
# because some objects can have associations of
|
97
|
-
# the same type as themselves.
|
98
|
-
results: [
|
99
|
-
{ key: "widgets", id: "2" },
|
100
|
-
{ key: "widgets", id: "10" }
|
101
|
-
],
|
133
|
+
before_filter :api_authenticate
|
102
134
|
|
103
|
-
|
135
|
+
rescue_from StandardError, with: :server_error
|
136
|
+
rescue_from Brainstem::SearchUnavailableError, with: :search_unavailable
|
137
|
+
rescue_from ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed
|
138
|
+
rescue_from ActiveRecord::RecordNotFound,
|
139
|
+
ActionController::RoutingError, with: :page_not_found
|
104
140
|
|
105
|
-
|
106
|
-
"10": {
|
107
|
-
id: "10",
|
108
|
-
name: "disco ball",
|
109
|
-
feature_ids: ["5"],
|
110
|
-
popularity: 85,
|
111
|
-
location_id: "2"
|
112
|
-
},
|
113
|
-
|
114
|
-
"2": {
|
115
|
-
id: "2",
|
116
|
-
name: "flubber",
|
117
|
-
feature_ids: ["6", "12"],
|
118
|
-
popularity: 100,
|
119
|
-
location_id: "2"
|
120
|
-
}
|
121
|
-
},
|
122
|
-
|
123
|
-
features: {
|
124
|
-
"5": { id: "5", name: "shiny" },
|
125
|
-
"6": { id: "6", name: "bouncy" },
|
126
|
-
"12": { id: "12", name: "physically impossible" }
|
127
|
-
}
|
128
|
-
}
|
141
|
+
private
|
129
142
|
|
130
|
-
|
143
|
+
def api_authenticate
|
144
|
+
# Implement your authentication here. We recommend Doorkeeper.
|
145
|
+
end
|
131
146
|
|
132
|
-
|
133
|
-
|
147
|
+
def server_error(exception)
|
148
|
+
render json: brainstem_system_error("A server error has occurred."), status: 500
|
149
|
+
end
|
134
150
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
151
|
+
def search_unavailable
|
152
|
+
render json: brainstem_system_error('Search is currently unavailable'), status: 503
|
153
|
+
end
|
154
|
+
|
155
|
+
def page_not_found
|
156
|
+
render json: brainstem_system_error('Record not found'), status: 404
|
157
|
+
end
|
158
|
+
|
159
|
+
def record_not_destroyed
|
160
|
+
render json: brainstem_model_error("Could not delete the #{brainstem_model_name.humanize.downcase.singularize}"), status: :unprocessable_entity
|
140
161
|
end
|
141
162
|
end
|
142
163
|
end
|
143
164
|
end
|
144
|
-
|
165
|
+
```
|
166
|
+
|
167
|
+
### Setup Rails to load Brainstem
|
168
|
+
|
169
|
+
To configure Brainstem for development and production, we do the following:
|
170
|
+
|
171
|
+
1) We add `lib` to our Rails autoload_paths in application.rb with `config.autoload_paths += "#{config.root}/lib"`
|
145
172
|
|
146
|
-
|
147
|
-
require 'api/v1/feature_presenter'
|
148
|
-
require 'api/v1/location_presenter'
|
149
|
-
# ...
|
173
|
+
2) We setup an initializer in `config/initializers/brainstem.rb`, similar to the following:
|
150
174
|
|
151
|
-
|
152
|
-
#
|
175
|
+
```ruby
|
176
|
+
# In order to support live code reload in the development environment, we register a `to_prepare` callback. This
|
177
|
+
# runs once in production (before the first request) and whenever a file has changed in development.
|
178
|
+
Rails.application.config.to_prepare do
|
179
|
+
# Forget all Brainstem configuration.
|
180
|
+
Brainstem.reset!
|
181
|
+
|
182
|
+
# Set the current default API namespace.
|
183
|
+
Brainstem.default_namespace = :v1
|
184
|
+
|
185
|
+
# (Optional) Load a default base helper into all presenters. You could use this to bring in a concept like `current_user`.
|
186
|
+
# While not necessarily the best approach, something like http://stackoverflow.com/a/11670283 can currently be used to
|
187
|
+
# access the requesting user inside of a Brainstem presenter. We hope to clean this up by allowing a user to be passed in
|
188
|
+
# when presenting in the future.
|
189
|
+
module ApiHelper
|
190
|
+
def current_user
|
191
|
+
Thread.current[:current_user]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
Brainstem::Presenter.helper(ApiHelper)
|
195
|
+
|
196
|
+
# Load the presenters themselves.
|
197
|
+
Dir[Rails.root.join("lib/api/v1/*_presenter.rb").to_s].each { |presenter_path| require_dependency(presenter_path) }
|
198
|
+
end
|
153
199
|
```
|
154
200
|
|
155
|
-
###
|
201
|
+
### Make an API request
|
156
202
|
|
157
|
-
|
203
|
+
The scope passed to `brainstem_present` can contain any starting scope conditions that you'd like. Requests can have
|
204
|
+
includes, filters, and sort orders specified in the params and automatically parsed by Brainstem.
|
158
205
|
|
159
|
-
|
206
|
+
GET /api/widgets.json?include=features&order=created_at:desc&location_name=san+francisco
|
160
207
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
208
|
+
Responses will look like the following:
|
209
|
+
|
210
|
+
```js
|
211
|
+
{
|
212
|
+
# Total number of results that matched the query.
|
213
|
+
count: 5,
|
214
|
+
|
215
|
+
# A lookup table to top-level keys. Necessary
|
216
|
+
# because some objects can have associations of
|
217
|
+
# the same type as themselves. Also helps to
|
218
|
+
# support polymorphic requests.
|
219
|
+
results: [
|
220
|
+
{ key: "widgets", id: "2" },
|
221
|
+
{ key: "widgets", id: "10" }
|
222
|
+
],
|
223
|
+
|
224
|
+
# Serialized models with any requested associations, keyed by ID.
|
225
|
+
|
226
|
+
widgets: {
|
227
|
+
"10": {
|
228
|
+
id: "10",
|
229
|
+
name: "disco ball",
|
230
|
+
feature_ids: ["5"],
|
231
|
+
popularity: 85,
|
232
|
+
location_id: "2"
|
233
|
+
},
|
234
|
+
|
235
|
+
"2": {
|
236
|
+
id: "2",
|
237
|
+
name: "flubber",
|
238
|
+
feature_ids: ["6", "12"],
|
239
|
+
popularity: 100,
|
240
|
+
location_id: "2"
|
241
|
+
}
|
242
|
+
},
|
243
|
+
|
244
|
+
features: {
|
245
|
+
"5": { id: "5", name: "shiny" },
|
246
|
+
"6": { id: "6", name: "bouncy" },
|
247
|
+
"12": { id: "12", name: "physically impossible" }
|
248
|
+
}
|
249
|
+
}
|
165
250
|
```
|
166
251
|
|
252
|
+
#### Valid URL params
|
253
|
+
|
254
|
+
Brainstem parses the request params and supports the following:
|
255
|
+
|
256
|
+
* Use `order` to select a `sort_order`. Seperate the `sort_order` name and direction with a colon, like `"order=created_at:desc"`.
|
257
|
+
* Perform a search with `search`. See the `search` block definition in the Presenter DSL section at the bottom of this README.
|
258
|
+
* To request associations, use the `include` option with a comma-seperated list of association names, for example `"include=features,location"`.
|
259
|
+
* Pagination is supported by providing either the `page` and `per_page` or `limit` and `offset` URL params. You can set
|
260
|
+
legal ranges for these by passing in the `:per_page` and `:max_per_page` options when presenting. The default
|
261
|
+
`per_page` is 20 and the default `:max_per_page` is 200.
|
262
|
+
* Brainstem supports a concept called "only queries" which allow you to request a specific set of records by ID, kind of like
|
263
|
+
a batch show request. These queries are triggered by the presence of the URL param `"only"` with a comma-seperated set
|
264
|
+
of one or more IDs, for example `"only=1,5,7"`. Please note that default filters are still applied to `only` queries, so you will receive
|
265
|
+
only the subset of the requested objects that pass any default filters. To prevent this, you can provide `apply_default_filters=false`
|
266
|
+
as a query param.
|
267
|
+
* Filters are standard URL parameters. To pass an option to a filter named `:location_name`, provide a request param like
|
268
|
+
`location_name=san+francisco`. Because filters are top-level params, avoid naming them after any of the other Brainstem
|
269
|
+
keywords, such as `search`, `page`, `per_page`, `limit`, `offset`, `order`, `only`, or `include`.
|
270
|
+
* Brainstem supports optional fields which will only be returned when requested, for example: `optional_fields=field1,field2`
|
271
|
+
|
167
272
|
--
|
168
273
|
|
169
|
-
For more detailed examples, please see the
|
274
|
+
For more detailed examples, please see the rest of this README and our detailed
|
275
|
+
[Rails example application](https://github.com/mavenlink/brainstem-demo-rails).
|
170
276
|
|
171
277
|
## Consuming a Brainstem API
|
172
278
|
|
173
|
-
APIs presented with Brainstem are just JSON APIs, so they can be consumed with just about any language.
|
279
|
+
APIs presented with Brainstem are just JSON APIs, so they can be consumed with just about any language. As Brainstem
|
280
|
+
evolves, we hope that people will contribute client libraries in many languages.
|
174
281
|
|
175
|
-
|
282
|
+
Existing libraries:
|
283
|
+
|
284
|
+
* If you're already using Backbone.js, integrating with a Brainstem API is super simple. Just use the
|
285
|
+
[brainstem-js](https://github.com/mavenlink/brainstem-js) gem (or its JavaScript contents) to access your relational
|
286
|
+
Brainstem API from JavaScript.
|
287
|
+
* For consuming Brainstem APIs in Ruby, take a look at the [brainstem-adaptor](https://github.com/mavenlink/brainstem-adaptor) gem.
|
288
|
+
|
289
|
+
### The Brainstem Results Array
|
176
290
|
|
177
291
|
{
|
178
292
|
results: [
|
@@ -185,11 +299,86 @@ APIs presented with Brainstem are just JSON APIs, so they can be consumed with j
|
|
185
299
|
name: "disco ball",
|
186
300
|
…
|
187
301
|
|
188
|
-
Brainstem returns objects as top-level hashes and provides a `results` array of `key` and `id` objects for finding the
|
302
|
+
Brainstem returns objects as top-level hashes and provides a `results` array of `key` and `id` objects for finding the
|
303
|
+
returned data in those hashes. The reason that we use the `results` array is two-fold: 1st) it provides order outside
|
304
|
+
of the serialized objects so that we can provide objects keyed by ID, and 2nd) it allows for polymorphic responses and
|
305
|
+
for objects that have associations of their own type (like posts and replies or tasks and sub-tasks).
|
189
306
|
|
190
|
-
|
307
|
+
## Testing your Brainstem API
|
191
308
|
|
192
|
-
|
309
|
+
We recommend writing specs for your Presenters and validating them with the `Brainstem::PresenterValidator`. Here is an
|
310
|
+
example RSpec shared behavior that you might want to use:
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
shared_examples_for "a Brainstem api presenter" do |presenter_class|
|
314
|
+
it 'passes Brainstem::PresenterValidator' do
|
315
|
+
validator = Brainstem::PresenterValidator.new(presenter_class)
|
316
|
+
validator.valid?
|
317
|
+
validator.should be_valid, "expected a valid presenter, got: #{validator.errors.full_messages}"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
And then use it in your presenter specs (e.g., in `spec/lib/api/v1/widget_presenter_spec.rb`:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
require 'spec_helper'
|
326
|
+
|
327
|
+
describe Api::V1::WidgetPresenter do
|
328
|
+
it_should_behave_like "a Brainstem api presenter", described_class
|
329
|
+
|
330
|
+
describe 'presented fields' do
|
331
|
+
let(:loaded_associations) { { } }
|
332
|
+
let(:user_requested_associations) { %w[features location] }
|
333
|
+
let(:model) { some_widget } # load from a fixture or create with a factory
|
334
|
+
let(:presented_data) {
|
335
|
+
# `present_model` will return the representation of a single model. As an optional
|
336
|
+
# side effect, it will store any requested associations in the Hash provided
|
337
|
+
# to `load_associations_into`.
|
338
|
+
described_class.new.present_model(model, user_requested_associations,
|
339
|
+
load_associations_into: loaded_associations)
|
340
|
+
}
|
341
|
+
|
342
|
+
describe 'attributes' do
|
343
|
+
it 'presents the attributes' do
|
344
|
+
presented_data['name'].should == model.name
|
345
|
+
end
|
346
|
+
|
347
|
+
describe 'something conditional on the presenter' do
|
348
|
+
describe 'for widgets with this behavior' do
|
349
|
+
let(:model) { widget_with_permissions }
|
350
|
+
|
351
|
+
it 'should be true' do
|
352
|
+
presented_data['conditional_thing'].should be_truthy
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
describe 'for widgets without this behavior' do
|
357
|
+
let(:model) { widget_without_permissions }
|
358
|
+
|
359
|
+
it 'should be missing' do
|
360
|
+
presented_data.should_not have_key('conditional_thing')
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
describe 'associations' do
|
367
|
+
it 'should load the associations' do
|
368
|
+
presented_data
|
369
|
+
loaded_associations.keys.should == %w[features location]
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
You can also write a spec that validates all presenters simultaniously by calling `Brainstem.presenter_collection.validate!`.
|
377
|
+
|
378
|
+
---
|
379
|
+
|
380
|
+
Brainstem also includes some spec helpers for controller specs. In order to use them, you need to include Brainstem in
|
381
|
+
your controller specs by adding the following to `spec/support/brainstem.rb` or in your `spec/spec_helper.rb`:
|
193
382
|
|
194
383
|
```ruby
|
195
384
|
require 'brainstem/test_helpers'
|
@@ -202,35 +391,267 @@ end
|
|
202
391
|
Now you are ready to use the `brainstem_data` method.
|
203
392
|
|
204
393
|
```ruby
|
205
|
-
#
|
394
|
+
# Access the request results:
|
395
|
+
expect(brainstem_data.results.first.name).to eq('name')
|
206
396
|
|
207
|
-
#
|
397
|
+
# View the resulting IDs
|
398
|
+
expect(brainstem_data.results.ids).to eq(['1', '2', '3'])
|
399
|
+
|
400
|
+
Selecting an item from a top-level collection by it's id
|
208
401
|
expect(brainstem_data.users.by_id(235).name).to eq('name')
|
209
402
|
|
210
|
-
#
|
211
|
-
expect(brainstem_data.
|
403
|
+
# Accessing the keys of presented model
|
404
|
+
expect(brainstem_data.results.first.keys).to =~ %w(id name email address)
|
405
|
+
```
|
212
406
|
|
213
|
-
|
214
|
-
expect(brainstem_data.users.first.keys).to =~ %w(id name email address)
|
407
|
+
## Upgrading from the pre-1.0 Brainstem
|
215
408
|
|
216
|
-
|
217
|
-
expect(brainstem_data.users.first.name).to eq('name')
|
218
|
-
expect(brainstem_data.users[2].name).to eq('name')
|
219
|
-
```
|
409
|
+
If you're upgrading from the previous version of Brainstem to 1.0, there are some key changes that you'll want to know about:
|
220
410
|
|
221
|
-
|
411
|
+
* The Presenter DSL has been rebuilt. Filters and sorts are the same, but the `present` method has been completely replaced
|
412
|
+
by a class-level DSL. Please see the documentation above and below.
|
413
|
+
* You can use `preload` instead of `custom_preload` now, although `custom_preload` still exists for complex cases.
|
414
|
+
* `present_objects` and `present` have been renamed to `brainstem_present_objects` and `brainstem_present`.
|
415
|
+
* `brainstem_key` is now an annotation on presenters and not needed when declaring associations. It should always be plural.
|
416
|
+
* `key_map` has been supplanted by `brainstem_key` in the presenter and has been removed.
|
417
|
+
* `options[:as]` is no longer used with `brainstem_present` / `PresenterCollection#presenting`. Use the `brainstem_key`
|
418
|
+
annotation in your presenters instead.
|
419
|
+
* `helper` can now take a block or module.
|
222
420
|
|
223
|
-
|
224
|
-
describe 'brainstem_data' do
|
225
|
-
subject { brainstem_data }
|
421
|
+
## Advanced Topics
|
226
422
|
|
227
|
-
|
228
|
-
|
229
|
-
|
423
|
+
### The presenter DSL
|
424
|
+
|
425
|
+
Brainstem provides a rich DSL for building presenters. This section details the methods available to you.
|
426
|
+
|
427
|
+
* `presents` - Accepts a list of classes that this specific presenter knows how to present. These are not inherited.
|
428
|
+
|
429
|
+
* `brainstem_key` - The name of the top-level JSON key in which these presented models will be returned. Defaults to the model's
|
430
|
+
table name. This annotation is useful when returning data under a different external name than you use for your internal
|
431
|
+
models, or when presenting data from STI tables that you want to have use the subclass's name.
|
432
|
+
|
433
|
+
* `sort_order` - Give `sort_order` a sort name (as a symbol) and either a string of SQL to be used for ordering
|
434
|
+
(like `"widgets.updated_at"`) or a lambda that accepts a scope and an order, like the following:
|
435
|
+
|
436
|
+
```ruby
|
437
|
+
sort_order :composite do |scope, direction|
|
438
|
+
# Be careful to avoid a SQL injection!
|
439
|
+
sanitized_direction = direction == "desc" ? "desc" : "asc"
|
440
|
+
scope.reorder("widgets.created_at #{sanitized_direction}, widgets.id #{sanitized_direction}")
|
441
|
+
end
|
442
|
+
```
|
443
|
+
|
444
|
+
* `default_sort_order` - The name and direction of the default sort for this presenter. The format is the same as is expected
|
445
|
+
in the URL parameter, for example `"name:desc"` or `"name:asc"`. The default value is `"updated_at:desc"`.
|
230
446
|
|
231
|
-
|
447
|
+
* `helper` - Provide a Module or block of helper methods to make available in filter, sort, conditional, association,
|
448
|
+
and field lambdas. Any instance variables defined in the helpers will only be available for a single model presentation.
|
232
449
|
|
233
|
-
|
450
|
+
```ruby
|
451
|
+
# Provide a global helper Module for all presenters.
|
452
|
+
Brainstem::Presenter.helper(ApiHelper)
|
453
|
+
|
454
|
+
# Inside of a Presenter, provide local helpers.
|
455
|
+
helper do
|
456
|
+
def some_widget_helper(widget)
|
457
|
+
widget.some_widget_method
|
458
|
+
end
|
459
|
+
end
|
460
|
+
```
|
461
|
+
|
462
|
+
* `filter` - Declare an available filter for this Presenter. Filters have a name, some options, and a block to run when
|
463
|
+
they're requested by a user. When a user provides either `"true"` or `"false"`, as in `include_legacy_widgets=true`,
|
464
|
+
they will be coerced into booleans. All other input formats are left as strings. Here are some examples:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
# Optional filter that applies a lambda.
|
468
|
+
filter :location_name do |scope, location_name|
|
469
|
+
scope.joins(:locations).where("locations.name = ?", location_name)
|
470
|
+
end
|
471
|
+
|
472
|
+
# Filter with an overridable default. This will run on every request,
|
473
|
+
# passing in `bool` as `false` unless a user has specified otherwise.
|
474
|
+
filter :include_legacy_widgets, default: false do |scope, bool|
|
475
|
+
bool ? scope : scope.without_legacy_widgets
|
476
|
+
end
|
477
|
+
```
|
478
|
+
|
479
|
+
* `search` - This annotation allows you to create a block that is run when your users provide the special `search` URL param.
|
480
|
+
When in "search" mode, Brainstem delegates entirely to this block and applies no filters or sorts beyond scoping to the
|
481
|
+
base scope passed into `presenting`. You're in charge of implementing whatever filters and sorts you'd like to support
|
482
|
+
in search mode inside of your search subsystem. The block should return an array where the first element is an array
|
483
|
+
of a page of matching model ids, and the second option is the total number of matched records.
|
484
|
+
|
485
|
+
```ruby
|
486
|
+
search do |search_string, options|
|
487
|
+
# options will contain:
|
488
|
+
# include: an array of the requested association inclusions
|
489
|
+
# order: { sort_order: sort_name, direction: direction }
|
490
|
+
# limit and offset or page and per_page, depending on which the user has provided
|
491
|
+
# requested filters and any default filters
|
492
|
+
|
493
|
+
# Talk to your search system (solr, elasticsearch, etc.) here.
|
494
|
+
results = do_an_actual_search(search_string, location_name: options[:location_name])
|
495
|
+
|
496
|
+
if results
|
497
|
+
[results.map { |result| result.id.to_i }, results.total]
|
498
|
+
else
|
499
|
+
[false, 0]
|
500
|
+
end
|
501
|
+
end
|
502
|
+
```
|
503
|
+
|
504
|
+
If you wish to perform your Brainstem filters in conjunction with your search block you can use the beta `search_and_filter`
|
505
|
+
query strategy. [See this for details](lib/brainstem/query_strategies/README.md).
|
506
|
+
|
507
|
+
* `preload` - Use this annotation to provide a list of valid associations to preload on this model. If you
|
508
|
+
always end up asking a question of each instance that requires loading an association, `preload` it here to avoid an
|
509
|
+
N+1 query. The syntax is the same as `preload` or `include` in Rails and allows for nesting.
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
preload :location
|
513
|
+
preload :location, features: :feature_creator
|
514
|
+
```
|
515
|
+
|
516
|
+
* `fields` - The Brainstem `fields` DSL is how you tell Brainstem what JSON fields to provide in each of your presented models.
|
517
|
+
Fields have a name, which is what they will be called in the returned JSON, a type which is used for API documentation,
|
518
|
+
an optional documentation string, and a number of options. By default, fields will call a model method with the same
|
519
|
+
name as the field's name and return the result. Use the `:via` option to call a different method, or the `:dynamic` option
|
520
|
+
to provide a lambda that takes the model and returns the field's output value. Fields which result in N + 1 queries can be
|
521
|
+
optimized with a `:lookup` option, detailed in the `lookup` section below. Fields can be conditionally returned with the
|
522
|
+
`:if` option, detailed in the `conditionals` section below. Expensive fields can be declared as `optional: true` so that they are
|
523
|
+
only returned when `optional_fields=field` is provided in the API request. Here are some example fields:
|
524
|
+
|
525
|
+
```ruby
|
526
|
+
fields do
|
527
|
+
field :name, :string, "the Widget's name"
|
528
|
+
field :legacy, :boolean, "true for legacy Widgets, false otherwise",
|
529
|
+
via: :legacy?
|
530
|
+
field :dynamic_name, :string, "a formatted name for this Widget",
|
531
|
+
dynamic: lambda { |widget| "This Widget's name is #{widget.name}" }
|
532
|
+
field :longform_description, :string, "feature-length description of this Widget",
|
533
|
+
optional: true
|
534
|
+
|
535
|
+
# Fields can be nested
|
536
|
+
fields :permissions do
|
537
|
+
field :access_level, :integer
|
538
|
+
end
|
539
|
+
end
|
540
|
+
```
|
541
|
+
|
542
|
+
* `associations` - Associations are one of the best features of Brainstem. Your users can provide the names of associations
|
543
|
+
to `include` with their response, preventing N+1 API requests. Declared `association` entries have a name, an ActiveRecord
|
544
|
+
class, an optional documentation string, and some options. By default, associations will call the association or
|
545
|
+
method on the model with their name. Like fields, you can use `:via` to call a different method or association and
|
546
|
+
`:dynamic` to provide a lambda that takes the model and returns a model, array of models, or relation of models.
|
547
|
+
Associations which result in N + 1 queries can be optimized with a `:lookup` option, detailed in the `lookup` secontion below.
|
548
|
+
|
549
|
+
If you have an association that tends to be large and expensive to return, you can annotate it with the
|
550
|
+
`restrict_to_only: true` option and it will only be returned when the `only` URL param is provided and contains a
|
551
|
+
specific set of requested model IDs.
|
552
|
+
|
553
|
+
Included associations will be present in the returned JSON as either `<field>_id`, `<field>_ids`, `<field>_ref`, or `<field>_refs`
|
554
|
+
depending on whether they reference a single model, an array (or Relation) of models, a single polymorphic
|
555
|
+
association (a polymorphic `belongs_to` or `has_one`), or a plural polymorphic association (a polymorphic `has_many`) respectively.
|
556
|
+
When a `*_ref` is returned, it will look like `{ "id": "2", "key": "widgets" }`, telling the consumer the top-level key in
|
557
|
+
which to find the identified record by ID.
|
558
|
+
|
559
|
+
If your model has a native column named `<field>_id`, it will be returned for free without being requested. Otherwise,
|
560
|
+
users need to request associations via the `include` url param.
|
561
|
+
|
562
|
+
```ruby
|
563
|
+
associations do
|
564
|
+
association :features, Feature, "features associated with this Widget"
|
565
|
+
association :location, Location, "the location of this Widget"
|
566
|
+
association :previous_location, Location, "the Widget's previous location",
|
567
|
+
dynamic: lambda { |widget| widget.previous_locations.first }
|
568
|
+
association :associated_objects, :polymorphic, "a mixture of objects related to this Widget"
|
569
|
+
end
|
570
|
+
```
|
571
|
+
|
572
|
+
* `lookup` - Use this option to avoid N + 1 queries for Fields and Associations. The `lookup` lambda runs once when
|
573
|
+
presenting and every presented model gets its assocation or value from the cache the `lookup` lambda generates. The
|
574
|
+
`lookup` lambda takes in the presented models and should generate a cache containing the models' coresponding assocations
|
575
|
+
or values. Brainstem expects the return result of the `lookup` to be a Hash where the keys are the presented models' ids
|
576
|
+
and the values are those models' associations or values. Use the `lookup` when you would like to preload but cannot
|
577
|
+
e.g. if your association references `current_user`. If both a `lookup` and `dynamic` options are defined,
|
578
|
+
the `lookup` will be used.
|
579
|
+
|
580
|
+
```ruby
|
581
|
+
associations do
|
582
|
+
association :current_user_groups, Group, "the Groups for the current user",
|
583
|
+
lookup: lambda { |models|
|
584
|
+
Group.where(subject_id: models.map(&:id)
|
585
|
+
.where(user_id: current_user.id)
|
586
|
+
.group_by { |group| group.subject_id }
|
587
|
+
}
|
588
|
+
end
|
589
|
+
```
|
590
|
+
|
591
|
+
* `lookup_fetch` - Use this option for Fields and Associations if you would like to override how a model should retrieve
|
592
|
+
its value or assocation returned by the `lookup` cache. The `lookup_fetch` lambda takes in the presented model and the result
|
593
|
+
from the `lookup` lambda. It should return the association or value from the `lookup` cache for that `model`. If
|
594
|
+
`lookup_fetch` is not defined, Brainstem will run the default. The example `lookup_fetch` below is equivalent to the default.
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
fields do
|
598
|
+
field :current_user_post_count, Post, "count of Posts the current_user has for this model",
|
599
|
+
lookup: lambda { |models|
|
600
|
+
lookup = Post.where(subject_id: models.map(&:id)
|
601
|
+
.where(user_id: current_user.id)
|
602
|
+
.group_by { |post| post.subject_id }
|
603
|
+
|
604
|
+
lookup
|
605
|
+
},
|
606
|
+
lookup_fetch: lambda { |lookup, model| lookup[model.id] }
|
607
|
+
end
|
608
|
+
```
|
609
|
+
|
610
|
+
* `conditionals` - Conditionals are named questions that can be used to restrict which `fields` are returned. The
|
611
|
+
`conditionals` block has two available methods, `request` and `model`. The `request` conditionals run once for the entire
|
612
|
+
set of presented models, while `model` ones run once per model. Use `request` conditionals to check and then cache things
|
613
|
+
like permissions checks that do not change between models, and use `model` conditionals to ask questions of specific
|
614
|
+
models. The optional documentation string is used in API doc generation.
|
615
|
+
|
616
|
+
```ruby
|
617
|
+
conditionals do
|
618
|
+
model :title_is_hello,
|
619
|
+
lambda { |model| model.title == 'hello' },
|
620
|
+
'visible when the title is hello'
|
621
|
+
|
622
|
+
request :user_is_bob,
|
623
|
+
lambda { current_user == 'bob' }, # Assuming some sort of `helper` that provides `current_user`
|
624
|
+
'visible only to bob'
|
625
|
+
end
|
626
|
+
|
627
|
+
fields do
|
628
|
+
field :hello_title, :string, 'the title, when it is exactly the word "hello"',
|
629
|
+
dynamic: lambda { |model| model.title + " is the title" },
|
630
|
+
if: :title_is_hello
|
631
|
+
|
632
|
+
field :secret, :string, "a secret, via the secret_info model method, only visible to bob and when the model's title is hello",
|
633
|
+
via: :secret_info,
|
634
|
+
if: [:user_is_bob, :title_is_hello]
|
635
|
+
|
636
|
+
with_options if: :user_is_bob do
|
637
|
+
field :bob_title, :string, 'another name for the title, only visible to Bob',
|
638
|
+
via: :title
|
639
|
+
end
|
640
|
+
end
|
641
|
+
```
|
642
|
+
|
643
|
+
### A note on Rails 4 Style Scopes
|
644
|
+
|
645
|
+
In Rails 3 it was acceptable to write scopes like this: `scope :popular, where(:popular => true)`. This was deprecated
|
646
|
+
in Rails 4 in preference of scopes that include a callable object: `scope :popular, lambda { where(:popular) => true }`.
|
647
|
+
|
648
|
+
If your scope does not take any parameters, this can cause a problem with Brainstem if you use a filter that delegates
|
649
|
+
to that scope in your presenter. (e.g., `filter :popular`). The preferable way to handle this is to write a Brainstem
|
650
|
+
scope that delegates to your model scope:
|
651
|
+
|
652
|
+
```ruby
|
653
|
+
filter :popular { |scope| scope.popular }
|
654
|
+
```
|
234
655
|
|
235
656
|
## Contributing
|
236
657
|
|