trailblazer-compat 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +7 -0
  5. data/README.md +76 -0
  6. data/Rakefile +10 -0
  7. data/lib/trailblazer/1.1/autoloading.rb +15 -0
  8. data/lib/trailblazer/1.1/endpoint.rb +31 -0
  9. data/lib/trailblazer/1.1/operation.rb +175 -0
  10. data/lib/trailblazer/1.1/operation/builder.rb +26 -0
  11. data/lib/trailblazer/1.1/operation/callback.rb +53 -0
  12. data/lib/trailblazer/1.1/operation/collection.rb +6 -0
  13. data/lib/trailblazer/1.1/operation/controller.rb +87 -0
  14. data/lib/trailblazer/1.1/operation/controller/active_record.rb +21 -0
  15. data/lib/trailblazer/1.1/operation/dispatch.rb +3 -0
  16. data/lib/trailblazer/1.1/operation/model.rb +50 -0
  17. data/lib/trailblazer/1.1/operation/model/active_model.rb +11 -0
  18. data/lib/trailblazer/1.1/operation/model/dsl.rb +29 -0
  19. data/lib/trailblazer/1.1/operation/model/external.rb +34 -0
  20. data/lib/trailblazer/1.1/operation/module.rb +29 -0
  21. data/lib/trailblazer/1.1/operation/policy.rb +85 -0
  22. data/lib/trailblazer/1.1/operation/policy/guard.rb +35 -0
  23. data/lib/trailblazer/1.1/operation/representer.rb +98 -0
  24. data/lib/trailblazer/1.1/operation/resolver.rb +30 -0
  25. data/lib/trailblazer/1.1/operation/responder.rb +18 -0
  26. data/lib/trailblazer/1.1/operation/uploaded_file.rb +77 -0
  27. data/lib/trailblazer/1.1/operation/worker.rb +112 -0
  28. data/lib/trailblazer/1.1/rails.rb +21 -0
  29. data/lib/trailblazer/1.1/rails/autoloading.rb +3 -0
  30. data/lib/trailblazer/1.1/rails/railtie.rb +24 -0
  31. data/lib/trailblazer/1.1/rails/test/integration.rb +6 -0
  32. data/lib/trailblazer/compat.rb +50 -0
  33. data/lib/trailblazer/compat/version.rb +5 -0
  34. data/trailblazer-compat.gemspec +25 -0
  35. metadata +118 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f2156ecbbb4653bcc48e2fd0dca22528b7079e4f
