trailblazer 0.3.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 06e8559ba04b20c80f460c00d2fe76e94a7dceae
4
- data.tar.gz: d16ed4ac3f269404152c316e2abcad2053c9f29c
3
+ metadata.gz: 6fe908e23b8ef76962d2eba061d666567e204120
4
+ data.tar.gz: 3df42356be844b922d258de9f3e64ed72f5b5a9c
5
5
  SHA512:
6
- metadata.gz: 2818fb4a0c7cfdc66b700df7feed6ac8a60dca747922ac901c311c92dffaf44dd0b3881ddfb46bf3eceebbc122d558bbd5c1acfc463a4ff2246824f519e475ee
7
- data.tar.gz: 39daeb303ccadc8dd261cfa44576bf046d94174568abecceed193de66c902cd3fc1a5ce27d99814644f26585b800d7f7568b094f5c5ff044eb8fbbaf29ea1fa6
6
+ metadata.gz: 9190472c21be262f4a216f52180c9b91bbf9c073650148fe53aec8b83ba2493f0365d4d10ebd603daa4a9f6c295411a634f66b302e7d5895c6fe8fdca66783b4
7
+ data.tar.gz: 119fbafbce0d79e95c9f4131400ba6860e12fe40f8e9e0901c4457da3450c0940915d9bb51e3ac0f502fd00cd6022224e4d082041dec843bf878537cdb3b6e71
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.3.2
2
+
3
+ * Allow to use `#contract` before `#validate`. The contract will be instantiated once per `#run` using `#contract` and then memoized. This allows to add/modify the contract _before_ you validate it using `#validate`.
4
+ * New signature for `Operation#contract_for(model, contract_class)`. It used to be contract, then model.
5
+
1
6
  # 0.3.1
2
7
 
3
8
  * Autoload `Dispatch`.
data/Gemfile CHANGED
@@ -8,4 +8,4 @@ gemspec
8
8
  # gem "disposable", path: "../disposable"
9
9
  gem "virtus"
10
10
  # gem "reform", github: "apotonick/reform"
11
- gem "reform", ">= 2.0.0.rc1"
11
+ gem "reform", "~> 2.0.0"
data/README.md CHANGED
@@ -420,6 +420,76 @@ Comment::Create.run(params) do |op|
420
420
  end
