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
@@ -0,0 +1,30 @@
1
+ require "trailblazer/operation/model/external"
2
+ require "trailblazer/operation/policy"
3
+
4
+ class Trailblazer::Operation
5
+ # Provides builds-> (model, policy, params).
6
+ module Resolver
7
+ def self.included(includer)
8
+ includer.class_eval do
9
+ include Policy # ::build_policy
10
+ include Model::External # ::build_operation_class
11
+
12
+ extend BuildOperation # ::build_operation
13
+ end
14
+ end
15
+
16
+ module BuildOperation
17
+ def build_operation(params, options={})
18
+ model = model!(params)
19
+ policy = policy_config.call(params[:current_user], model)
20
+ build_operation_class(model, policy, params).
21
+ new(params, options.merge(model: model, policy: policy))
22
+ end
23
+ end
24
+
25
+ def initialize(params, options)
26
+ @policy = options[:policy]
27
+ super
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,4 @@
1
1
  module Trailblazer::Operation::Responder
2
- # TODO: test me.
3
2
  def self.included(base)
4
3
  base.extend ClassMethods
5
4
  end
@@ -7,7 +7,7 @@ class Trailblazer::Operation
7
7
  # Works with Reform 2, only.
8
8
  module Worker
9
9
  def self.included(base)
10
- base.send(:include, Sidekiq::Worker) # TODO: this will work with any bg gem.
10
+ base.send(:include, Sidekiq::Worker)
11
11
  base.extend(ClassMethods)
12
12
  end
13
13
 
@@ -17,10 +17,28 @@ class Trailblazer::Operation
17
17
  return perform_async(serializable(params))
18
18
  end
19
19
 
20
- new.run(params)
20
+ super(params)
21
+ end
22
+
23
+ def new(*args)
24
+ return super if args.any?
25
+ # sidekiq behavior: (not a big fan of this)
26
+ self
27
+ end
28
+
29
+ def perform(params) # called by Sidekiq.
30
+ build_operation(params).perform
31
+ end
32
+
33
+ def jid=(jid)
34
+ puts "@@@@@ #{jid.inspect}"
21
35
  end
22
36
 
23
37
  private
38
+ def perform_async(*args)
39
+ client_push('class' => self, 'args' => args) # calls class.new.perform(params)
40
+ end
41
+
24
42
  def background? # TODO: make configurable.
25
43
  true
26
44
  # if Rails.env == "production" or Rails.env == "staging"
@@ -32,16 +50,15 @@ class Trailblazer::Operation
32
50
  end
33
51
 
34
52
 
35
- # called from Sidekiq.
36
- def perform(params)
53
+ def perform#(params)
37
54
  # the serialized params hash from Sidekiq contains a Op::UploadedFile hash.
38
55
 
39
56
  # the following code is basically what happens in a controller.
40
57
  # this is a bug in Rails, it doesn't work without requiring as/hash/ina
41
58
  # params = ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(params) # TODO: this might make it ultra-slow as Reform converts it back to strings.
42
- params = params.with_indifferent_access
43
-
44
- run(deserializable(params))
59
+ params = @params.with_indifferent_access
60
+ @params = deserializable(params)
61
+ run
45
62
  end
46
63
 
47
64
  private
@@ -1,3 +1,3 @@
1
1
  module Trailblazer
2
- VERSION = "0.3.3"
2
+ VERSION = "1.0.0.rc1"
3
3
  end
@@ -1,5 +1,6 @@
1
1
  require "test_helper"
2
2
  require "trailblazer/operation/collection"
3
+ require "trailblazer/operation/model"
3
4
 
4
5
  class CollectionTest < MiniTest::Spec
5
6
  Song = Struct.new(:title, :id) do
@@ -14,7 +15,7 @@ class CollectionTest < MiniTest::Spec
14
15
 
15
16
 
16
17
  class CreateOperation < Trailblazer::Operation
17
- include CRUD
18
+ include Model
18
19
  model Song
19
20
  action :create
20
21
 
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
  require 'trailblazer/operation'
3
3
 
4
- class CrudTest < MiniTest::Spec
4
+ class ModelTest < MiniTest::Spec
5
5
  Song = Struct.new(:title, :id) do
6
6
  class << self
7
7
  attr_accessor :find_result # TODO: eventually, replace with AR test.
@@ -14,7 +14,7 @@ class CrudTest < MiniTest::Spec
14
14
  end
15
15
 
16
16
  class CreateOperation < Trailblazer::Operation
17
- include CRUD
17
+ include Model
18
18
  model Song
19
19
  action :create
20
20
 
@@ -32,9 +32,9 @@ class CrudTest < MiniTest::Spec
32
32
 
33
33
 
34
34
  # creates model for you.
