trailblazer 0.3.3 → 1.0.0.rc1

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.
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