trailblazer 1.0.0 → 1.0.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 +4 -4
- data/CHANGES.md +6 -0
- data/README.md +85 -386
- data/lib/trailblazer/autoloading.rb +4 -0
- data/lib/trailblazer/endpoint.rb +1 -1
- data/lib/trailblazer/operation.rb +1 -1
- data/lib/trailblazer/operation/controller.rb +30 -12
- data/lib/trailblazer/operation/representer.rb +1 -0
- data/lib/trailblazer/version.rb +1 -1
- metadata +2 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2618bcea766e5192fadaa575315a4ad04170654
|
4
|
+
data.tar.gz: fe623b65e42ac232f9bc0ec45a723b5fa5da70e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: befbbee9f37e868da73002c26ce09b91f609ee0e1ded07ffc976dc56cd4440cfa23609911939394c48427d7e9a69bcf28839404a2fb411584d6215b11f79f8c2
|
7
|
+
data.tar.gz: e132220e955dc8b0338584b3fc1055d9abacd2100308277829cbf51bfa6b0ecb5bf2a605c7759bb9a36a191ac9985fe514c4a581ede512cd003a74728928c6e6
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
# 1.0.1
|
2
|
+
|
3
|
+
* Treat `:js` requests as non-document, too.
|
4
|
+
* `Controller#form` now returns the form object and not the operation.
|
5
|
+
* In `Controller`, `#form`, `#present`, `#run` and `#respond` now all have the same API: `run(constant, options)`. If you want to pass a custom params hash, use `run Comment::Create, params: {..}`.
|
6
|
+
|
1
7
|
# 1.0.0
|
2
8
|
|
3
9
|
* All Rails-relevant files are now in the `trailblazer-rails` gem. You have to include it should you be in a Rails environment.
|
data/README.md
CHANGED
@@ -18,13 +18,13 @@ _Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation,
|
|
18
18
|
|
19
19
|
Trailblazer is designed to handle different contexts like user roles by applying [inheritance](#inheritance) between and [composing](#composing) of operations, form objects, policies, representers and callbacks.
|
20
20
|
|
21
|
-
|
21
|
+
Wanna see some code? Jump [right here](#controllers)!
|
22
22
|
|
23
23
|
## Mission
|
24
24
|
|
25
25
|
While _Trailblazer_ offers you abstraction layers for all aspects of Ruby On Rails, it does _not_ missionize you. Wherever you want, you may fall back to the "Rails Way" with fat models, monolithic controllers, global helpers, etc. This is not a bad thing, but allows you to step-wise introduce Trailblazer's encapsulation in your app without having to rewrite it.
|
26
26
|
|
27
|
-
Trailblazer is all about structure. It helps re-organize existing code into smaller components where different concerns are handled in separated classes.
|
27
|
+
Trailblazer is all about structure. It helps re-organize existing code into smaller components where different concerns are handled in separated classes.
|
28
28
|
|
29
29
|
Again, you can pick which layers you want. Trailblazer doesn't impose technical implementations, it offers mature solutions for recurring problems in all types of Rails applications.
|
30
30
|
|
@@ -61,75 +61,80 @@ The opposite is the case: Controller, view and model become lean endpoints for H
|
|
61
61
|
|
62
62
|
## Routing
|
63
63
|
|
64
|
-
Trailblazer uses Rails routing to map URLs to controllers
|
64
|
+
Trailblazer uses Rails routing to map URLs to controllers, because it works.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Rails.application.routes.draw do
|
68
|
+
resources :comments
|
69
|
+
end
|
70
|
+
```
|
65
71
|
|
66
72
|
## Controllers
|
67
73
|
|
68
|
-
Controllers are lean endpoints for HTTP. They
|
74
|
+
Controllers are lean endpoints for HTTP. They do not contain any business logic. Actions immediately dispatch to an operation.
|
69
75
|
|
70
76
|
```ruby
|
71
77
|
class CommentsController < ApplicationController
|
72
78
|
def create
|
73
|
-
Comment::Create.
|
79
|
+
run Comment::Create # Comment::Create is an operation class.
|
74
80
|
end
|
75
81
|
```
|
76
82
|
|
77
|
-
|
83
|
+
The `#run` method invokes the operation. It allows you to run a conditional block of logic if the operation was successful.
|
78
84
|
|
79
85
|
```ruby
|
80
86
|
class CommentsController < ApplicationController
|
81
87
|
def create
|
82
88
|
run Comment::Create do |op|
|
83
|
-
return redirect_to(comment_path op.model) # success
|
89
|
+
return redirect_to(comment_path op.model) # success!
|
84
90
|
end
|
85
91
|
|
86
|
-
render :new # re-render form.
|
92
|
+
render :new # invalid. re-render form.
|
87
93
|
end
|
88
94
|
```
|
89
95
|
|
90
|
-
Again, the controller only
|
96
|
+
Again, the controller only dispatchs to the operation and handles successful/invalid processing on the HTTP level. For instance by redirecting, setting flash messages, or signing in a user.
|
91
97
|
|
92
|
-
|
98
|
+
[Learn more.](http://trailblazerb.org/gems/operation/controller.html)
|
93
99
|
|
94
|
-
|
100
|
+
## Operation
|
95
101
|
|
96
|
-
Operations encapsulate business logic and are the heart of a Trailblazer architecture.
|
102
|
+
Operations encapsulate business logic and are the heart of a Trailblazer architecture.
|
97
103
|
|
98
|
-
|
104
|
+
Operations don't know about HTTP or the environment. You could use an operation in Rails, Lotus, or Roda, it wouldn't know. This makes them an ideal replacement for test factories.
|
99
105
|
|
100
|
-
|
106
|
+
An operation is not just a monolithic replacement for your business code. It's a simple orchestrator between the form object, models and your business code.
|
101
107
|
|
102
108
|
```ruby
|
103
|
-
class Comment <
|
104
|
-
|
105
|
-
|
106
|
-
# do whatever you feel like.
|
107
|
-
end
|
109
|
+
class Comment::Create < Trailblazer::Operation
|
110
|
+
def process(params)
|
111
|
+
# do whatever you feel like.
|
108
112
|
end
|
109
113
|
end
|
110
114
|
```
|
111
115
|
|
112
116
|
Operations only need to implement `#process` which receives the params from the caller.
|
113
117
|
|
118
|
+
[Learn more.](http://trailblazerb.org/gems/operation)
|
119
|
+
|
114
120
|
## Validations
|
115
121
|
|
116
|
-
|
122
|
+
In Trailblazer, an operation (usually) has a form object which is simply a `Reform::Form` class. All the [API documented in Reform](https://github.com/apotonick/reform) can be applied and used.
|
117
123
|
|
118
124
|
The operation makes use of the form object using the `#validate` method.
|
119
125
|
|
120
126
|
```ruby
|
121
|
-
class Comment <
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
127
|
+
class Comment::Create < Trailblazer::Operation
|
128
|
+
contract do
|
129
|
+
# this is a Reform::Form class!
|
130
|
+
property :body, validates: {presence: true}
|
131
|
+
end
|
126
132
|
|
127
|
-
|
128
|
-
|
133
|
+
def process(params)
|
134
|
+
@model = Comment.new
|
129
135
|
|
130
|
-
|
131
|
-
|
132
|
-
end
|
136
|
+
validate(params[:comment], @model) do |f|
|
137
|
+
f.save
|
133
138
|
end
|
134
139
|
end
|
135
140
|
end
|
@@ -137,59 +142,43 @@ end
|
|
137
142
|
|
138
143
|
The contract (aka _form_) is defined in the `::contract` block. You can implement nested forms, default values, validations, and everything else Reform provides.
|
139
144
|
|
140
|
-
In
|
141
|
-
|
142
|
-
Technically speaking, what really happens in `Operation#validate` is the following.
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
contract_class.new(@model).validate(params[:comment])
|
146
|
-
```
|
147
|
-
|
148
|
-
This is a familiar work-flow from Reform. Validation does _not_ touch the model.
|
145
|
+
In the `#process` method you can define your business logic.
|
149
146
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
If you prefer keeping your forms in separate classes or even files, you're free to do so.
|
154
|
-
|
155
|
-
```ruby
|
156
|
-
class Create < Trailblazer::Operation
|
157
|
-
self.contract_class = MyCommentForm
|
158
|
-
```
|
147
|
+
[Learn more.](http://trailblazerb.org/gems/operation/api.html)
|
159
148
|
|
160
149
|
## Callbacks
|
161
150
|
|
162
151
|
Post-processing logic (also known as _callbacks_) is configured in operations.
|
163
152
|
|
164
|
-
|
153
|
+
Callbacks can be defined in groups. They use the form object's state tracking to find out whether they should be run.
|
165
154
|
|
166
155
|
```ruby
|
167
|
-
class Create < Trailblazer::Operation
|
156
|
+
class Comment::Create < Trailblazer::Operation
|
168
157
|
callback(:after_save) do
|
169
|
-
on_change :
|
170
|
-
|
171
|
-
property :user do
|
172
|
-
on_create :notify_user!
|
173
|
-
end
|
158
|
+
on_change :markdownize_body! # this is only run when the form object has changed.
|
174
159
|
end
|
175
160
|
```
|
176
161
|
|
177
|
-
|
162
|
+
Callbacks are never triggered automatically, you have to invoke them! This is called _Imperative Callback_.
|
178
163
|
|
179
164
|
```ruby
|
180
|
-
class Create < Trailblazer::Operation
|
181
|
-
|
165
|
+
class Comment::Create < Trailblazer::Operation
|
182
166
|
def process(params)
|
183
167
|
validate(params) do
|
184
168
|
contract.save
|
185
|
-
dispatch!(:after_save)
|
169
|
+
dispatch!(:after_save) # run markdownize_body!, but only if form changed.
|
186
170
|
end
|
187
171
|
end
|
172
|
+
|
173
|
+
def markdownize_body!(comment)
|
174
|
+
comment.body = Markdownize.(comment.body)
|
175
|
+
end
|
188
176
|
end
|
189
177
|
```
|
190
178
|
|
191
179
|
No magical triggering of unwanted logic anymore, but explicit invocations where you want it.
|
192
180
|
|
181
|
+
[Learn more.](http://trailblazerb.org/gems/operation/callback.html)
|
193
182
|
|
194
183
|
## Models
|
195
184
|
|
@@ -198,10 +187,8 @@ Models for persistence can be implemented using any ORM you fancy, for instance
|
|
198
187
|
In Trailblazer, models are completely empty. They solely contain associations and finders. No business logic is allowed in models.
|
199
188
|
|
200
189
|
```ruby
|
201
|
-
class
|
202
|
-
|
203
|
-
has_many :users, through: :authorships
|
204
|
-
has_many :authorships
|
190
|
+
class Comment < ActiveRecord::Base
|
191
|
+
belongs_to :thing
|
205
192
|
|
206
193
|
scope :latest, lambda { all.limit(9).order("id DESC") }
|
207
194
|
end
|
@@ -211,14 +198,12 @@ Only operations and views/cells can access models directly.
|
|
211
198
|
|
212
199
|
## Policies
|
213
200
|
|
214
|
-
The full documentation for [Policy is here](http://trailblazerb.org/gems/operation/policy.html).
|
215
|
-
|
216
201
|
You can abort running an operation using a policy. "[Pundit](https://github.com/elabs/pundit)-style" policy classes define the rules.
|
217
202
|
|
218
203
|
```ruby
|
219
|
-
class
|
220
|
-
def initialize(user,
|
221
|
-
@user, @
|
204
|
+
class Comment::Policy
|
205
|
+
def initialize(user, comment)
|
206
|
+
@user, @comment = user, comment
|
222
207
|
end
|
223
208
|
|
224
209
|
def create?
|
@@ -230,31 +215,15 @@ end
|
|
230
215
|
The rule is enabled via the `::policy` call.
|
231
216
|
|
232
217
|
```ruby
|
233
|
-
class
|
218
|
+
class Comment::Create < Trailblazer::Operation
|
234
219
|
include Policy
|
235
220
|
|
236
|
-
policy
|
221
|
+
policy Comment::Policy, :create?
|
237
222
|
```
|
238
223
|
|
239
224
|
The policy is evaluated in `#setup!`, raises an exception if `false` and suppresses running `#process`.
|
240
225
|
|
241
|
-
|
242
|
-
Thing::Create.(current_user: User.find(1), thing: {}) # raises exception.
|
243
|
-
```
|
244
|
-
|
245
|
-
You can query the `policy` object at any point in your operation without raising an exception.
|
246
|
-
|
247
|
-
To [use policies in your builders](http://trailblazerb.org/gems/operation/builder#resolver.html), please read the documentation.
|
248
|
-
|
249
|
-
```ruby
|
250
|
-
class Thing::Create < Trailblazer::Operation
|
251
|
-
include Resolver
|
252
|
-
|
253
|
-
builder-> (model, policy, params) do
|
254
|
-
return Admin if policy.admin?
|
255
|
-
return SignedIn if params[:current_user]
|
256
|
-
end
|
257
|
-
```
|
226
|
+
[Learn more.](http://trailblazerb.org/gems/operation/policy.html)
|
258
227
|
|
259
228
|
## Views
|
260
229
|
|
@@ -262,304 +231,63 @@ View rendering can happen using the controller as known from Rails. This is abso
|
|
262
231
|
|
263
232
|
More complex UI logic happens in _View Models_ as found in [Cells](https://github.com/apotonick/cells). View models also replace helpers.
|
264
233
|
|
265
|
-
|
266
|
-
## Representers
|
267
|
-
|
268
|
-
Operations can use representers from [Roar](https://github.com/apotonick/roar) to serialize and parse JSON and XML documents for APIs.
|
269
|
-
|
270
|
-
Representers can be inferred automatically from your contract, then may be refined, e.g. with hypermedia or a format like `JSON-API`.
|
271
|
-
|
272
|
-
```ruby
|
273
|
-
class Create < Trailblazer::Operation
|
274
|
-
|
275
|
-
representer do
|
276
|
-
# inherited :body
|
277
|
-
include Roar::JSON::HAL
|
278
|
-
|
279
|
-
link(:self) { comment_path(represented.id) }
|
280
|
-
end
|
281
|
-
```
|
282
|
-
|
283
|
-
The operation can then parse incoming JSON documents in `validate` and render a document via `to_json`. The full [documentation is here](http://trailblazerb.org/gems/operation/representer.html) or in the [Trailblazer book](https://leanpub.com/trailblazer), chapter _Hypermedia APIs_.
|
284
|
-
|
285
|
-
## Tests
|
286
|
-
|
287
|
-
Subject to tests are mainly _Operation_s and _View Model_s, as they encapsulate endpoint behavior of your app. As a nice side effect, factories are replaced by simple _Operation_ calls.
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
## Overview
|
292
|
-
|
293
|
-
Trailblazer is a collection of mature gems that have been developed over the past 10 years and are used in thousands of production apps.
|
294
|
-
|
295
|
-
Using the different layers is completely optional and up to you: Both Cells and Reform can be excluded from your stack if you wish so.
|
296
|
-
|
297
|
-
## Controller API
|
298
|
-
|
299
|
-
[Learn more](http://trailblazerb.org/gems/operation/controller.html)
|
300
|
-
|
301
|
-
Trailblazer provides four methods to present and invoke operations. But before that, you need to include the `Controller` module.
|
234
|
+
The operation's form object can be rendered in views, too.
|
302
235
|
|
303
236
|
```ruby
|
304
237
|
class CommentsController < ApplicationController
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
### Running an operation
|
309
|
-
|
310
|
-
If you do not intend to maintain different request formats, the easiest is to use `#run` to process incoming data using an operation.
|
311
|
-
|
312
|
-
```ruby
|
313
|
-
def create
|
314
|
-
run Comment::Create
|
315
|
-
end
|
316
|
-
```
|
317
|
-
|
318
|
-
This will simply run `Comment::Create[params]`.
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
----
|
325
|
-
|
326
|
-
It's up to the operation's builder to decide which class to instantiate.
|
327
|
-
|
328
|
-
```ruby
|
329
|
-
class Create < Trailblazer::Operation
|
330
|
-
builds do |params|
|
331
|
-
JSON if params[:format] == "json"
|
238
|
+
def new
|
239
|
+
form Comment::Create # will assign the form object to @form.
|
332
240
|
end
|
333
|
-
end
|
334
241
|
```
|
335
242
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
## Operation API
|
243
|
+
Since Reform objects can be passed to form builders, you can use the operation to render and process the form!
|
340
244
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
```ruby
|
346
|
-
op = Comment::Create.(params)
|
245
|
+
```haml
|
246
|
+
= simple_form_for @form do |f|
|
247
|
+
= f.input :body
|
347
248
|
```
|
348
249
|
|
349
|
-
The call style runs the operation and return the operation instance, only.
|
350
|
-
|
351
|
-
In case of an invalid operation, this will raise an exception.
|
352
|
-
|
353
|
-
### Run style
|
354
|
-
|
355
|
-
The _run style_ will do the same as call, but won't raise an exception in case of an invalid result. Instead, it returns result _and_ the operation instance.
|
356
|
-
|
357
|
-
```ruby
|
358
|
-
result, op = Comment::Create.run(params)
|
359
|
-
```
|
360
|
-
|
361
|
-
Additionally, it accepts a block that's only run for a valid state.
|
362
|
-
|
363
|
-
```ruby
|
364
|
-
Comment::Create.run(params) do |op|
|
365
|
-
# only run when valid.
|
366
|
-
end
|
367
|
-
```
|
368
|
-
|
369
|
-
### Inheritance
|
370
|
-
|
371
|
-
Operations fully support inheritance and will copy the contract, the callback groups and any methods from the original operation class.
|
372
|
-
|
373
|
-
```ruby
|
374
|
-
class Create < Trailblazer::Operation
|
375
|
-
contract do
|
376
|
-
property :title
|
377
|
-
end
|
378
250
|
|
379
|
-
|
380
|
-
on_change :notify!
|
381
|
-
end
|
382
|
-
|
383
|
-
def notify!(*)
|
384
|
-
end
|
385
|
-
end
|
386
|
-
```
|
387
|
-
|
388
|
-
This happens with normal Ruby inheritance.
|
389
|
-
|
390
|
-
```ruby
|
391
|
-
class Update < Create
|
392
|
-
# inherited:
|
393
|
-
# contract
|
394
|
-
# callback(:before_save)
|
395
|
-
# def notify!(*)
|
396
|
-
end
|
397
|
-
```
|
398
|
-
|
399
|
-
You can customize callback groups and contracts using the `:inherit` option, add and remove properties or add methods.
|
400
|
-
|
401
|
-
[Learn more](http://trailblazerb.org/gems/operation/inheritance.html)
|
402
|
-
|
403
|
-
### Modules
|
404
|
-
|
405
|
-
In case inheritance is not enough for you, use modules to share common functionality.
|
406
|
-
|
407
|
-
```ruby
|
408
|
-
module ExtendedCreate
|
409
|
-
include Trailblazer::Operation::Module
|
410
|
-
|
411
|
-
contract do
|
412
|
-
property :id
|
413
|
-
end
|
414
|
-
|
415
|
-
callback do
|
416
|
-
on_update :update!
|
417
|
-
end
|
418
|
-
|
419
|
-
def update!(song)
|
420
|
-
# do something
|
421
|
-
end
|
422
|
-
end
|
423
|
-
```
|
424
|
-
|
425
|
-
Modules can be included and will simply run the declarations in the including class.
|
426
|
-
|
427
|
-
```ruby
|
428
|
-
class Create::Admin < Create
|
429
|
-
include ExtendedCreate
|
430
|
-
|
431
|
-
# contract has :title and :id now.
|
432
|
-
```
|
433
|
-
|
434
|
-
Modules are often used to modify an existing operation for admin or signed-in roles.
|
435
|
-
|
436
|
-
[Learn more](http://trailblazerb.org/gems/operation/inheritance.html)
|
437
|
-
|
438
|
-
|
439
|
-
## Form API
|
440
|
-
|
441
|
-
Usually, an operation has a form object.
|
442
|
-
|
443
|
-
```ruby
|
444
|
-
class Create < Trailblazer::Operation
|
445
|
-
contract do
|
446
|
-
property :body
|
447
|
-
validates :body: presence: true, length: {max: 160}
|
448
|
-
end
|
449
|
-
```
|
450
|
-
|
451
|
-
A `::contract` block simply opens a new Reform class for you.
|
452
|
-
|
453
|
-
```ruby
|
454
|
-
contract do #=> Class.new(Reform::Form) do
|
455
|
-
```
|
456
|
-
|
457
|
-
This allows using Reform's API in the block.
|
458
|
-
|
459
|
-
When inheriting, the block is `class_eval`ed in the inherited class' context and allows adding, removing and customizing the sub contract.
|
460
|
-
|
461
|
-
### CRUD Semantics
|
462
|
-
|
463
|
-
You can make Trailblazer find and create models for you using the `Model` module.
|
464
|
-
|
465
|
-
```ruby
|
466
|
-
require 'trailblazer/operation/model'
|
467
|
-
|
468
|
-
class Comment < ActiveRecord::Base
|
469
|
-
class Create < Trailblazer::Operation
|
470
|
-
include Model
|
471
|
-
model Comment, :create
|
472
|
-
|
473
|
-
contract do
|
474
|
-
# ..
|
475
|
-
end
|
476
|
-
|
477
|
-
def process(params)
|
478
|
-
validate(params[:comment]) do |f|
|
479
|
-
f.save
|
480
|
-
end
|
481
|
-
end
|
482
|
-
end
|
483
|
-
end
|
484
|
-
```
|
485
|
-
|
486
|
-
You have to tell `Model` the model class and what action to implement using `::model`.
|
487
|
-
|
488
|
-
Note how you do not have to pass the `@model` to validate anymore. Also, the `@model` gets created automatically and is accessible using `Operation#model`.
|
489
|
-
|
490
|
-
In inherited operations, you can override the action, only, using `::action`.
|
491
|
-
|
492
|
-
```ruby
|
493
|
-
class Update < Create
|
494
|
-
action :update
|
495
|
-
end
|
496
|
-
```
|
497
|
-
|
498
|
-
Another action is `:find` (which is currently doing the same as `:update`) to find a model by using `params[:id]`.
|
499
|
-
|
500
|
-
|
501
|
-
### Normalizing params
|
502
|
-
|
503
|
-
Override `#setup_params!` to add or remove values to params before the operation is run.
|
504
|
-
|
505
|
-
```ruby
|
506
|
-
class Create < Trailblazer::Operation
|
507
|
-
def process(params)
|
508
|
-
params #=> {show_all: true, admin: true, .. }
|
509
|
-
end
|
510
|
-
|
511
|
-
private
|
512
|
-
def setup_params!(params)
|
513
|
-
params.merge!(show_all: true) if params[:admin]
|
514
|
-
end
|
515
|
-
end
|
516
|
-
end
|
517
|
-
```
|
518
|
-
|
519
|
-
This centralizes params normalization and doesn't require you to do that manually in `#process`.
|
520
|
-
|
521
|
-
### Collections
|
522
|
-
|
523
|
-
Operations can also be used to present (and soon to process) collections of objects, e.g. for an `Index` operation. This is [documented here](http://trailblazerb.org/gems/operation/collection.html).
|
251
|
+
## Representers
|
524
252
|
|
525
|
-
|
253
|
+
Operations can use representers from [Roar](https://github.com/apotonick/roar) to serialize and parse JSON and XML documents for APIs.
|
526
254
|
|
527
|
-
|
255
|
+
Representers can be inferred automatically from your contract, then may be refined, e.g. with hypermedia or a format like `JSON-API`.
|
528
256
|
|
529
257
|
```ruby
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
258
|
+
class Comment::Create < Trailblazer::Operation
|
259
|
+
representer do
|
260
|
+
# inherited :body
|
261
|
+
include Roar::JSON::HAL
|
534
262
|
|
535
|
-
|
536
|
-
# will be run asynchronously.
|
263
|
+
link(:self) { comment_path(represented.id) }
|
537
264
|
end
|
538
|
-
end
|
539
265
|
```
|
540
266
|
|
267
|
+
The operation can then parse incoming JSON documents in `validate` and render a document via `to_json`.
|
541
268
|
|
542
|
-
|
543
|
-
|
269
|
+
[Learn more.](http://trailblazerb.org/gems/operation/representer.html)
|
544
270
|
|
545
|
-
|
546
|
-
|
547
|
-
## Autoloading
|
271
|
+
## Tests
|
548
272
|
|
549
|
-
|
273
|
+
In Trailblazer, you only have operation unit tests and integration smoke tests to test the operation/controller wiring.
|
550
274
|
|
551
|
-
|
275
|
+
Operations completely replace the need for leaky factories.
|
552
276
|
|
553
277
|
```ruby
|
554
|
-
|
278
|
+
describe Comment::Update do
|
279
|
+
let(:comment) { Comment::Create.(comment: {body: "[That](http://trailblazerb.org)!"}) }
|
555
280
|
```
|
556
281
|
|
557
|
-
|
282
|
+
## More
|
558
283
|
|
559
|
-
|
284
|
+
Trailblazer has many more architectural features such as
|
560
285
|
|
561
|
-
|
286
|
+
* Polymorphic builders and operations
|
287
|
+
* Inheritance and composition support
|
288
|
+
* Polymorphic views
|
562
289
|
|
290
|
+
Check the project website and the book.
|
563
291
|
|
564
292
|
## Installation
|
565
293
|
|
@@ -573,40 +301,11 @@ gem "cells"
|
|
573
301
|
|
574
302
|
Cells is _not_ required per default! Add it if you use it, which is highly recommended.
|
575
303
|
|
576
|
-
A few quirks are required at the moment as Rails autoloading is giving us a hard time. The setup of an app [is documented here](https://github.com/apotonick/gemgem-trbrb/wiki/Things-You-Should-Know).
|
577
|
-
|
578
|
-
## Undocumented Features
|
579
|
-
|
580
|
-
(Please don't read this section!)
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
### Named Controller Instance Variables
|
585
|
-
|
586
|
-
If you want to include named instance variables for you views you must include another ActiveRecord specific module.
|
587
|
-
|
588
|
-
```ruby
|
589
|
-
require 'trailblazer/operation/controller/active_record'
|
590
|
-
|
591
|
-
class ApplicationController < ActionController::Base
|
592
|
-
include Trailblazer::Operation::Controller
|
593
|
-
include Trailblazer::Operation::Controller::ActiveRecord
|
594
|
-
end
|
595
|
-
```
|
596
|
-
|
597
|
-
This will setup a named instance variable of your operation's model, for example `@song`.
|
598
|
-
|
599
304
|
## The Book
|
600
305
|
|
601
306
|

|
602
307
|
|
603
|
-
Please buy
|
308
|
+
Please buy it: [Trailblazer - A new architecture for Rails](https://leanpub.com/trailblazer).
|
604
309
|
|
605
310
|
The [demo application](https://github.com/apotonick/gemgem-trbrb) implements what we discuss in the book.
|
606
311
|
|
607
|
-
## Why?
|
608
|
-
|
609
|
-
* Grouping code, views and assets by concepts increases the **maintainability** of your apps. Developers will find their way faster into your structure as the file layout is more intuitive.
|
610
|
-
* Finding bugs gets less frustrating as encapsulated layers allow **testing components** in total isolation. Once you know your form and your view are ok, it must be the parsing code.
|
611
|
-
* The reusability of code increases drastically as Trailblazer gently pushes you towards encapsulation. No more redundant helpers but clean inheritance.
|
612
|
-
* No more surprises from ActiveRecord's massive API. The separation between persistence and domain automatically results in smaller, less destructive APIs for your models.
|
data/lib/trailblazer/endpoint.rb
CHANGED
@@ -137,7 +137,7 @@ module Trailblazer
|
|
137
137
|
result
|
138
138
|
end
|
139
139
|
|
140
|
-
# When using Op
|
140
|
+
# When using Op.(), an invalid contract will raise an exception.
|
141
141
|
def raise!(contract)
|
142
142
|
raise InvalidContract.new(contract.errors.to_s) if @options[:raise_on_invalid]
|
143
143
|
end
|
@@ -5,13 +5,13 @@ private
|
|
5
5
|
def form(*args)
|
6
6
|
operation!(*args).tap do |op|
|
7
7
|
op.contract.prepopulate! # equals to @form.prepopulate!
|
8
|
-
end
|
8
|
+
end.contract
|
9
9
|
end
|
10
10
|
|
11
11
|
# Provides the operation instance, model and contract without running #process.
|
12
12
|
# Returns the operation.
|
13
|
-
def present(operation_class,
|
14
|
-
operation!(operation_class,
|
13
|
+
def present(operation_class, options={})
|
14
|
+
operation!(operation_class, skip_form: true)
|
15
15
|
end
|
16
16
|
|
17
17
|
def collection(*args)
|
@@ -20,8 +20,8 @@ private
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def run(operation_class,
|
24
|
-
res, op = operation_for!(operation_class,
|
23
|
+
def run(operation_class, options={}, &block)
|
24
|
+
res, op = operation_for!(operation_class, options) { |params| operation_class.run(params) }
|
25
25
|
|
26
26
|
yield op if res and block_given?
|
27
27
|
|
@@ -29,8 +29,9 @@ private
|
|
29
29
|
end
|
30
30
|
|
31
31
|
# The block passed to #respond is always run, regardless of the validity result.
|
32
|
-
def respond(operation_class, options={},
|
33
|
-
|
32
|
+
def respond(operation_class, options={}, &block)
|
33
|
+
options[:___dont_deprecate] = 1 # TODO: remove in 1.1.
|
34
|
+
res, op = operation_for!(operation_class, options) { |params| operation_class.run(params) }
|
34
35
|
namespace = options.delete(:namespace) || []
|
35
36
|
|
36
37
|
return respond_with *namespace, op, options if not block_given?
|
@@ -38,8 +39,8 @@ private
|
|
38
39
|
end
|
39
40
|
|
40
41
|
private
|
41
|
-
def operation!(operation_class,
|
42
|
-
res, op = operation_for!(operation_class,
|
42
|
+
def operation!(operation_class, options={}) # or #model or #setup.
|
43
|
+
res, op = operation_for!(operation_class, options) { |params| [true, operation_class.present(params)] }
|
43
44
|
op
|
44
45
|
end
|
45
46
|
|
@@ -47,9 +48,13 @@ private
|
|
47
48
|
end
|
48
49
|
|
49
50
|
# Normalizes parameters and invokes the operation (including its builders).
|
50
|
-
def operation_for!(operation_class,
|
51
|
-
|
52
|
-
|
51
|
+
def operation_for!(operation_class, options, &block)
|
52
|
+
options = deprecate_positional_params_argument!(options)
|
53
|
+
|
54
|
+
# Per default, only treat :html and js as non-document.
|
55
|
+
default_options = {is_document: ![:html, :js].include?(request.format.to_sym)}
|
56
|
+
options = default_options.merge(options)
|
57
|
+
params = options.delete(:params) || self.params # TODO: test params: parameter properly in all 4 methods.
|
53
58
|
|
54
59
|
process_params!(params)
|
55
60
|
res, op = Trailblazer::Endpoint.new(operation_class, params, request, options).(&block)
|
@@ -58,6 +63,19 @@ private
|
|
58
63
|
[res, op]
|
59
64
|
end
|
60
65
|
|
66
|
+
def deprecate_positional_params_argument!(options) # TODO: remove in 1.1.
|
67
|
+
return options if options.has_key?(:skip_form)
|
68
|
+
return options if options.has_key?(:is_document)
|
69
|
+
return options if options.has_key?(:params)
|
70
|
+
return options if options.has_key?(:namespace)
|
71
|
+
return options if options.has_key?(:___dont_deprecate)
|
72
|
+
return options if options.size == 0
|
73
|
+
|
74
|
+
warn "[Trailblazer] The positional params argument for #run, #present, #form and #respond is deprecated and will be removed in 1.1.
|
75
|
+
Please provide a custom params via `run Comment::Create, params: {..}` and have a nice day."
|
76
|
+
{params: options}
|
77
|
+
end
|
78
|
+
|
61
79
|
def setup_operation_instance_variables!(operation, options)
|
62
80
|
@operation = operation
|
63
81
|
@model = operation.model
|
@@ -33,6 +33,7 @@ module Trailblazer::Operation::Representer
|
|
33
33
|
options_from: :deserializer, # use :instance etc. in deserializer.
|
34
34
|
superclass: Representable::Decorator,
|
35
35
|
representer_from: lambda { |inline| inline.representer_class },
|
36
|
+
exclude_options: [:default, :populator] # TODO: test with populator: in an operation.
|
36
37
|
)
|
37
38
|
end
|
38
39
|
end
|
data/lib/trailblazer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trailblazer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Sutterer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uber
|
@@ -237,4 +237,3 @@ test_files:
|
|
237
237
|
- test/representer_test.rb
|
238
238
|
- test/rollback_test.rb
|
239
239
|
- test/test_helper.rb
|
240
|
-
has_rdoc:
|