35
- it { CreateOperation[song: {title: "Blue Rondo a la Turk"}].model.title.must_equal "Blue Rondo a la Turk" }
35
+ it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
36
36
  # exposes #model.
37
- it { CreateOperation[song: {title: "Blue Rondo a la Turk"}].model.must_be_instance_of Song }
37
+ it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.must_be_instance_of Song }
38
38
 
39
39
  class ModifyingCreateOperation < CreateOperation
40
40
  def process(params)
@@ -47,8 +47,8 @@ class CrudTest < MiniTest::Spec
47
47
  end
48
48
 
49
49
  # lets you modify model.
50
- it { ModifyingCreateOperation[song: {title: "Blue Rondo a la Turk"}].model.title.must_equal "Blue Rondo a la Turk" }
51
- it { ModifyingCreateOperation[song: {title: "Blue Rondo a la Turk"}].model.genre.must_equal "Punkrock" }
50
+ it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
51
+ it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.genre.must_equal "Punkrock" }
52
52
 
53
53
  # Update
54
54
  class UpdateOperation < CreateOperation
@@ -57,10 +57,10 @@ class CrudTest < MiniTest::Spec
57
57
 
58
58
  # finds model and updates.
59
59
  it do
60
- song = CreateOperation[song: {title: "Anchor End"}].model
60
+ song = CreateOperation.(song: {title: "Anchor End"}).model
61
61
  Song.find_result = song
62
62
 
63
- UpdateOperation[id: song.id, song: {title: "The Rip"}].model.title.must_equal "The Rip"
63
+ UpdateOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
64
64
  song.title.must_equal "The Rip"
65
65
  end
66
66
 
@@ -71,16 +71,16 @@ class CrudTest < MiniTest::Spec
71
71
 
72
72
  # finds model and updates.
73
73
  it do
74
- song = CreateOperation[song: {title: "Anchor End"}].model
74
+ song = CreateOperation.(song: {title: "Anchor End"}).model
75
75
  Song.find_result = song
76
76
 
77
- FindOperation[id: song.id, song: {title: "The Rip"}].model.title.must_equal "The Rip"
77
+ FindOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
78
78
  song.title.must_equal "The Rip"
79
79
  end
80
80
 
81
81
 
82
82
  class DefaultCreateOperation < Trailblazer::Operation
83
- include CRUD
83
+ include Model
84
84
  model Song
85
85
 
86
86
  def process(params)
@@ -89,7 +89,7 @@ class CrudTest < MiniTest::Spec
89
89
  end
90
90
 
91
91
  # uses :create as default if not set via ::action.
92
- it { DefaultCreateOperation[{}].model.must_equal Song.new }
92
+ it { DefaultCreateOperation.({}).model.must_equal Song.new }
93
93
 
94
94
  # model Song, :action
95
95
  class ModelUpdateOperation < CreateOperation
@@ -99,7 +99,7 @@ class CrudTest < MiniTest::Spec
99
99
  # allows ::model, :action.
100
100
  it do
101
101
  Song.find_result = song = Song.new
102
- ModelUpdateOperation[{id: 1, song: {title: "Mercy Day For Mr. Vengeance"}}].model.must_equal song
102
+ ModelUpdateOperation.({id: 1, song: {title: "Mercy Day For Mr. Vengeance"}}).model.must_equal song
103
103
  end
104
104
 
105
105
 
@@ -111,13 +111,13 @@ class CrudTest < MiniTest::Spec
111
111
  end
112
112
  end
113
113
 
114
- it { SetupModelOperation[song: {title: "Emily Kane"}].model.params.must_equal "{:song=>{:title=>\"Emily Kane\"}}" }
114
+ it { SetupModelOperation.(song: {title: "Emily Kane"}).model.params.must_equal "{:song=>{:title=>\"Emily Kane\"}}" }
115
115
 
116
116
 
117
117
 
118
118
  # no call to ::model raises error.
119
119
  class NoModelOperation < Trailblazer::Operation
120
- include CRUD
120
+ include Model
121
121
 
122
122
  def process(params)
123
123
  self
@@ -125,29 +125,11 @@ class CrudTest < MiniTest::Spec
125
125
  end
126
126
 
127
127
  # uses :create as default if not set via ::action.
128
- it { assert_raises(RuntimeError){ NoModelOperation[{}] } }
129
-
130
-
131
-
132
- # contract infers model_name.
133
- # TODO: this a Rails/ActiveModel-specific test.
134
- class ContractKnowsModelNameOperation < Trailblazer::Operation
135
- include CRUD
136
- model Song
137
- include CRUD::ActiveModel
138
-
139
- contract do
140
- include Reform::Form::ActiveModel # this usually happens in Reform::Form::Rails.
141
- property :title
142
- end
143
- end
144
-
145
- it { ContractKnowsModelNameOperation.present(song: {title: "Direct Hit"}).contract.class.model_name.to_s.must_equal "CrudTest::Song" }
146
-
128
+ it { assert_raises(RuntimeError){ NoModelOperation.({}) } }
147
129
 
