trailblazer 0.2.0 → 0.2.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/.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
|