4
+ data.tar.gz: 8558904f18e93de025483229552bd0ddc4f8134e
5
+ SHA512:
6
+ metadata.gz: e41a477d030046014dce30b2aecef41efa6c5fa6bafa879e3b9a5037ed12d88ecfb53112f0840650f35b358bc6b7db112acfc7b84a9600d664dbe866906d35f1
7
+ data.tar.gz: 10bf310b9094eb11a0d0a431690afe9b528e145489cfacf7927e1a6d822aaa25c1b3a78501435fcc7f9eda3e31963eb095b060719a03d59df431bd2ce110e635
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in trailblazer-compat.gemspec
4
+ gemspec
5
+
6
+ gem "trailblazer", path: "../trailblazer"
7
+ gem "trailblazer-operation", path: "../operation"
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Trailblazer::Compat
2
+
3
+ This gem provides a seamless-er™ upgrade from TRB 1.1 to 2.x.
4
+
5
+ It allows to run both old TRB 1.1 operations along with new or refactored 2.x code in the same application, making it easier to upgrade operation code `step`-wise (no pun intended!) or add new TRB2 operations, workflows, etc. without having to change the old code.
6
+
7
+ ## Installation
8
+
9
+ Your exisiting application's `Gemfile` should point to the new `trailblazer` gem.
10
+
11
+ ```ruby
12
+ gem "trailblazer", ">= 2.0.0"
13
+ gem "trailblazer-compat"
14
+ ```
15
+
16
+ In a Rails application, you also need to pull the 1.x line of the `trailblazer-rails` gem.
17
+
18
+ ```ruby
19
+ gem "trailblazer-rails", ">= 1.0.0"
20
+ ```
21
+
22
+ ## Upgrade Path / Overview
23
+
24
+ The complete migration path is [documented here](http://trailblazer.to/gems/trailblazer/upgrading-1-to-2.html).
25
+
26
+ 1. You can keep old TRB1 operations.
27
+
28
+ ```ruby
29
+ # /app/concepts/song/create.rb
30
+ class Song
31
+ class Create < Trailblazer::Operation
32
+ model Song, :create
33
+ policy Song::Policy, :admin?
34
+
35
+ contract do
36
+ property :id
37
+ # ...
38
+ end
39
+
40
+ def process(params)
41
+ validate(params[:song]) do |form|
42
+ form.save
43
+ end
44
+ end
45
+ end
46
+ end
47
+ ```
48
+ 2. At any point, you can introduce new TRB2 operations or update old classes by inheriting from `Trailblazer::Operation.version(2)`.
49
+
50
+ ```ruby
51
+ # /app/concepts/song/create.rb
52
+ class Song
53
+ class Create < Trailblazer::Operation.version(2)
54
+ class Form < Reform::Form
55
+ property :id
56
+ # ...
57
+ end
58
+
59
+ class New < Trailblazer::Operation.version(2)
60
+ step Model( Song, :new )
61
+ step Policy::Pundit( Song::Policy, :admin? )
62
+ step Contract::Build( constant: Form )
63
+ end
64
+
65
+ step Nested(New)
66
+ step Contract::Validate( key: :admin )
67
+ step Contract::Persist()
68
+ end
69
+ end
70
+ ```
71
+
72
+ 3. Should you ever be finished updating your application, simply remove the `trailblazer-compat` gem from the `Gemfile`. You can then safely delete `.version(2)` across all files.
73
+
74
+ ## Development Status
75
+
76
+ The `compat` gem tries to make the transition to newer versions as painless as possible. However, if you run into any problems specific to your application, please [don't hesitate to contact us](https://gitter.im/trailblazer/chat). Pull requests (even ugly hacks) are appreciated in this gem, and this gem only.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,15 @@
1
+ Trailblazer::V1_1.class_eval do
2
+ autoload :NotAuthorizedError, "trailblazer/1.1/operation/policy"
3
+ end
4
+
5
+ Trailblazer::V1_1::Operation.class_eval do
6
+ autoload :Controller, "trailblazer/1.1/operation/controller"
7
+ autoload :Model, "trailblazer/1.1/operation/model"
8
+ autoload :Collection, "trailblazer/1.1/operation/collection"
9
+ autoload :Dispatch, "trailblazer/1.1/operation/dispatch" # TODO: remove in 1.2.
10
+ autoload :Callback, "trailblazer/1.1/operation/callback"
11
+ autoload :Module, "trailblazer/1.1/operation/module"
12
+ autoload :Representer,"trailblazer/1.1/operation/representer"
13
+ autoload :Policy, "trailblazer/1.1/operation/policy"
14
+ autoload :Resolver, "trailblazer/1.1/operation/resolver"
15
+ end
@@ -0,0 +1,31 @@
1
+ module Trailblazer::V1_1
2
+ # Encapsulates HTTP-specific logic needed before running an operation.
3
+ # Right now, all this does is #document_body! which figures out whether or not to pass the request body
4
+ # into params, so the operation can use a representer to deserialize the original document.
5
+ # To be used in Hanami, Roda, Rails, etc.
6
+ class Endpoint
7
+ def initialize(operation_class, params, request, options)
8
+ @operation_class = operation_class
9
+ @params = params
10
+ @request = request
11
+ @is_document = options[:is_document]
12
+ end
13
+
14
+ def call
15
+ document_body! if @is_document
16
+ yield @params# Create.run(params)
17
+ end
18
+
19
+ private
20
+ attr_reader :params, :operation_class, :request
21
+
22
+ def document_body!
23
+ # this is what happens:
24
+ # respond_with Comment::Update::JSON.run(params.merge(comment: request.body.string))
25
+ concept_name = operation_class.model_class.to_s.underscore # this could be renamed to ::concept_class soon.
26
+ request_body = request.body.respond_to?(:string) ? request.body.string : request.body.read
27
+
28
+ params.merge!(concept_name => request_body)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,175 @@
1
+ require "reform"
2
+
3
+ module Trailblazer::V1_1
4
+ class Operation
5
+ require "trailblazer/1.1/operation/builder"
6
+ extend Builder # imports ::builder_class and ::build_operation.
7
+
8
+ extend Uber::InheritableAttr
9
+ inheritable_attr :contract_class
10
+ self.contract_class = Reform::Form.clone
11
+
12
+ class << self
13
+ def run(params, &block) # Endpoint behaviour
14
+ res, op = build_operation(params).run
15
+
16
+ if block_given?
17
+ yield op if res
18
+ return op
19
+ end
20
+
21
+ [res, op]
22
+ end
23
+
24
+ # Like ::run, but yield block when invalid.
25
+ def reject(*args)
26
+ res, op = run(*args)
27
+ yield op if res == false
28
+ op
29
+ end
30
+
31
+ # ::call only returns the Operation instance (or whatever was returned from #validate).
32
+ # This is useful in tests or in irb, e.g. when using Op as a factory and you already know it's valid.
33
+ def call(params)
34
+ build_operation(params, raise_on_invalid: true).run.last
35
+ end
36
+
37
+ def [](*args) # TODO: remove in 1.2.
38
+ warn "[Trailblazer] Operation[] is deprecated. Please use Operation.() and have a nice day."
39
+ call(*args)
40
+ end
41
+
42
+ # Runs #setup! but doesn't process the operation.
43
+ def present(params)
44
+ build_operation(params)
45
+ end
46
+
47
+ # This is a DSL method. Use ::contract_class and ::contract_class= for the explicit version.
48
+ # Op.contract #=> returns contract class
49
+ # Op.contract do .. end # defines contract
50
+ # Op.contract CommentForm # copies (and subclasses) external contract.
51
+ # Op.contract CommentForm do .. end # copies and extends contract.
52
+ def contract(constant=nil, &block)
53
+ return contract_class unless constant or block_given?
54
+
55
+ self.contract_class= Class.new(constant) if constant
56
+ contract_class.class_eval(&block) if block_given?
57
+ end
58
+ end
59
+
60
+
61
+ def initialize(params, options={})
62
+ @options = options
63
+ @valid = true
64
+
65
+ setup!(params) # assign/find the model
66
+ end
67
+
68
+ # Operation.run(body: "Fabulous!") #=> [true, <Comment body: "Fabulous!">]
69
+ def run
70
+ process(@params)
71
+
72
+ [valid?, self]
73
+ end
74
+
75
+ attr_reader :model
76
+
77
+ def errors
78
+ contract.errors
79
+ end
80
+
81
+ def valid?
82
+ @valid
83
+ end
84
+
85
+ def contract(*args)
86
+ contract!(*args)
87
+ end
88
+
89
+ private
90
+ module Setup
91
+ attr_writer :model
92
+
93
+ def setup!(params)
94
+ params = assign_params!(params)
95
+ setup_params!(params)
96
+ build_model!(params)
97
+ params # TODO: test me.
98
+ end
99
+
100
+ def assign_params!(params)
101
+ @params = params!(params)
102
+ end
103
+
104
+ # Overwrite #params! if you need to change its structure, by returning a new params object
105
+ # from this method.
106
+ # This is helpful if you don't want to change the original via #setup_params!.
107
+ def params!(params)
108
+ params
109
+ end
110
+
111
+ def setup_params!(params)
112
+ end
113
+
114
+ def build_model!(*args)
115
+ assign_model!(*args) # @model = ..
116
+ setup_model!(*args)
117
+ end
118
+
119
+ def assign_model!(*args)
120
+ @model = model!(*args)
121
+ end
122
+
123
+ # Implement #model! to find/create your operation model (if required).
124
+ def model!(params)
125
+ end
126
+
127
+ # Override to add attributes that can be infered from params.
128
+ def setup_model!(params)
129
+ end
130
+ end
131
+ include Setup
132
+
133
+ def validate(params, model=nil, contract_class=nil)
134
+ contract!(model, contract_class)
135
+
136
+ if @valid = validate_contract(params)
137
+ yield contract if block_given?
138
+ else
139
+ raise!(contract)
140
+ end
141
+
142
+ @valid
143
+ end
144
+
145
+ def validate_contract(params)
146
+ contract.validate(params)
147
+ end
148
+
149
+ def invalid!(result=self)
150
+ @valid = false
151
+ result
152
+ end
153
+
154
+ # When using Op.(), an invalid contract will raise an exception.
155
+ def raise!(contract)
156
+ raise InvalidContract.new(contract.errors.to_s) if @options[:raise_on_invalid]
157
+ end
158
+
159
+ # Instantiate the contract, either by using the user's contract passed into #validate
160
+ # or infer the Operation contract.
161
+ def contract_for(model=nil, contract_class=nil)
162
+ model ||= self.model
163
+ contract_class ||= self.class.contract_class
164
+
165
+ contract_class.new(model)
166
+ end
167
+
168
+ def contract!(*args)
169
+ @contract ||= contract_for(*args)
170
+ end
171
+
172
+ class InvalidContract < RuntimeError
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,26 @@
1
+ require "uber/builder"
2
+
3
+ # Allows to add builders via ::builds.
4
+ module Trailblazer::V1_1::Operation::Builder
5
+ def self.extended(extender)
6
+ extender.send(:include, Uber::Builder)
7
+ end
8
+
9
+ def builder_class
10
+ @builders
11
+ end
12
+
13
+ def builder_class=(constant)
14
+ @builders = constant
15
+ end
16
+
17
+ private
18
+ # Runs the builders for this operation class to figure out the actual class.
19
+ def build_operation_class(*args)
20
+ class_builder(self).(*args) # Uber::Builder::class_builder(context)
21
+ end
22
+
23
+ def build_operation(params, options={})
24
+ build_operation_class(params).new(params, options)
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ require "declarative"
2
+ require "disposable/callback"
3
+
4
+ module Trailblazer::V1_1::Operation::Callback
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.extend Declarative::Heritage::Inherited
9
+ base.extend Declarative::Heritage::DSL
10
+ end
11
+
12
+ def callback!(name=:default, options={ operation: self, contract: contract, params: @params }) # FIXME: test options.
13
+ config = self.class.callbacks.fetch(name) # TODO: test exception
14
+ group = config[:group].new(contract)
15
+
16
+ options[:context] ||= (config[:context] == :operation ? self : group)
17
+ group.(options)
18
+
19
+ invocations[name] = group
20
+ end
21
+
22
+ def dispatch!(*args, &block)
23
+ callback!(*args, &block)
24
+ end
25
+
26
+ def invocations
27
+ @invocations ||= {}
28
+ end
29
+
30
+ module ClassMethods
31
+ def callbacks
32
+ @callbacks ||= {}
33
+ end
34
+
35
+ def callback(name=:default, constant=nil, &block)
36
+ return callbacks[name][:group] unless constant or block_given?
37
+
38
+ add_callback(name, constant, &block)
39
+ end
40
+
41
+ private
42
+ def add_callback(name, constant, &block)
43
+ heritage.record(:add_callback, name, constant, &block)
44
+
45
+ callbacks[name] ||= {
46
+ group: Class.new(constant || Disposable::Callback::Group),
47
+ context: constant ? nil : :operation # `context: :operation` when the callback is inline. `context: group` otherwise.
48
+ }
49
+
50
+ callbacks[name][:group].class_eval(&block) if block_given?
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,6 @@
1
+ module Trailblazer::V1_1::Operation::Collection
2
+ # Collection does not produce a contract.
3
+ def present
4
+ self
5
+ end
6
+ end
@@ -0,0 +1,87 @@
1
+ require "trailblazer/1.1/endpoint"
2
+
3
+ module Trailblazer::V1_1::Operation::Controller
4
+ private
5
+ def form(operation_class, options={})
6
+ run_v2_for(operation_class, options) and return
7
+
8
+ res, op, options = operation!(operation_class, options)
9
+ op.contract.prepopulate!(options) # equals to @form.prepopulate!
10
+
11
+ op.contract
12
+ end
13
+
14
+ # Provides the operation instance, model and contract without running #process.
15
+ # Returns the operation.
16
+ def present(operation_class, options={})
17
+ run_v2_for(operation_class, options) and return
18
+
19
+ res, op = operation!(operation_class, options.merge(skip_form: true))
20
+ op
21
+ end
22
+
23
+ def collection(*args)
24
+ res, op = operation!(*args)
25
+ @collection = op.model
26
+ op
27
+ end
28
+
29
+ def run(operation_class, options={}, &block)
30
+ run_v2_for(operation_class, options, &block) and return
31
+
32
+ res, op = operation_for!(operation_class, options) { |params| operation_class.run(params) }
33
+
34
+ yield op if res and block_given?
35
+
36
+ op
37
+ end
38
+
39
+ # The block passed to #respond is always run, regardless of the validity result.
40
+ def respond(operation_class, options={}, &block)
41
+ res, op = operation_for!(operation_class, options) { |params| operation_class.run(params) }
42
+ namespace = options.delete(:namespace) || []
43
+
44
+ return respond_with *namespace, op, options if not block_given?
45
+ respond_with *namespace, op, options, &Proc.new { |formats| block.call(op, formats) } if block_given?
46
+ end
47
+
48
+ private
49
+ def operation!(operation_class, options={}) # or #model or #setup.
50
+ operation_for!(operation_class, options) { |params| [true, operation_class.present(params)] }
51
+ end
52
+
53
+ def process_params!(params)
54
+ end
55
+
56
+ # Override and return arbitrary params object.
57
+ def params!(params)
58
+ params
59
+ end
60
+
61
+ # Normalizes parameters and invokes the operation (including its builders).
62
+ def operation_for!(operation_class, options, &block)
63
+ params = options[:params] || self.params # TODO: test params: parameter properly in all 4 methods.
64
+ params = params!(params)
65
+ process_params!(params) # deprecate or rename to #setup_params!
66
+
67
+ res, op = Trailblazer::V1_1::Endpoint.new(operation_class, params, request, options).(&block)
68
+ setup_operation_instance_variables!(op, options)
69
+
70
+ [res, op, options.merge(params: params)]
71
+ end
72
+
73
+ def setup_operation_instance_variables!(operation, options)
74
+ @operation = operation
75
+ @model = operation.model
76
+ @form = operation.contract unless options[:skip_form]
77
+ end
78
+
79
+ def run_v2_for(operation_class, *args, &block)
80
+ if operation_class < Trailblazer::V2::Operation
81
+ # run_v2(operation_class, *args)
82
+ run_v2(operation_class, &block)
83
+ return true
84
+ end
85
+ false
86
+ end
87
+ end