148
130
  # allow passing validate(params, model, contract_class)
149
131
  class OperationWithPrivateContract < Trailblazer::Operation
150
- include CRUD
132
+ include Model
151
133
  model Song
152
134
 
153
135
  class Contract < Reform::Form
@@ -0,0 +1,41 @@
1
+ require "test_helper"
2
+
3
+ class OperationBuilderTest < MiniTest::Spec
4
+ class ParentOperation < Trailblazer::Operation
5
+ def process(params)
6
+ end
7
+
8
+ class Sub < self
9
+ end
10
+
11
+ builds do |params|
12
+ Sub if params[:sub]
13
+ end
14
+ end
15
+
16
+ it { ParentOperation.run({}).last.class.must_equal ParentOperation }
17
+ it { ParentOperation.run({sub: true}).last.class.must_equal ParentOperation::Sub }
18
+ it { ParentOperation.({}).class.must_equal ParentOperation }
19
+ it { ParentOperation.({sub: true}).class.must_equal ParentOperation::Sub }
20
+ end
21
+
22
+ class OperationBuilderClassTest < MiniTest::Spec
23
+ class SuperOperation < Trailblazer::Operation
24
+ builds do |params|
25
+ self::Sub if params[:sub] # Sub is defined in ParentOperation.
26
+ end
27
+ end
28
+
29
+ class ParentOperation < Trailblazer::Operation
30
+ def process(params)
31
+ end
32
+
33
+ class Sub < self
34
+ end
35
+
36
+ self.builder_class = SuperOperation.builder_class
37
+ end
38
+
39
+ it { ParentOperation.({}).class.must_equal ParentOperation }
40
+ it { ParentOperation.({sub: true}).class.must_equal ParentOperation::Sub }
41
+ end
@@ -0,0 +1,108 @@
1
+ require "test_helper"
2
+ require "trailblazer/operation/dispatch"
3
+
4
+
5
+ class DslCallbackTest < MiniTest::Spec
6
+ module SongProcess
7
+ def process(params)
8
+ contract(OpenStruct.new).validate(params)
9
+ dispatch!
10
+ end
11
+
12
+ def _invocations
13
+ @_invocations ||= []
14
+ end
15
+
16
+ def self.included(includer)
17
+ includer.contract do
18
+ property :title
19
+ end
20
+ end
21
+ end
22
+
23
+ describe "inheritance across operations" do
24
+ class Operation < Trailblazer::Operation
25
+ include Dispatch
26
+ include SongProcess
27
+
28
+ callback do
29
+ on_change :default!
30
+ end
31
+
32
+ class Admin < self
33
+ callback do
34
+ on_change :admin_default!
35
+ end
36
+
37
+ callback(:after_save) { on_change :after_save! }
38
+
39
+ def admin_default!(*); _invocations << :admin_default!; end
40
+ def after_save!(*); _invocations << :after_save!; end
41
+
42
+ def process(*)
43
+ super
44
+ dispatch!(:after_save)
45
+ end
46
+ end
47
+
48
+ def default!(*); _invocations << :default!; end
49
+ end
50
+
51
+ it { Operation.({"title"=> "Love-less"})._invocations.must_equal([:default!]) }
52
+ it { Operation::Admin.({"title"=> "Love-less"})._invocations.must_equal([:default!, :admin_default!, :after_save!]) }
53
+ end
54
+
55
+ describe "Op.callback" do
56
+ it { Operation.callback(:default).must_equal Operation.callbacks[:default] }
57
+ end
58
+
59
+ describe "Op.callback :after_save, AfterSaveCallback" do
60
+ class AfterSaveCallback < Disposable::Callback::Group
61
+ on_change :after_save!
62
+ end
63
+
64
+ class OpWithExternalCallback < Trailblazer::Operation
65
+ include Dispatch
66
+ include SongProcess
67
+ callback :after_save, AfterSaveCallback
68
+
69
+ def process(params)
70
+ contract(OpenStruct.new).validate(params)
71
+ dispatch!(:after_save)
72
+ end
73
+
74
+ def after_save!(*); _invocations << :after_save!; end
75
+ end
76
+
77
+ it { OpWithExternalCallback.("title"=>"Thunder Rising")._invocations.must_equal([:after_save!]) }
78
+ end
79
+
80
+ describe "Op.callback :after_save, AfterSaveCallback do .. end" do
81
+ class DefaultCallback < Disposable::Callback::Group
82
+ on_change :default!
83
+ end
84
+
85
+ class OpUsingCallback < Trailblazer::Operation
86
+ include Dispatch
87
+ include SongProcess
88
+ callback :default, DefaultCallback
89
+ def default!(*); _invocations << :default!; end
90
+ end
91
+
92
+ class OpExtendingCallback < Trailblazer::Operation
93
+ include Dispatch
94
+ include SongProcess
95
+ callback :default, DefaultCallback do
96
+ on_change :after_save!
97
+ end
98
+
99
+ def default!(*); _invocations << :default!; end
100
+ def after_save!(*); _invocations << :after_save!; end
101
+ end
102
+
103
+ # this operation copies DefaultCallback and shouldn't run #after_save!.
104
+ it { OpUsingCallback.(title: "Thunder Rising")._invocations.must_equal([:default!]) }
105
+ # this operation copies DefaultCallback, extends it and runs #after_save!.
106
+ it { OpExtendingCallback.(title: "Thunder Rising")._invocations.must_equal([:default!, :after_save!]) }
107
+ end
108
+ end
@@ -0,0 +1,104 @@
1
+ require "test_helper"
2
+
3
+ # ::contract builds Reform::Form class
4
+ class DslContractTest < MiniTest::Spec
5
+ module SongProcess
6
+ def process(params)
7
+ validate(params, @model = OpenStruct.new)
8
+ end
9
+ end
10
+
11
+ describe "inheritance across operations" do
12
+ # inheritance
13
+ class Operation < Trailblazer::Operation
14
+ contract do
15
+ property :title
16
+ property :band
17
+ end
18
+
19
+ class JSON < self
20
+ contract do # inherit Contract
21
+ property :genre, validates: {presence: true}
22
+ property :band, virtual: true
23
+ end
24
+ end
25
+ end
26
+
27
+ # inherits subclassed Contract.
28
+ it { Operation.contract_class.wont_equal Operation::JSON.contract_class }
29
+
30
+ it do
31
+ form = Operation.contract_class.new(OpenStruct.new)
32
+ form.validate({})#.must_equal true
33
+ form.errors.to_s.must_equal "{}"
34
+
35
+ form = Operation::JSON.contract_class.new(OpenStruct.new)
36
+ form.validate({})#.must_equal true
37
+ form.errors.to_s.must_equal "{:genre=>[\"can't be blank\"]}"
38
+ end
39
+
40
+ # allows overriding options
41
+ it do
42
+ form = Operation::JSON.contract_class.new(song = OpenStruct.new)
43
+ form.validate({genre: "Punkrock", band: "Osker"}).must_equal true
44
+ form.sync
45
+
46
+ song.genre.must_equal "Punkrock"
47
+ song.band.must_equal nil
48
+ end
49
+ end
50
+
51
+ describe "Op.contract" do
52
+ it { Operation.contract.must_equal Operation.contract_class }
53
+ end
54
+
55
+ describe "Op.contract CommentForm" do
56
+ class SongForm < Reform::Form
57
+ property :songTitle, validates: {presence: true}
58
+ end
59
+
60
+ class OpWithExternalContract < Trailblazer::Operation
61
+ contract SongForm
62
+ include SongProcess
63
+ end
64
+
65
+ it { OpWithExternalContract.("songTitle"=> "Monsterparty").contract.songTitle.must_equal "Monsterparty" }
66
+ end
67
+
68
+ describe "Op.contract CommentForm do .. end" do
69
+ class DifferentSongForm < Reform::Form
70
+ property :songTitle, validates: {presence: true}
71
+ end
72
+
73
+ class OpNotExtendingContract < Trailblazer::Operation
74
+ contract DifferentSongForm
75
+ include SongProcess
76
+ end
77
+
78
+ class OpExtendingContract < Trailblazer::Operation
79
+ contract DifferentSongForm do
80
+ property :genre
81
+ end
82
+ include SongProcess
83
+ end
84
+
85
+ # this operation copies DifferentSongForm and shouldn't have `genre`.
86
+ it do
87
+ contract = OpNotExtendingContract.("songTitle"=>"Monsterparty", "genre"=>"Punk").contract
88
+ contract.songTitle.must_equal "Monsterparty"
89
+ assert_raises(NoMethodError) { contract.genre }
90
+ end
91
+
92
+ # this operation copies DifferentSongForm and extends it with the property `genre`.
93
+ it do
94
+ contract = OpExtendingContract.("songTitle"=>"Monsterparty", "genre"=>"Punk").contract
95
+ contract.songTitle.must_equal "Monsterparty"
96
+ contract.genre.must_equal "Punk"
97
+ end
98
+
99
+ # of course, the original contract wasn't modified, either.
100
+ it do
101
+ assert_raises(NoMethodError) { DifferentSongForm.new(OpenStruct.new).genre }
102
+ end
103
+ end
104
+ end