brainstem 0.2.6.1 → 1.0.0.pre.1
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.
- 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
|
+
[](https://gitter.im/mavenlink/brainstem?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
6
|
+
|
3
7
|
[](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
|
|