trailblazer 0.3.3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGES.md +12 -0
- data/Gemfile +2 -1
- data/README.md +73 -38
- data/Rakefile +1 -1
- data/lib/trailblazer/autoloading.rb +4 -1
- data/lib/trailblazer/endpoint.rb +7 -13
- data/lib/trailblazer/operation.rb +54 -40
- data/lib/trailblazer/operation/builder.rb +26 -0
- data/lib/trailblazer/operation/collection.rb +1 -2
- data/lib/trailblazer/operation/controller.rb +36 -48
- data/lib/trailblazer/operation/dispatch.rb +11 -11
- data/lib/trailblazer/operation/model.rb +50 -0
- data/lib/trailblazer/operation/model/dsl.rb +29 -0
- data/lib/trailblazer/operation/model/external.rb +34 -0
- data/lib/trailblazer/operation/policy.rb +87 -0
- data/lib/trailblazer/operation/policy/guard.rb +34 -0
- data/lib/trailblazer/operation/representer.rb +33 -12
- data/lib/trailblazer/operation/resolver.rb +30 -0
- data/lib/trailblazer/operation/responder.rb +0 -1
- data/lib/trailblazer/operation/worker.rb +24 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +2 -1
- data/test/{crud_test.rb → model_test.rb} +17 -35
- data/test/operation/builder_test.rb +41 -0
- data/test/operation/dsl/callback_test.rb +108 -0
- data/test/operation/dsl/contract_test.rb +104 -0
- data/test/operation/dsl/representer_test.rb +143 -0
- data/test/operation/external_model_test.rb +71 -0
- data/test/operation/guard_test.rb +97 -0
- data/test/operation/policy_test.rb +97 -0
- data/test/operation/resolver_test.rb +83 -0
- data/test/operation_test.rb +7 -75
- data/test/rails/__respond_test.rb +20 -0
- data/test/rails/controller_test.rb +4 -102
- data/test/rails/endpoint_test.rb +7 -47
- data/test/rails/fake_app/controllers.rb +16 -21
- data/test/rails/fake_app/rails_app.rb +5 -0
- data/test/rails/fake_app/song/operations.rb +11 -4
- data/test/rails/respond_test.rb +95 -0
- data/test/responder_test.rb +6 -6
- data/test/rollback_test.rb +2 -2
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +2 -2
- metadata +38 -15
- data/lib/trailblazer/operation/crud.rb +0 -82
- data/lib/trailblazer/rails/railtie.rb +0 -34
- data/test/rails/fake_app/views/bands/show.html.erb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c67bd17df3cf4afa247418e31cf3747e07ae7f1
|
4
|
+
data.tar.gz: ceb2f1a7f81c63d82152938607f4c46cb4b075fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 633a5a70e6b0f362247be2509dd637e234d10135f0d6c1917c81d3646646e12ae5efdaa964468592203f5eabafc274b1620c2d48b6b5870c26551bc92e11f971
|
7
|
+
data.tar.gz: 6276f20bc9d09a11d8dc313efd964383a04d1e05f77d87eded95e64953c8e854ecdaf0ec5be23f916fbafef0a867bffd6b89fc8b5b73ddd5e41cbddf1d3ef055
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# 1.0.0
|
2
|
+
|
3
|
+
* `Operation[{..}]` is deprecated in favor of `Operation.({..})`.
|
4
|
+
setup in initialize: when Op.run() with Worker, the policy will be run "delayed" and not with the actual permission set. this will result in many crashing sidekiq workers.
|
5
|
+
* `Operation::CRUD` is now `Operation::Model`.
|
6
|
+
* `Controller#form` now invokes `#prepopulate!` before rendering the view. Use `#present` if you don't want that.
|
7
|
+
|
8
|
+
# 0.3.4
|
9
|
+
|
10
|
+
* Added `Operation::Policy`.
|
11
|
+
* Added `Operation::Resolver`.
|
12
|
+
|
1
13
|
# 0.3.3
|
2
14
|
|
3
15
|
* Add `Operation::reject` which will run the block when _invalid_.
|
data/Gemfile
CHANGED
@@ -4,8 +4,9 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
# gem "representable", path: "../representable"
|
7
|
-
# gem "reform", path: "../reform"
|
8
7
|
# gem "disposable", path: "../disposable"
|
9
8
|
gem "virtus"
|
10
9
|
# gem "reform", github: "apotonick/reform"
|
11
10
|
gem "reform", "~> 2.0.0"
|
11
|
+
# gem "reform", path: "../reform"
|
12
|
+
gem "multi_json"
|
data/README.md
CHANGED
@@ -2,7 +2,23 @@
|
|
2
2
|
|
3
3
|
_Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation, an intuitive code structure and gives you an object-oriented architecture._
|
4
4
|
|
5
|
-
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/trailblazer.svg)](http://badge.fury.io/rb/trailblazer)
|
6
|
+
[![Gitter Chat](https://badges.gitter.im/trailblazer/chat.svg)](https://gitter.im/trailblazer/chat)
|
7
|
+
|
8
|
+
|
9
|
+
## Trailblazer In A Nutshell
|
10
|
+
|
11
|
+
1. All business logic is encapsulated in [operations](#operation) (service objects).
|
12
|
+
* An optional Reform [form](#validations) object in the operation deserializes and validates input. The form object can also be used for rendering.
|
13
|
+
* An optional [policy](#policies) object blocks unauthorized users from running the operation.
|
14
|
+
* Optional [callback](#callbacks) objects allow declaring post-processing logic.
|
15
|
+
3. [Controllers](#controllers) instantly delegate to an operation. No business code in controllers, only HTTP-specific logic.
|
16
|
+
4. [Models](#models) are persistence-only and solely define associations and scopes. No business code is to be found here. No validations, no callbacks.
|
17
|
+
5. The presentation layer offers optional [view models](#cells) (Cells) and [representers](#representers) for document APIs.
|
18
|
+
|
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
|
+
|
21
|
+
|
6
22
|
|
7
23
|
## Mission
|
8
24
|
|
@@ -81,7 +97,7 @@ Operations encapsulate business logic and are the heart of a Trailblazer archite
|
|
81
97
|
|
82
98
|
An operation is not just a monolithic replacement for your business code. An operation is a simple orchestrator between the form object, models and your business code.
|
83
99
|
|
84
|
-
You don't have to use the form/contract if you don't want it
|
100
|
+
You don't have to use the form/contract if you don't want it.
|
85
101
|
|
86
102
|
```ruby
|
87
103
|
class Comment < ActiveRecord::Base
|
@@ -158,7 +174,7 @@ class Create < Trailblazer::Operation
|
|
158
174
|
end
|
159
175
|
```
|
160
176
|
|
161
|
-
The _Imperative Callback_ pattern then allows you to call this _callback group_
|
177
|
+
The _Imperative Callback_ pattern then allows you to call this _callback group_ wherever you need it.
|
162
178
|
|
163
179
|
```ruby
|
164
180
|
class Create < Trailblazer::Operation
|
@@ -193,6 +209,52 @@ end
|
|
193
209
|
|
194
210
|
Only operations and views/cells can access models directly.
|
195
211
|
|
212
|
+
## Policies
|
213
|
+
|
214
|
+
The full documentation for [Policy is here](http://trailblazerb.org/gems/operation/policy.html).
|
215
|
+
|
216
|
+
You can abort running an operation using a policy. "[Pundit](https://github.com/elabs/pundit)-style" policy classes define the rules.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class Thing::Policy
|
220
|
+
def initialize(user, thing)
|
221
|
+
@user, @thing = user, thing
|
222
|
+
end
|
223
|
+
|
224
|
+
def create?
|
225
|
+
@user.admin?
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
The rule is enabled via the `::policy` call.
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class Thing::Create < Trailblazer::Operation
|
234
|
+
include Policy
|
235
|
+
|
236
|
+
policy Thing::Policy, :create?
|
237
|
+
```
|
238
|
+
|
239
|
+
The policy is evaluated in `#setup!`, raises an exception if `false` and suppresses running `#process`.
|
240
|
+
|
241
|
+
```ruby
|
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
|
+
```
|
196
258
|
|
197
259
|
## Views
|
198
260
|
|
@@ -222,13 +284,13 @@ The operation can then parse incoming JSON documents in `validate` and render a
|
|
222
284
|
|
223
285
|
## Tests
|
224
286
|
|
225
|
-
Subject to tests are mainly _Operation_s and _View Model_s, as they encapsulate endpoint
|
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.
|
226
288
|
|
227
289
|
|
228
290
|
|
229
291
|
## Overview
|
230
292
|
|
231
|
-
Trailblazer is
|
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.
|
232
294
|
|
233
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.
|
234
296
|
|
@@ -243,26 +305,6 @@ class CommentsController < ApplicationController
|
|
243
305
|
include Trailblazer::Operation::Controller
|
244
306
|
```
|
245
307
|
|
246
|
-
### Rendering the form object
|
247
|
-
|
248
|
-
Operations can populate and present their form object so it can be used with `simple_form` and other form helpers.
|
249
|
-
|
250
|
-
```ruby
|
251
|
-
|
252
|
-
def new
|
253
|
-
form Comment::Create
|
254
|
-
end
|
255
|
-
```
|
256
|
-
|
257
|
-
This will run the operation but _not_ its `validate` code. It then sets the `@form` instance variable in the controller so it can be rendered.
|
258
|
-
|
259
|
-
```haml
|
260
|
-
= form_for @form do |f|
|
261
|
-
= f.input f.body
|
262
|
-
```
|
263
|
-
|
264
|
-
`#form` is meant for HTML actions like `#new` and `#edit`, only.
|
265
|
-
|
266
308
|
### Running an operation
|
267
309
|
|
268
310
|
If you do not intend to maintain different request formats, the easiest is to use `#run` to process incoming data using an operation.
|
@@ -514,14 +556,14 @@ When inheriting, the block is `class_eval`ed in the inherited class' context and
|
|
514
556
|
|
515
557
|
### CRUD Semantics
|
516
558
|
|
517
|
-
You can make Trailblazer find and create models for you using the `
|
559
|
+
You can make Trailblazer find and create models for you using the `Model` module.
|
518
560
|
|
519
561
|
```ruby
|
520
|
-
require 'trailblazer/operation/
|
562
|
+
require 'trailblazer/operation/model'
|
521
563
|
|
522
564
|
class Comment < ActiveRecord::Base
|
523
565
|
class Create < Trailblazer::Operation
|
524
|
-
include
|
566
|
+
include Model
|
525
567
|
model Comment, :create
|
526
568
|
|
527
569
|
contract do
|
@@ -537,9 +579,9 @@ class Comment < ActiveRecord::Base
|
|
537
579
|
end
|
538
580
|
```
|
539
581
|
|
540
|
-
You have to tell `
|
582
|
+
You have to tell `Model` the model class and what action to implement using `::model`.
|
541
583
|
|
542
|
-
Note how you do not have to pass the `@model` to validate anymore. Also, the `@model` gets created automatically and is
|
584
|
+
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`.
|
543
585
|
|
544
586
|
In inherited operations, you can override the action, only, using `::action`.
|
545
587
|
|
@@ -614,14 +656,6 @@ to `config/initializers/trailblazer.rb` and implementation classes like `Operati
|
|
614
656
|
|
615
657
|
If you structure your CRUD operations using the `app/concepts/*/crud.rb` file layout we use in the book, the `crud.rb` files are not gonna be found by Rails automatically. It is a good idea to enable CRUD autoloading.
|
616
658
|
|
617
|
-
At the end of your `config/application.rb` file, add the following.
|
618
|
-
|
619
|
-
```ruby
|
620
|
-
require "trailblazer/rails/railtie"
|
621
|
-
```
|
622
|
-
|
623
|
-
This will go through `app/concepts/`, find all the `crud.rb` files, autoload their corresponding namespace (e.g. `Thing`, which is a model) and then load the `crud.rb` file.
|
624
|
-
|
625
659
|
|
626
660
|
## Installation
|
627
661
|
|
@@ -629,6 +663,7 @@ The obvious needs to be in your `Gemfile`.
|
|
629
663
|
|
630
664
|
```ruby
|
631
665
|
gem "trailblazer"
|
666
|
+
gem "trailblazer-rails" # if you are in rails.
|
632
667
|
gem "cells"
|
633
668
|
```
|
634
669
|
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ default_task = Rake::Task[:build]
|
|
6
6
|
|
7
7
|
Rake::TestTask.new(:test) do |test|
|
8
8
|
test.libs << 'test'
|
9
|
-
test.test_files = FileList['test/*_test.rb', "test/operation
|
9
|
+
test.test_files = FileList['test/*_test.rb', "test/operation/**/*_test.rb"]
|
10
10
|
test.verbose = true
|
11
11
|
end
|
12
12
|
|
@@ -1,8 +1,11 @@
|
|
1
1
|
Trailblazer::Operation.class_eval do
|
2
2
|
autoload :Controller, "trailblazer/operation/controller"
|
3
3
|
autoload :Responder, "trailblazer/operation/responder"
|
4
|
-
autoload :
|
4
|
+
autoload :Model, "trailblazer/operation/model"
|
5
5
|
autoload :Collection, "trailblazer/operation/collection"
|
6
6
|
autoload :Dispatch, "trailblazer/operation/dispatch"
|
7
7
|
autoload :Module, "trailblazer/operation/module"
|
8
|
+
autoload :Representer,"trailblazer/operation/representer"
|
9
|
+
autoload :Policy, "trailblazer/operation/policy"
|
10
|
+
autoload :Resolver, "trailblazer/operation/resolver"
|
8
11
|
end
|
data/lib/trailblazer/endpoint.rb
CHANGED
@@ -1,31 +1,25 @@
|
|
1
1
|
module Trailblazer
|
2
|
+
# Encapsulates HTTP-specific logic needed before running an operation.
|
3
|
+
# Right now, all this does is #document_body!
|
2
4
|
# To be used in Lotus, Roda, Rails, etc.
|
3
5
|
class Endpoint
|
4
|
-
def initialize(
|
5
|
-
@controller = controller
|
6
|
+
def initialize(operation_class, params, request, options)
|
6
7
|
@operation_class = operation_class
|
7
8
|
@params = params
|
8
9
|
@request = request
|
9
|
-
@
|
10
|
+
@is_document = options[:is_document]
|
10
11
|
end
|
11
12
|
|
12
13
|
def call
|
13
|
-
@controller.send(:process_params!, params) # FIXME.
|
14
|
-
|
15
14
|
document_body! if document_request?
|
16
|
-
|
17
|
-
res, operation = yield # Create.run(params)
|
18
|
-
@controller.send(:setup_operation_instance_variables!, operation)
|
19
|
-
|
20
|
-
[res, operation] # DISCUSS: do we need result here? or can we just go pick op.valid?
|
15
|
+
yield # Create.run(params)
|
21
16
|
end
|
22
17
|
|
23
18
|
private
|
24
|
-
attr_reader :params, :operation_class, :request
|
19
|
+
attr_reader :params, :operation_class, :request
|
25
20
|
|
26
21
|
def document_request?
|
27
|
-
|
28
|
-
@config[:document_formats][request.format.to_sym]
|
22
|
+
@is_document
|
29
23
|
end
|
30
24
|
|
31
25
|
def document_body!
|
@@ -1,9 +1,9 @@
|
|
1
|
-
require "uber/builder"
|
2
1
|
require "reform"
|
3
2
|
|
4
|
-
|
5
3
|
module Trailblazer
|
6
4
|
class Operation
|
5
|
+
require "trailblazer/operation/builder"
|
6
|
+
extend Builder # imports ::builder_class and ::build_operation.
|
7
7
|
extend Uber::InheritableAttr
|
8
8
|
inheritable_attr :contract_class
|
9
9
|
self.contract_class = Reform::Form.clone
|
@@ -15,8 +15,8 @@ module Trailblazer
|
|
15
15
|
end
|
16
16
|
|
17
17
|
class << self
|
18
|
-
def run(
|
19
|
-
res, op =
|
18
|
+
def run(params, &block) # Endpoint behaviour
|
19
|
+
res, op = build_operation(params).run
|
20
20
|
|
21
21
|
if block_given?
|
22
22
|
yield op if res
|
@@ -30,49 +30,55 @@ module Trailblazer
|
|
30
30
|
def reject(*args)
|
31
31
|
res, op = run(*args)
|
32
32
|
yield op if res == false
|
33
|
-
|
33
|
+
op
|
34
34
|
end
|
35
35
|
|
36
36
|
# ::call only returns the Operation instance (or whatever was returned from #validate).
|
37
37
|
# This is useful in tests or in irb, e.g. when using Op as a factory and you already know it's valid.
|
38
|
-
def call(
|
39
|
-
|
38
|
+
def call(params)
|
39
|
+
build_operation(params, raise_on_invalid: true).run.last
|
40
40
|
end
|
41
|
-
alias_method :[], :call # TODO: deprecate #[] in favor of .().
|
42
41
|
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
def [](*args) # TODO: remove in 1.1.
|
43
|
+
warn "[Trailblazer] Operation[] is deprecated. Please use Operation.() and have a nice day."
|
44
|
+
call(*args)
|
46
45
|
end
|
47
46
|
|
48
|
-
|
49
|
-
|
47
|
+
# Runs #setup! and returns the form object.
|
48
|
+
def present(params)
|
49
|
+
build_operation(params).present
|
50
50
|
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
# This is a DSL method. Use ::contract_class and ::contract_class= for the explicit version.
|
53
|
+
# Op.contract #=> returns contract class
|
54
|
+
# Op.contract do .. end # defines contract
|
55
|
+
# Op.contract CommentForm # copies (and subclasses) external contract.
|
56
|
+
# Op.contract CommentForm do .. end # copies and extends contract.
|
57
|
+
def contract(constant=nil, &block)
|
58
|
+
return contract_class unless constant or block_given?
|
59
|
+
|
60
|
+
self.contract_class= Class.new(constant) if constant
|
61
|
+
contract_class.class_eval(&block) if block_given?
|
55
62
|
end
|
56
63
|
end
|
57
64
|
|
58
|
-
include Uber::Builder
|
59
65
|
|
60
|
-
def initialize(options={})
|
66
|
+
def initialize(params, options={})
|
67
|
+
@params = params
|
68
|
+
@options = options
|
61
69
|
@valid = true
|
62
|
-
|
70
|
+
|
71
|
+
setup!(params) # assign/find the model
|
63
72
|
end
|
64
73
|
|
65
74
|
# Operation.run(body: "Fabulous!") #=> [true, <Comment body: "Fabulous!">]
|
66
|
-
def run
|
67
|
-
|
68
|
-
|
69
|
-
process(*params)
|
75
|
+
def run
|
76
|
+
process(@params)
|
70
77
|
|
71
78
|
[valid?, self]
|
72
79
|
end
|
73
80
|
|
74
|
-
def present
|
75
|
-
setup!(*params)
|
81
|
+
def present
|
76
82
|
contract!
|
77
83
|
self
|
78
84
|
end
|
@@ -92,23 +98,33 @@ module Trailblazer
|
|
92
98
|
end
|
93
99
|
|
94
100
|
private
|
95
|
-
|
96
|
-
|
101
|
+
module Setup
|
102
|
+
def setup!(params)
|
103
|
+
setup_params!(params)
|
104
|
+
build_model!(params)
|
105
|
+
end
|
97
106
|
|
98
|
-
|
99
|
-
|
100
|
-
end
|
107
|
+
def setup_params!(params)
|
108
|
+
end
|
101
109
|
|
102
|
-
|
103
|
-
|
104
|
-
|
110
|
+
def build_model!(*args)
|
111
|
+
assign_model!(*args) # @model = ..
|
112
|
+
setup_model!(*args)
|
113
|
+
end
|
105
114
|
|
106
|
-
|
107
|
-
|
108
|
-
|
115
|
+
def assign_model!(*args)
|
116
|
+
@model = model!(*args)
|
117
|
+
end
|
109
118
|
|
110
|
-
|
119
|
+
# Implement #model! to find/create your operation model (if required).
|
120
|
+
def model!(params)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Override to add attributes that can be infered from params.
|
124
|
+
def setup_model!(params)
|
125
|
+
end
|
111
126
|
end
|
127
|
+
include Setup
|
112
128
|
|
113
129
|
def validate(params, model=nil, contract_class=nil)
|
114
130
|
contract!(model, contract_class)
|
@@ -133,7 +149,7 @@ module Trailblazer
|
|
133
149
|
|
134
150
|
# When using Op::[], an invalid contract will raise an exception.
|
135
151
|
def raise!(contract)
|
136
|
-
raise InvalidContract.new(contract.errors.to_s) if @raise_on_invalid
|
152
|
+
raise InvalidContract.new(contract.errors.to_s) if @options[:raise_on_invalid]
|
137
153
|
end
|
138
154
|
|
139
155
|
# Instantiate the contract, either by using the user's contract passed into #validate
|
@@ -153,5 +169,3 @@ module Trailblazer
|
|
153
169
|
end
|
154
170
|
end
|
155
171
|
end
|
156
|
-
|
157
|
-
require 'trailblazer/operation/crud'
|