trailblazer 0.3.3 → 1.0.0.rc1
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/.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
|
+
[](http://badge.fury.io/rb/trailblazer)
|
6
|
+
[](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'
|