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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGES.md +12 -0
  4. data/Gemfile +2 -1
  5. data/README.md +73 -38
  6. data/Rakefile +1 -1
  7. data/lib/trailblazer/autoloading.rb +4 -1
  8. data/lib/trailblazer/endpoint.rb +7 -13
  9. data/lib/trailblazer/operation.rb +54 -40
  10. data/lib/trailblazer/operation/builder.rb +26 -0
  11. data/lib/trailblazer/operation/collection.rb +1 -2
  12. data/lib/trailblazer/operation/controller.rb +36 -48
  13. data/lib/trailblazer/operation/dispatch.rb +11 -11
  14. data/lib/trailblazer/operation/model.rb +50 -0
  15. data/lib/trailblazer/operation/model/dsl.rb +29 -0
  16. data/lib/trailblazer/operation/model/external.rb +34 -0
  17. data/lib/trailblazer/operation/policy.rb +87 -0
  18. data/lib/trailblazer/operation/policy/guard.rb +34 -0
  19. data/lib/trailblazer/operation/representer.rb +33 -12
  20. data/lib/trailblazer/operation/resolver.rb +30 -0
  21. data/lib/trailblazer/operation/responder.rb +0 -1
  22. data/lib/trailblazer/operation/worker.rb +24 -7
  23. data/lib/trailblazer/version.rb +1 -1
  24. data/test/collection_test.rb +2 -1
  25. data/test/{crud_test.rb → model_test.rb} +17 -35
  26. data/test/operation/builder_test.rb +41 -0
  27. data/test/operation/dsl/callback_test.rb +108 -0
  28. data/test/operation/dsl/contract_test.rb +104 -0
  29. data/test/operation/dsl/representer_test.rb +143 -0
  30. data/test/operation/external_model_test.rb +71 -0
  31. data/test/operation/guard_test.rb +97 -0
  32. data/test/operation/policy_test.rb +97 -0
  33. data/test/operation/resolver_test.rb +83 -0
  34. data/test/operation_test.rb +7 -75
  35. data/test/rails/__respond_test.rb +20 -0
  36. data/test/rails/controller_test.rb +4 -102
  37. data/test/rails/endpoint_test.rb +7 -47
  38. data/test/rails/fake_app/controllers.rb +16 -21
  39. data/test/rails/fake_app/rails_app.rb +5 -0
  40. data/test/rails/fake_app/song/operations.rb +11 -4
  41. data/test/rails/respond_test.rb +95 -0
  42. data/test/responder_test.rb +6 -6
  43. data/test/rollback_test.rb +2 -2
  44. data/test/worker_test.rb +13 -9
  45. data/trailblazer.gemspec +2 -2
  46. metadata +38 -15
  47. data/lib/trailblazer/operation/crud.rb +0 -82
  48. data/lib/trailblazer/rails/railtie.rb +0 -34
  49. 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: c6355544b09375074e8ef41304a2ccdf99d5bdf9
4
- data.tar.gz: 5c80ec4a93e1628e84aa6a83290027ccfdb02cca
3
+ metadata.gz: 4c67bd17df3cf4afa247418e31cf3747e07ae7f1
4
+ data.tar.gz: ceb2f1a7f81c63d82152938607f4c46cb4b075fc
5
5
  SHA512:
6
- metadata.gz: 6c27cbe26a73a84c413341a2d9ec528c80083d98b4110b71f8569a72961a3932790347c47389a7fda74d3aa61a5c196e8178fa186c86fa1016048726e71705eb
7
- data.tar.gz: 4c359e9e33ea39d269710a04d615a59a4d8e53225f9438e2f70b3f83eb6182c85beea60479b54b34d6a193216ba61b86b12719e9f88df51bc5fd13b11364be29
6
+ metadata.gz: 633a5a70e6b0f362247be2509dd637e234d10135f0d6c1917c81d3646646e12ae5efdaa964468592203f5eabafc274b1620c2d48b6b5870c26551bc92e11f971
7
+ data.tar.gz: 6276f20bc9d09a11d8dc313efd964383a04d1e05f77d87eded95e64953c8e854ecdaf0ec5be23f916fbafef0a867bffd6b89fc8b5b73ddd5e41cbddf1d3ef055
@@ -3,4 +3,4 @@ rvm:
3
3
  - 2.2.0
4
4
  - 2.1.2
5
5
  - 2.0.0
6
- - 1.9.3
6
+ # - 1.9.3
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
- In a nutshell: Trailblazer makes you write **logicless models** that purely act as data objects, don't contain callbacks, nested attributes, validations or domain logic. **Controllers** become lean HTTP endpoints. Your **business logic** (including validation) is decoupled from the actual Rails framework and modeled in _operations_.
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, BTW.
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_ whereever you need it.
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 behaviour of your app. As a nice side effect, factories are replaced by simple _Operation_ calls.
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 basically a mash-up of mature gems that have been developed over the past 10 years and are used in hundreds and thousands of production apps.
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 `CRUD` module.
559
+ You can make Trailblazer find and create models for you using the `Model` module.
518
560
 
519
561
  ```ruby
520
- require 'trailblazer/operation/crud'
562
+ require 'trailblazer/operation/model'
521
563
 
522
564
  class Comment < ActiveRecord::Base
523
565
  class Create < Trailblazer::Operation
524
- include CRUD
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 `CRUD` the model class and what action to implement using `::model`.
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 accessable using `Operation#model`.
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/*_test.rb"]
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 :CRUD, "trailblazer/operation/crud"
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
@@ -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(controller, operation_class, params, request, config)
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
- @config = config
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, :controller
19
+ attr_reader :params, :operation_class, :request
25
20
 
26
21
  def document_request?
27
- # request.format == :html
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(*params, &block) # Endpoint behaviour
19
- res, op = build_operation_class(*params).new.run(*params)
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
- return op
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(*params)
39
- build_operation_class(*params).new(raise_on_invalid: true).run(*params).last
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
- # Runs #setup! and returns the form object.
44
- def present(*params)
45
- build_operation_class(*params).new.present(*params)
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
- def contract(&block)
49
- contract_class.class_eval(&block)
47
+ # Runs #setup! and returns the form object.
48
+ def present(params)
49
+ build_operation(params).present
50
50
  end
51
51
 
52
- private
53
- def build_operation_class(*params)
54
- class_builder.call(*params) # Uber::Builder::class_builder
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
- @raise_on_invalid = options[:raise_on_invalid] || false
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(*params)
67
- setup!(*params) # where do we assign/find the model?
68
-
69
- process(*params)
75
+ def run
76
+ process(@params)
70
77
 
71
78
  [valid?, self]
72
79
  end
73
80
 
74
- def present(*params)
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
- def setup!(*params)
96
- setup_params!(*params)
101
+ module Setup
102
+ def setup!(params)
103
+ setup_params!(params)
104
+ build_model!(params)
105
+ end
97
106
 
98
- @model = model!(*params)
99
- setup_model!(*params)
100
- end
107
+ def setup_params!(params)
108
+ end
101
109
 
102
- # Implement #model! to find/create your operation model (if required).
103
- def model!(*params)
104
- end
110
+ def build_model!(*args)
111
+ assign_model!(*args) # @model = ..
112
+ setup_model!(*args)
113
+ end
105
114
 
106
- # Override to add attributes that can be infered from params.
107
- def setup_model!(*params)
108
- end
115
+ def assign_model!(*args)
116
+ @model = model!(*args)
117
+ end
109
118
 
110
- def setup_params!(*params)
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'