trailblazer 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -1
- data/CHANGES.md +7 -0
- data/README.md +67 -0
- data/lib/trailblazer/operation.rb +10 -3
- data/lib/trailblazer/operation/controller.rb +6 -2
- data/lib/trailblazer/operation/controller/active_record.rb +12 -0
- data/lib/trailblazer/operation/crud.rb +28 -5
- data/lib/trailblazer/rails/railtie.rb +3 -1
- data/lib/trailblazer/version.rb +1 -1
- data/test/crud_test.rb +29 -0
- data/test/operation_test.rb +15 -8
- data/test/rails/controller_test.rb +12 -3
- data/test/rails/fake_app/controllers.rb +14 -1
- data/test/rails/fake_app/rails_app.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 973befd1d4260ea9bee84355c8b204084a51b5f9
|
4
|
+
data.tar.gz: 41e5430171b1d65a99716fd0502a9b9c8eb9d078
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a54b22de4bd4966d6045fc78b0a0720fe1d7d4fbfc044115e476eb669fc90cbac645fcb9f780806ddc89c1ac6de7b984341956a703d35954f3e2c5730e64e1cb
|
7
|
+
data.tar.gz: 495cccf41dbb172fce76dcf9b5b8de68e66961ae78efcc1bfc5491400fa40fb6cb84e9343d56e827fe991f203ea00c6df547299ca13c87c4242126109215bb3a
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# 0.2.1
|
2
|
+
|
3
|
+
* Added `Operation#setup_model!(params)` that can be overridden to add nested objects or process models right after `model!`. Don't add deserialization logic here, let Reform/Representable do that.
|
4
|
+
* Added `Operation#setup_params!(params)` to normalize parameters before `#process`. Thanks to @gogogarrett.
|
5
|
+
* Added `Controller::ActiveRecord` that will setup a named controller instance variable for your operation model. Thanks @gogogarrett!
|
6
|
+
* Added `CRUD::ActiveModel` that currently infers the contract's `::model` from the operation's model.
|
7
|
+
|
1
8
|
# 0.2.0
|
2
9
|
|
3
10
|
## API Changes
|
data/README.md
CHANGED
@@ -179,6 +179,8 @@ Note that `#present` will leave rendering up to you - `respond_to` is _not_ call
|
|
179
179
|
|
180
180
|
In all three cases the following instance variables are assigned: `@operation`, `@form`, `@model`.
|
181
181
|
|
182
|
+
Named instance variables can be included, too. This is documented [here](#named-controller-instance-variables).
|
183
|
+
|
182
184
|
## Operation
|
183
185
|
|
184
186
|
Operations encapsulate business logic. One operation per high-level domain _function_ is used. Different formats or environments are handled in subclasses. Operations don't know about HTTP.
|
@@ -361,6 +363,28 @@ end
|
|
361
363
|
|
362
364
|
Another action is `:find` (which is currently doing the same as `:update`) to find a model by using `params[:id]`.
|
363
365
|
|
366
|
+
|
367
|
+
### Normalizing params
|
368
|
+
|
369
|
+
Override #setup_params! to add or remove values to params before the operation is run.
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
class Create < Trailblazer::Operation
|
373
|
+
def process(params)
|
374
|
+
params #=> {show_all: true, admin: true, .. }
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
def process_params!(params)
|
379
|
+
params.merge!(show_all: true) if params[:admin]
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
```
|
384
|
+
|
385
|
+
This centralizes params normalization and doesn't require you to do that manually in `#process`.
|
386
|
+
|
387
|
+
|
364
388
|
### Background Processing
|
365
389
|
|
366
390
|
To run an operation in Sidekiq (ActiveJob-support coming!) all you need to do is include the `Worker` module.
|
@@ -433,6 +457,49 @@ require "trailblazer/rails/railtie"
|
|
433
457
|
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.
|
434
458
|
|
435
459
|
|
460
|
+
## Undocumented Features
|
461
|
+
|
462
|
+
(Please don't read this section!)
|
463
|
+
|
464
|
+
### Additional Model Setup
|
465
|
+
|
466
|
+
Override `Operation#setup_model(params)` to add nested objects that can be infered from `params` or are static.
|
467
|
+
|
468
|
+
This is called right after `#model!`.
|
469
|
+
|
470
|
+
### ActiveModel Semantics
|
471
|
+
|
472
|
+
When using `Reform::Form::ActiveModel` (which is used automatically in a Rails environment to make form builders work) you need to invoke `model Comment` in the contract. This can be inferred automatically from the operation by including `CRUD::ActiveModel`.
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
class Create < Trailblazer::Operation
|
476
|
+
include CRUD
|
477
|
+
include CRUD::ActiveModel
|
478
|
+
|
479
|
+
model Comment
|
480
|
+
|
481
|
+
contract do # no need to call ::model, here.
|
482
|
+
property :text
|
483
|
+
end
|
484
|
+
```
|
485
|
+
|
486
|
+
If you want that in all CRUD operations, check out [how you can include](https://github.com/apotonick/gemgem-trbrb/blob/chapter-5/config/initializers/trailblazer.rb#L26) it automatically.
|
487
|
+
|
488
|
+
### Named Controller Instance Variables
|
489
|
+
|
490
|
+
If you want to include named instance variables for you views you must include another ActiveRecord specific module.
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
require 'trailblazer/operation/controller/active_record'
|
494
|
+
|
495
|
+
class ApplicationController < ActionController::Base
|
496
|
+
include Trailblazer::Operation::Controller
|
497
|
+
include Trailblazer::Operation::Controller::ActiveRecord
|
498
|
+
end
|
499
|
+
```
|
500
|
+
|
501
|
+
This will setup a named instance variable of your operation's model, for example `@song`.
|
502
|
+
|
436
503
|
## Why?
|
437
504
|
|
438
505
|
* 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.
|
@@ -46,7 +46,6 @@ module Trailblazer
|
|
46
46
|
contract_class.class_eval(&block)
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
49
|
private
|
51
50
|
def build_operation_class(*params)
|
52
51
|
class_builder.call(*params) # Uber::Builder::class_builder
|
@@ -55,7 +54,6 @@ module Trailblazer
|
|
55
54
|
|
56
55
|
include Uber::Builder
|
57
56
|
|
58
|
-
|
59
57
|
def initialize(options={})
|
60
58
|
@valid = true
|
61
59
|
# DISCUSS: use reverse_merge here?
|
@@ -83,15 +81,24 @@ module Trailblazer
|
|
83
81
|
end
|
84
82
|
|
85
83
|
private
|
86
|
-
|
87
84
|
def setup!(*params)
|
85
|
+
setup_params!(*params)
|
86
|
+
|
88
87
|
@model = model!(*params)
|
88
|
+
setup_model!(*params)
|
89
89
|
end
|
90
90
|
|
91
91
|
# Implement #model! to find/create your operation model (if required).
|
92
92
|
def model!(*params)
|
93
93
|
end
|
94
94
|
|
95
|
+
# Override to add attributes that can be infered from params.
|
96
|
+
def setup_model!(*params)
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_params!(*params)
|
100
|
+
end
|
101
|
+
|
95
102
|
def validate(params, model, contract_class=nil) # NOT to be overridden?!! it creates Result for us.
|
96
103
|
@contract = contract_for(contract_class, model)
|
97
104
|
|
@@ -71,9 +71,13 @@ private
|
|
71
71
|
end
|
72
72
|
|
73
73
|
res, @operation = yield # Create.run(params)
|
74
|
-
|
75
|
-
@model = @operation.model
|
74
|
+
setup_operation_instance_variables!
|
76
75
|
|
77
76
|
[res, @operation] # DISCUSS: do we need result here? or can we just go pick op.valid?
|
78
77
|
end
|
78
|
+
|
79
|
+
def setup_operation_instance_variables!
|
80
|
+
@form = @operation.contract
|
81
|
+
@model = @operation.model
|
82
|
+
end
|
79
83
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Assigns an additional instance variable for +@model+ named after the model's table name (e.g. @comment).
|
2
|
+
module Trailblazer::Operation::Controller::ActiveRecord
|
3
|
+
private
|
4
|
+
def setup_operation_instance_variables!
|
5
|
+
super
|
6
|
+
instance_variable_set(:"@#{operation_model_name}", @model)
|
7
|
+
end
|
8
|
+
|
9
|
+
def operation_model_name
|
10
|
+
@model.class.table_name.singularize
|
11
|
+
end
|
12
|
+
end
|
@@ -5,13 +5,18 @@ module Trailblazer
|
|
5
5
|
module CRUD
|
6
6
|
attr_reader :model
|
7
7
|
|
8
|
-
|
9
|
-
base
|
10
|
-
|
11
|
-
|
8
|
+
module Included
|
9
|
+
def included(base)
|
10
|
+
base.extend Uber::InheritableAttr
|
11
|
+
base.inheritable_attr :config
|
12
|
+
base.config = {}
|
12
13
|
|
13
|
-
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
14
16
|
end
|
17
|
+
# this makes ::included overrideable, e.g. to add more featues like CRUD::ActiveModel.
|
18
|
+
extend Included
|
19
|
+
|
15
20
|
|
16
21
|
module ClassMethods
|
17
22
|
def model(name, action=nil)
|
@@ -56,6 +61,24 @@ module Trailblazer
|
|
56
61
|
end
|
57
62
|
|
58
63
|
alias_method :find_model, :update_model
|
64
|
+
|
65
|
+
|
66
|
+
# Rails-specific.
|
67
|
+
# ActiveModel will automatically call Form::model when creating the contract and passes
|
68
|
+
# the operation's +::model+, so you don't have to call it twice.
|
69
|
+
# This assumes that the form class includes Form::ActiveModel, though.
|
70
|
+
module ActiveModel
|
71
|
+
def self.included(base)
|
72
|
+
base.extend ClassMethods
|
73
|
+
end
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
def contract(&block)
|
77
|
+
super
|
78
|
+
contract_class.model(model_class) # this assumes that Form::ActiveModel is mixed in.
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
59
82
|
end
|
60
83
|
end
|
61
84
|
end
|
@@ -11,7 +11,9 @@ module Trailblazer
|
|
11
11
|
end
|
12
12
|
|
13
13
|
# thank you, http://stackoverflow.com/a/17573888/465070
|
14
|
-
initializer 'trailblazer.install' do |app|
|
14
|
+
initializer 'trailblazer.install', after: :load_config_initializers do |app|
|
15
|
+
# the trb autoloading has to be run after initializers have been loaded, so we can tweak inclusion of features in
|
16
|
+
# initializers.
|
15
17
|
if Rails.configuration.cache_classes
|
16
18
|
Trailblazer::Railtie.autoload_crud_operations(app)
|
17
19
|
else
|
data/lib/trailblazer/version.rb
CHANGED
data/test/crud_test.rb
CHANGED
@@ -101,6 +101,18 @@ class CrudTest < MiniTest::Spec
|
|
101
101
|
ModelUpdateOperation[{id: 1, song: {title: "Mercy Day For Mr. Vengeance"}}].model.must_equal song
|
102
102
|
end
|
103
103
|
|
104
|
+
|
105
|
+
# Op#setup_model!
|
106
|
+
class SetupModelOperation < CreateOperation
|
107
|
+
def setup_model!(params)
|
108
|
+
model.instance_eval { @params = params; def params; @params.to_s; end }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it { SetupModelOperation[song: {title: "Emily Kane"}].model.params.must_equal "{:song=>{:title=>\"Emily Kane\"}}" }
|
113
|
+
|
114
|
+
|
115
|
+
|
104
116
|
# no call to ::model raises error.
|
105
117
|
class NoModelOperation < Trailblazer::Operation
|
106
118
|
include CRUD
|
@@ -112,4 +124,21 @@ class CrudTest < MiniTest::Spec
|
|
112
124
|
|
113
125
|
# uses :create as default if not set via ::action.
|
114
126
|
it { assert_raises(RuntimeError){ NoModelOperation[{}] } }
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
# contract infers model_name.
|
131
|
+
# TODO: this a Rails/ActiveModel-specific test.
|
132
|
+
class ContractKnowsModelNameOperation < Trailblazer::Operation
|
133
|
+
include CRUD
|
134
|
+
model Song
|
135
|
+
include CRUD::ActiveModel
|
136
|
+
|
137
|
+
contract do
|
138
|
+
include Reform::Form::ActiveModel # this usually happens in Reform::Form::Rails.
|
139
|
+
property :title
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it { ContractKnowsModelNameOperation.present(song: {title: "Direct Hit"}).contract.class.model_name.to_s.must_equal "CrudTest::Song" }
|
115
144
|
end
|
data/test/operation_test.rb
CHANGED
@@ -7,6 +7,20 @@ module Comparable
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
class OperationSetupParamsTest < MiniTest::Spec
|
11
|
+
class OperationSetupParam < Trailblazer::Operation
|
12
|
+
def process(params)
|
13
|
+
params
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_params!(params)
|
17
|
+
params.merge!(garrett: "Rocks!")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let (:operation) { OperationSetupParam.new }
|
22
|
+
it { OperationSetupParam.run({valid: true}).must_equal [true, {valid: true, garrett: "Rocks!"}] }
|
23
|
+
end
|
10
24
|
|
11
25
|
class OperationRunTest < MiniTest::Spec
|
12
26
|
class Operation < Trailblazer::Operation
|
@@ -93,7 +107,6 @@ class OperationRunTest < MiniTest::Spec
|
|
93
107
|
it { Operation.(true).contract.must_equal contract }
|
94
108
|
end
|
95
109
|
|
96
|
-
|
97
110
|
class OperationTest < MiniTest::Spec
|
98
111
|
class Operation < Trailblazer::Operation
|
99
112
|
def process(params)
|
@@ -172,7 +185,6 @@ class OperationTest < MiniTest::Spec
|
|
172
185
|
# ::[]
|
173
186
|
it { OperationWithManualValid.(Object).must_equal(Object) }
|
174
187
|
|
175
|
-
|
176
188
|
# re-assign params
|
177
189
|
class OperationReassigningParams < Trailblazer::Operation
|
178
190
|
def process(params)
|
@@ -184,7 +196,6 @@ class OperationTest < MiniTest::Spec
|
|
184
196
|
# ::run
|
185
197
|
it { OperationReassigningParams.run({:title => "Day Like This"}).must_equal [true, "Day Like This"] }
|
186
198
|
|
187
|
-
|
188
199
|
# #invalid!(result)
|
189
200
|
class OperationCallingInvalid < Trailblazer::Operation
|
190
201
|
def process(params)
|
@@ -206,7 +217,6 @@ class OperationTest < MiniTest::Spec
|
|
206
217
|
|
207
218
|
it { OperationCallingInvalidWithoutResult.run(true).must_equal [false, OperationCallingInvalidWithoutResult.new] }
|
208
219
|
|
209
|
-
|
210
220
|
# calling return from #validate block leaves result true.
|
211
221
|
class OperationUsingReturnInValidate < Trailblazer::Operation
|
212
222
|
self.contract_class = class Contract
|
@@ -229,7 +239,6 @@ class OperationTest < MiniTest::Spec
|
|
229
239
|
it { OperationUsingReturnInValidate.run(true).must_equal [true, 1] }
|
230
240
|
it { OperationUsingReturnInValidate.run(false).must_equal [false, 2] }
|
231
241
|
|
232
|
-
|
233
242
|
# unlimited arguments for ::run and friends.
|
234
243
|
class OperationReceivingLottaArguments < Trailblazer::Operation
|
235
244
|
def process(model, params)
|
@@ -239,7 +248,6 @@ class OperationTest < MiniTest::Spec
|
|
239
248
|
|
240
249
|
it { OperationReceivingLottaArguments.run(Object, {}).must_equal([true, [Object, {}]]) }
|
241
250
|
|
242
|
-
|
243
251
|
# ::present only runs #setup! which runs #model!.
|
244
252
|
class ContractOnlyOperation < Trailblazer::Operation
|
245
253
|
self.contract_class = class Contract
|
@@ -260,7 +268,6 @@ class OperationTest < MiniTest::Spec
|
|
260
268
|
end
|
261
269
|
|
262
270
|
it { ContractOnlyOperation.present({}).contract._model.must_equal Object }
|
263
|
-
|
264
271
|
end
|
265
272
|
|
266
273
|
class OperationBuilderTest < MiniTest::Spec
|
@@ -332,4 +339,4 @@ class OperationInheritanceTest < MiniTest::Spec
|
|
332
339
|
song.genre.must_equal "Punkrock"
|
333
340
|
song.band.must_equal nil
|
334
341
|
end
|
335
|
-
end
|
342
|
+
end
|
@@ -124,7 +124,6 @@ class ControllerRespondTest < ActionController::TestCase
|
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
|
-
|
128
127
|
class ResponderRunTest < ActionController::TestCase
|
129
128
|
tests BandsController
|
130
129
|
|
@@ -171,7 +170,6 @@ class ControllerPresentTest < ActionController::TestCase
|
|
171
170
|
end
|
172
171
|
end
|
173
172
|
|
174
|
-
|
175
173
|
# #form.
|
176
174
|
class ControllerFormTest < ActionController::TestCase
|
177
175
|
tests BandsController
|
@@ -194,4 +192,15 @@ class ControllerFormTest < ActionController::TestCase
|
|
194
192
|
|
195
193
|
assert_select "b", ",Band,true,Band::Create::Admin"
|
196
194
|
end
|
197
|
-
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class ActiveRecordPresentTest < ActionController::TestCase
|
198
|
+
tests ActiveRecordBandsController
|
199
|
+
|
200
|
+
test "#present" do
|
201
|
+
band = Band::Create[band: {name: "Nofx"}].model
|
202
|
+
get :show, id: band.id
|
203
|
+
|
204
|
+
assert_equal "active_record_bands/show.html: Band, Band, true, Band::Update", response.body
|
205
|
+
end
|
206
|
+
end
|
@@ -102,4 +102,17 @@ private
|
|
102
102
|
params[:band] ||= {}
|
103
103
|
params[:band][:locality] = "Essen"
|
104
104
|
end
|
105
|
-
end
|
105
|
+
end
|
106
|
+
|
107
|
+
require 'trailblazer/operation/controller/active_record'
|
108
|
+
class ActiveRecordBandsController < ApplicationController
|
109
|
+
include Trailblazer::Operation::Controller
|
110
|
+
include Trailblazer::Operation::Controller::ActiveRecord
|
111
|
+
respond_to :html
|
112
|
+
|
113
|
+
def show
|
114
|
+
present Band::Update
|
115
|
+
|
116
|
+
render text: "active_record_bands/show.html: #{@model.class}, #{@band.class}, #{@form.is_a?(Reform::Form)}, #{@operation.class}"
|
117
|
+
end
|
118
|
+
end
|
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: 0.2.
|
4
|
+
version: 0.2.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-01-
|
11
|
+
date: 2015-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -181,6 +181,7 @@ files:
|
|
181
181
|
- lib/trailblazer/autoloading.rb
|
182
182
|
- lib/trailblazer/operation.rb
|
183
183
|
- lib/trailblazer/operation/controller.rb
|
184
|
+
- lib/trailblazer/operation/controller/active_record.rb
|
184
185
|
- lib/trailblazer/operation/crud.rb
|
185
186
|
- lib/trailblazer/operation/representer.rb
|
186
187
|
- lib/trailblazer/operation/responder.rb
|