421
421
  ```
422
422
 
423
+ ### Inheritance
424
+
425
+ Operations fully support inheritance and will copy the contract, the callback groups and any methods from the original operation class.
426
+
427
+ ```ruby
428
+ class Create < Trailblazer::Operation
429
+ contract do
430
+ property :title
431
+ end
432
+
433
+ callback(:before_save) do
434
+ on_change :notify!
435
+ end
436
+
437
+ def notify!(*)
438
+ end
439
+ end
440
+ ```
441
+
442
+ This happens with normal Ruby inheritance.
443
+
444
+ ```ruby
445
+ class Update < Create
446
+ # inherited:
447
+ # contract
448
+ # callback(:before_save)
449
+ # def notify!(*)
450
+ end
451
+ ```
452
+
453
+ You can customize callback groups and contracts using the `:inherit` option, add and remove properties or add methods.
454
+
455
+ [Learn more](http://trailblazerb.org/gems/operation/inheritance.html)
456
+
457
+ ### Modules
458
+
459
+ In case inheritance is not enough for you, use modules to share common functionality.
460
+
461
+ ```ruby
462
+ module ExtendedCreate
463
+ include Trailblazer::Operation::Module
464
+
465
+ contract do
466
+ property :id
467
+ end
468
+
469
+ callback do
470
+ on_update :update!
471
+ end
472
+
473
+ def update!(song)
474
+ # do something
475
+ end
476
+ end
477
+ ```
478
+
479
+ Modules can be included and will simply run the declarations in the including class.
480
+
481
+ ```ruby
482
+ class Create::Admin < Create
483
+ include ExtendedCreate
484
+
485
+ # contract has :title and :id now.
486
+ ```
487
+
488
+ Modules are often used to modify an existing operation for admin or signed-in roles.
489
+
490
+ [Learn more](http://trailblazerb.org/gems/operation/inheritance.html)
491
+
492
+
423
493
  ## Form API
424
494
 
425
495
  Usually, an operation has a form object.
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']
9
+ test.test_files = FileList['test/*_test.rb', "test/operation/*_test.rb"]
10
10
  test.verbose = true
11
11
  end
12
12
 
@@ -4,4 +4,5 @@ Trailblazer::Operation.class_eval do
4
4
  autoload :CRUD, "trailblazer/operation/crud"
5
5
  autoload :Collection, "trailblazer/operation/collection"
6
6
  autoload :Dispatch, "trailblazer/operation/dispatch"
7
+ autoload :Module, "trailblazer/operation/module"
7
8
  end
@@ -1,10 +1,6 @@
1
- require 'uber/builder'
2
- # TODO: extract all reform-contract stuff into optional module.
3
- require 'reform'
1
+ require "uber/builder"
2
+ require "reform"
4
3
 
5
- # TODO: OP[] without wrapper, OP.run with (eg for params)
6
-
7
- # to be documented: only implement #setup! when not using CRUD and when ::present needed (make example for simple Op without #setup!)
8
4
 
9
5
  module Trailblazer
10
6
  class Operation
@@ -70,12 +66,10 @@ module Trailblazer
70
66
 
71
67
  def present(*params)
72
68
  setup!(*params)
73
-
74
- @contract = contract_for(nil, @model)
69
+ contract!
75
70
  self
76
71
  end
77
72
 
78
- attr_reader :contract
79
73
  attr_reader :model
80
74
 
81
75
  def errors
@@ -86,6 +80,10 @@ module Trailblazer
86
80
  @valid
87
81
  end
88
82
 
83
+ def contract(*args)
84
+ contract!(*args)
85
+ end
86
+
89
87
  private
90
88
  def setup!(*params)
91
89
  setup_params!(*params)
@@ -105,8 +103,8 @@ module Trailblazer
105
103
  def setup_params!(*params)
106
104
  end
107
105
 
108
- def validate(params, model, contract_class=nil)
109
- @contract = contract_for(contract_class, model)
106
+ def validate(params, model=nil, contract_class=nil)
107
+ contract!(model, contract_class)
110
108
 
111
109
  if @valid = validate_contract(params)
112
110
  yield contract if block_given?
@@ -133,8 +131,15 @@ module Trailblazer
133
131
 
134
132
  # Instantiate the contract, either by using the user's contract passed into #validate
135
133
  # or infer the Operation contract.
136
- def contract_for(contract_class, *model)
137
- (contract_class || self.class.contract_class).new(*model)
134
+ def contract_for(model=nil, contract_class=nil)
135
+ model ||= self.model
136
+ contract_class ||= self.class.contract_class
137
+
138
+ contract_class.new(model)
139
+ end
140
+
141
+ def contract!(*args)
142
+ @contract ||= contract_for(*args)
138
143
  end
139
144
 
140
145
  class InvalidContract < RuntimeError
@@ -143,9 +148,3 @@ module Trailblazer
143
148
  end
144
149
 
145
150
  require 'trailblazer/operation/crud'
146
-
147
- # run
148
- # setup
149
- # process
150
- # contract
151
- # validate
@@ -12,9 +12,7 @@ private
12
12
  yield @operation if block_given?
13
13
  end
14
14
 
15
- # Doesn't run #validate.
16
- # TODO: allow only_setup.
17
- # TODO: dependency to CRUD (::model_name)
15
+ # Doesn't run #process.
18
16
  def present(operation_class, params=self.params)
19
17
  res, op = operation!(operation_class, params) { [true, operation_class.present(params)] }
20
18
 
@@ -0,0 +1,29 @@
1
+ module Trailblazer::Operation::Module
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ base.extend Included
5
+ end
6
+
7
+ module Included # TODO: use representable's inheritance mechanism.
8
+ def included(base)
9
+ super
10
+ instructions.each { |cfg|
11
+ method = cfg[0]
12
+ args = cfg[1].dup
13
+ block = cfg[2]
14
+ # options = args.extract_options!.dup # we need to duplicate options has as AM::Validations messes it up later.
15
+
16
+ base.send(method, *args, &block) } # property :name, {} do .. end
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ def method_missing(method, *args, &block)
22
+ instructions << [method, args, block]
23
+ end
24
+
25
+ def instructions
26
+ @instructions ||= []
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,3 @@
1
1
  module Trailblazer
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
@@ -0,0 +1,93 @@
1
+ require 'test_helper'
2
+ require "trailblazer/operation/module"
3
+ require "trailblazer/operation/dispatch"
4
+
5
+ class OperationModuleTest < MiniTest::Spec
6
+ Song = Struct.new(:name, :artist)
7
+ Artist = Struct.new(:id, :full_name)
8
+
9
+ class Create < Trailblazer::Operation
10
+ include Trailblazer::Operation::Dispatch
11
+
12
+ contract do
13
+ property :name
14
+ property :artist, populate_if_empty: Artist do
15
+ property :id
16
+ end
17
+ end
18
+
19
+ callback do
20
+ on_change :notify_me!
21
+ end
22
+
23
+ def process(params)
24
+ @model = Song.new
25
+
26
+ validate(params, @model) do
27
+ contract.sync
28
+
29
+ dispatch!
30
+ end
31
+ end
32
+
33
+ def dispatched
34
+ @dispatched ||= []
35
+ end
36
+
37
+ private
38
+ def notify_me!(*)
39
+ dispatched << :notify_me!
40
+ end
41
+ end
42
+
43
+
44
+ module SignedIn
45
+ include Trailblazer::Operation::Module
46
+
47
+ contract do
48
+ property :artist, inherit: true do
49
+ property :full_name
50
+ end
51
+ end
52
+
53
+ callback do
54
+ on_change :notify_you!
55
+ end
56
+
57
+ def notify_you!(*)
58
+ dispatched << :notify_you!
59
+ end
60
+ end
61
+
62
+
63
+ class Update < Create
64
+ callback do
65
+ on_change :notify_them!
66
+ end
67
+
68
+ include SignedIn
69
+
70
+ def notify_them!(*)
71
+ dispatched << :notify_them!
72
+ end
73
+ end
74
+
75
+
76
+ it do
77
+ op = Create.({name: "Feelings", artist: {id: 1, full_name: "The Offspring"}})
78
+
79
+ op.dispatched.must_equal [:notify_me!]
80
+ op.model.name.must_equal "Feelings"
81
+ op.model.artist.id.must_equal 1
82
+ op.model.artist.full_name.must_equal nil # property not declared.
83
+ end
84
+
85
+ it do
86
+ op = Update.({name: "Feelings", artist: {id: 1, full_name: "The Offspring"}})
87
+
88
+ op.dispatched.must_equal [:notify_me!, :notify_them!, :notify_you!]
89
+ op.model.name.must_equal "Feelings"
90
+ op.model.artist.id.must_equal 1
91
+ op.model.artist.full_name.must_equal "The Offspring" # property declared via Module.
92
+ end
93
+ end
@@ -0,0 +1,30 @@
1
+ require "test_helper"
2
+
3
+
4
+ class OperationContractTest < MiniTest::Spec
5
+
6
+ class Operation < Trailblazer::Operation
7
+ contract do
8
+ property :id
9
+ property :title
10
+ property :length
11
+ end
12
+
13
+ def process(params)
14
+ @model = Struct.new(:id, :title, :length).new
15
+
16
+ contract.id = 1
17
+ validate(params) do
18
+ contract.length = 3
19
+ end
20
+ end
21
+ end
22
+
23
+ # allow using #contract before #validate.
24
+ it do
25
+ op = Operation.(title: "Beethoven")
26
+ op.contract.id.must_equal 1
27
+ op.contract.title.must_equal "Beethoven"
28
+ op.contract.length.must_equal 3
29
+ end
30
+ end
@@ -57,12 +57,9 @@ class Band < ActiveRecord::Base
57
57
  end
58
58
  end
59
59
 
60
+ require "representable/json"
60
61
  class JSON < self
61
62
  include Representer
62
- require "reform/form/json"
63
- contract do
64
- include Reform::Form::JSON # this allows deserialising JSON.
65
- end
66
63
 
67
64
  representer do
68
65
  collection :songs, inherit: true, render_empty: false # tested in ControllerPresentTest.
@@ -77,17 +74,9 @@ class Band < ActiveRecord::Base
77
74
  end
78
75
  end
79
76
 
80
- # TODO: wait for uber 0.0.10 and @dutow.
81
- # builds -> (params)
82
- # return JSON if params[:format] == "json"
83
- # return Admin if params[:admin]
84
- # end
85
- builds do |params|
86
- if params[:format] == "json"
87
- JSON
88
- elsif params[:admin]
89
- Admin
90
- end
77
+ builds -> (params) do
78
+ return JSON if params[:format] == "json"
79
+ return Admin if params[:admin]
91
80
  end
92
81
  end
93
82
 
@@ -1,4 +1,9 @@
1
1
  require 'trailblazer'
2
2
  require 'minitest/autorun'
3
3
 
4
- require 'fake_app/rails_app.rb'
4
+ require "reform/form/active_model/validations"
5
+ Reform::Form.class_eval do
6
+ include Reform::Form::ActiveModel::Validations
7
+ end
8
+
9
+ require 'fake_app/rails_app.rb'
@@ -55,3 +55,8 @@ class MiniTest::Spec
55
55
  end
56
56
  end
57
57
 
58
+
59
+ require "reform/form/active_model/validations"
60
+ Reform::Form.class_eval do
61
+ include Reform::Form::ActiveModel::Validations
62
+ end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'trailblazer/version'
@@ -26,7 +25,6 @@ Gem::Specification.new do |spec|
26
25
  spec.add_development_dependency "bundler"
27
26
  spec.add_development_dependency "rake"
28
27
  spec.add_development_dependency "minitest"
29
- # spec.add_development_dependency "minitest-spec-rails" # TODO: can anyone make this work (test/rails).
30
28
  spec.add_development_dependency "sidekiq", "~> 3.1.0"
31
29
  spec.add_development_dependency "actionpack", '>= 3.0.0' # Rails is optional.
32
30
  spec.add_development_dependency "rails"
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.3.1
4
+ version: 0.3.2
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-07-03 00:00:00.000000000 Z
11
+ date: 2015-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uber
@@ -208,6 +208,7 @@ files:
208
208
  - lib/trailblazer/operation/controller/active_record.rb
209
209
  - lib/trailblazer/operation/crud.rb
210
210
  - lib/trailblazer/operation/dispatch.rb
211
+ - lib/trailblazer/operation/module.rb
211
212
  - lib/trailblazer/operation/representer.rb
212
213
  - lib/trailblazer/operation/responder.rb
213
214
  - lib/trailblazer/operation/uploaded_file.rb
@@ -219,6 +220,8 @@ files:
219
220
  - test/dispatch_test.rb
220
221
  - test/fixtures/apotomo.png
221
222
  - test/fixtures/cells.png
223
+ - test/module_test.rb
224
+ - test/operation/contract_test.rb
222
225
  - test/operation_test.rb
223
226
  - test/rails/controller_test.rb
224
227
  - test/rails/endpoint_test.rb
@@ -271,6 +274,8 @@ test_files:
271
274
  - test/dispatch_test.rb
272
275
  - test/fixtures/apotomo.png
273
276
  - test/fixtures/cells.png
277
+ - test/module_test.rb
278
+ - test/operation/contract_test.rb
274
279
  - test/operation_test.rb
275
280
  - test/rails/controller_test.rb
276
281
  - test/rails/endpoint_test.rb