trailblazer-compat 0.1.0

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