trailblazer 1.1.2 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -7
- data/CHANGES.md +108 -0
- data/COMM-LICENSE +91 -0
- data/Gemfile +18 -4
- data/LICENSE.txt +7 -20
- data/README.md +55 -15
- data/Rakefile +21 -2
- data/draft-1.2.rb +7 -0
- data/lib/trailblazer.rb +17 -4
- data/lib/trailblazer/dsl.rb +47 -0
- data/lib/trailblazer/operation/auto_inject.rb +47 -0
- data/lib/trailblazer/operation/builder.rb +18 -18
- data/lib/trailblazer/operation/callback.rb +31 -38
- data/lib/trailblazer/operation/contract.rb +46 -0
- data/lib/trailblazer/operation/controller.rb +45 -27
- data/lib/trailblazer/operation/guard.rb +24 -0
- data/lib/trailblazer/operation/model.rb +41 -33
- data/lib/trailblazer/operation/nested.rb +43 -0
- data/lib/trailblazer/operation/params.rb +13 -0
- data/lib/trailblazer/operation/persist.rb +13 -0
- data/lib/trailblazer/operation/policy.rb +26 -72
- data/lib/trailblazer/operation/present.rb +19 -0
- data/lib/trailblazer/operation/procedural/contract.rb +15 -0
- data/lib/trailblazer/operation/procedural/validate.rb +22 -0
- data/lib/trailblazer/operation/pundit.rb +42 -0
- data/lib/trailblazer/operation/representer.rb +25 -92
- data/lib/trailblazer/operation/rescue.rb +23 -0
- data/lib/trailblazer/operation/resolver.rb +18 -24
- data/lib/trailblazer/operation/validate.rb +50 -0
- data/lib/trailblazer/operation/wrap.rb +37 -0
- data/lib/trailblazer/version.rb +1 -1
- data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
- data/test/docs/auto_inject_test.rb +30 -0
- data/test/docs/contract_test.rb +429 -0
- data/test/docs/dry_test.rb +31 -0
- data/test/docs/guard_test.rb +143 -0
- data/test/docs/nested_test.rb +117 -0
- data/test/docs/policy_test.rb +2 -0
- data/test/docs/pundit_test.rb +109 -0
- data/test/docs/representer_test.rb +268 -0
- data/test/docs/rescue_test.rb +153 -0
- data/test/docs/wrap_test.rb +174 -0
- data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
- data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
- data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
- data/test/module_test.rb +22 -15
- data/test/operation/builder_test.rb +66 -18
- data/test/operation/callback_test.rb +70 -0
- data/test/operation/contract_test.rb +385 -15
- data/test/operation/dsl/callback_test.rb +18 -30
- data/test/operation/dsl/contract_test.rb +209 -19
- data/test/operation/dsl/representer_test.rb +42 -15
- data/test/operation/guard_test.rb +1 -147
- data/test/operation/model_test.rb +105 -0
- data/test/operation/params_test.rb +36 -0
- data/test/operation/persist_test.rb +44 -0
- data/test/operation/pipedream_test.rb +59 -0
- data/test/operation/pipetree_test.rb +104 -0
- data/test/operation/present_test.rb +24 -0
- data/test/operation/pundit_test.rb +104 -0
- data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
- data/test/operation/resolver_test.rb +34 -70
- data/test/operation_test.rb +57 -189
- data/test/test_helper.rb +23 -3
- data/trailblazer.gemspec +8 -7
- metadata +91 -59
- data/gemfiles/Gemfile.rails.lock +0 -130
- data/gemfiles/Gemfile.reform-2.0 +0 -6
- data/gemfiles/Gemfile.reform-2.1 +0 -7
- data/lib/trailblazer/autoloading.rb +0 -15
- data/lib/trailblazer/endpoint.rb +0 -31
- data/lib/trailblazer/operation.rb +0 -175
- data/lib/trailblazer/operation/collection.rb +0 -6
- data/lib/trailblazer/operation/dispatch.rb +0 -3
- data/lib/trailblazer/operation/model/dsl.rb +0 -29
- data/lib/trailblazer/operation/model/external.rb +0 -34
- data/lib/trailblazer/operation/policy/guard.rb +0 -35
- data/lib/trailblazer/operation/uploaded_file.rb +0 -77
- data/test/callback_test.rb +0 -104
- data/test/collection_test.rb +0 -57
- data/test/model_test.rb +0 -148
- data/test/operation/external_model_test.rb +0 -71
- data/test/operation/policy_test.rb +0 -97
- data/test/operation/reject_test.rb +0 -34
- data/test/rollback_test.rb +0 -47
data/Rakefile
CHANGED
@@ -5,6 +5,25 @@ task :default => [:test]
|
|
5
5
|
|
6
6
|
Rake::TestTask.new(:test) do |test|
|
7
7
|
test.libs << 'test'
|
8
|
-
test.test_files = FileList['test/**/*_test.rb']
|
8
|
+
# test.test_files = FileList['test/**/*_test.rb']
|
9
|
+
test_files = FileList[%w{
|
10
|
+
test/operation/guard_test.rb
|
11
|
+
test/operation/pundit_test.rb
|
12
|
+
test/operation/builder_test.rb
|
13
|
+
test/operation/model_test.rb
|
14
|
+
test/operation/contract_test.rb
|
15
|
+
test/operation/persist_test.rb
|
16
|
+
test/operation/callback_test.rb
|
17
|
+
test/operation/resolver_test.rb
|
18
|
+
test/operation/dsl/contract_test.rb
|
19
|
+
|
20
|
+
test/docs/*_test.rb
|
21
|
+
}]
|
22
|
+
|
23
|
+
if RUBY_VERSION == "1.9.3"
|
24
|
+
test_files = test_files - %w{test/docs/dry_test.rb test/docs/auto_inject_test.rb}
|
25
|
+
end
|
26
|
+
|
27
|
+
test.test_files = test_files
|
9
28
|
test.verbose = true
|
10
|
-
end
|
29
|
+
end
|
data/draft-1.2.rb
ADDED
data/lib/trailblazer.rb
CHANGED
@@ -1,7 +1,20 @@
|
|
1
1
|
require "trailblazer/operation"
|
2
|
+
require "trailblazer/operation/pipetree"
|
3
|
+
|
4
|
+
require "trailblazer/dsl"
|
2
5
|
require "trailblazer/version"
|
3
|
-
require "uber/inheritable_attr"
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
require "trailblazer/operation/builder"
|
8
|
+
require "trailblazer/operation/model"
|
9
|
+
require "trailblazer/operation/contract"
|
10
|
+
require "trailblazer/operation/validate"
|
11
|
+
require "trailblazer/operation/representer"
|
12
|
+
require "trailblazer/operation/present"
|
13
|
+
require "trailblazer/operation/policy"
|
14
|
+
require "trailblazer/operation/pundit"
|
15
|
+
require "trailblazer/operation/guard"
|
16
|
+
require "trailblazer/operation/persist"
|
17
|
+
# require "trailblazer/operation/callback"
|
18
|
+
require "trailblazer/operation/nested"
|
19
|
+
require "trailblazer/operation/wrap"
|
20
|
+
require "trailblazer/operation/rescue"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module DSL
|
3
|
+
# Boring DSL code that allows to set a skill class, or define it ad-hoc using a block.
|
4
|
+
# passing a constant always wipes out the existing class.
|
5
|
+
#
|
6
|
+
# Used in Contract, Representer, Callback, ..
|
7
|
+
class Build
|
8
|
+
# options[:prefix]
|
9
|
+
# options[:class]
|
10
|
+
# options[:container]
|
11
|
+
|
12
|
+
# Currently, adds .class only to classes. this could break builder instances?
|
13
|
+
|
14
|
+
def call(options, name=nil, constant=nil, dsl_block, &block)
|
15
|
+
# contract MyForm
|
16
|
+
if name.is_a?(Class)
|
17
|
+
constant = name
|
18
|
+
name = :default
|
19
|
+
end
|
20
|
+
|
21
|
+
is_instance = !(constant.kind_of?(Class) || dsl_block) # i don't like this magic too much, but since it's the only DSL method in TRB, it should be ok. # DISCUSS: options[:is_instance]
|
22
|
+
|
23
|
+
path = path_name(options[:prefix], name, is_instance ? nil : "class") # "contract.default.class"
|
24
|
+
|
25
|
+
if is_instance
|
26
|
+
skill = constant
|
27
|
+
else
|
28
|
+
extended = options[:container][path] # Operation["contract.default.class"]
|
29
|
+
extended = yield extended if extended && block_given?
|
30
|
+
|
31
|
+
# only extend an existing skill class when NO constant was passed.
|
32
|
+
constant = (extended || options[:class]) if constant.nil?# && block_given?
|
33
|
+
|
34
|
+
skill = Class.new(constant)
|
35
|
+
skill.class_eval(&dsl_block) if dsl_block
|
36
|
+
end
|
37
|
+
|
38
|
+
[path, skill]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def path_name(prefix, name, suffix)
|
43
|
+
[prefix, name, suffix].compact.join(".") # "contract.class" for default, otherwise "contract.params.class" etc.
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "dry/auto_inject"
|
2
|
+
|
3
|
+
class Trailblazer::Operation
|
4
|
+
# Thanks, @timriley! <3
|
5
|
+
# https://gist.github.com/timriley/d314a58da9784912159006e208ba8ea9
|
6
|
+
module AutoInject
|
7
|
+
class InjectStrategy < Module
|
8
|
+
ClassMethods = Class.new(Module)
|
9
|
+
|
10
|
+
attr_reader :container
|
11
|
+
attr_reader :dependency_map
|
12
|
+
attr_reader :class_mod
|
13
|
+
|
14
|
+
def initialize(container, *dependency_names)
|
15
|
+
@container = container
|
16
|
+
@dependency_map = Dry::AutoInject::DependencyMap.new(*dependency_names)
|
17
|
+
@class_mod = ClassMethods.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def included(klass)
|
21
|
+
define_call
|
22
|
+
|
23
|
+
klass.singleton_class.prepend @class_mod
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def define_call
|
31
|
+
class_mod.class_exec(container, dependency_map) do |container, dependency_map|
|
32
|
+
define_method :call do |params={}, options={}, *dependencies|
|
33
|
+
options_with_deps = dependency_map.to_h.each_with_object({}) { |(name, identifier), obj|
|
34
|
+
obj[name] = options[name] || container[identifier]
|
35
|
+
}.merge(options)
|
36
|
+
|
37
|
+
super(params, options_with_deps, *dependencies)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.AutoInject(container)
|
45
|
+
Dry::AutoInject(container, strategies: {default: AutoInject::InjectStrategy})
|
46
|
+
end
|
47
|
+
end
|
@@ -1,26 +1,26 @@
|
|
1
1
|
require "uber/builder"
|
2
2
|
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
# http://trailblazer.to/gems/operation/2.0/builder.html
|
4
|
+
class Trailblazer::Operation
|
5
|
+
module Builder
|
6
|
+
def self.import!(operation, import, user_builder)
|
7
|
+
import.(:>>, user_builder,
|
8
|
+
name: "builder.call",
|
9
|
+
before: "operation.new")
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
end
|
11
|
+
false # suppress inheritance. dislike. FIXME at some point.
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
# Include this when you want the ::builds DSL.
|
15
|
+
def self.included(includer)
|
16
|
+
includer.extend DSL # ::builds, ::builders
|
17
|
+
includer.| includer.Builder( includer.builders ) # pass class Builders object to our ::import!.
|
18
|
+
end
|
16
19
|
|
17
|
-
|
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)
|
20
|
+
DSL = Uber::Builder::DSL
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
|
23
|
+
def self.Builder(*args, &block)
|
24
|
+
[ Builder, args, block ]
|
25
25
|
end
|
26
|
-
end
|
26
|
+
end
|
@@ -1,53 +1,46 @@
|
|
1
1
|
require "declarative"
|
2
2
|
require "disposable/callback"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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)
|
4
|
+
# Needs #[], #[]= skill dependency.
|
5
|
+
class Trailblazer::Operation
|
6
|
+
module Callback
|
7
|
+
def self.import!(operation, import, group)
|
8
|
+
import.(:&, ->(input, options) { input.callback!(group) },
|
9
|
+
name: "callback.#{group}")
|
10
|
+
|
11
|
+
operation.send :include, self
|
12
|
+
end
|
18
13
|
|
19
|
-
|
20
|
-
|
14
|
+
def callback!(name=:default, options=self) # FIXME: test options.
|
15
|
+
config = self["callback.#{name}.class"] || raise #.fetch(name) # TODO: test exception
|
16
|
+
group = config[:group].new(self["contract.default"])
|
21
17
|
|
22
|
-
|
23
|
-
|
24
|
-
end
|
18
|
+
options[:context] ||= (config[:context] == :operation ? self : group)
|
19
|
+
group.(options)
|
25
20
|
|
26
|
-
|
27
|
-
@invocations ||= {}
|
28
|
-
end
|
29
|
-
|
30
|
-
module ClassMethods
|
31
|
-
def callbacks
|
32
|
-
@callbacks ||= {}
|
21
|
+
invocations[name] = group
|
33
22
|
end
|
34
23
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
add_callback(name, constant, &block)
|
24
|
+
def invocations
|
25
|
+
@invocations ||= {}
|
39
26
|
end
|
40
27
|
|
41
|
-
|
42
|
-
|
43
|
-
|
28
|
+
module DSL
|
29
|
+
def callback(name=:default, constant=nil, &block)
|
30
|
+
heritage.record(:callback, name, constant, &block)
|
44
31
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
32
|
+
# FIXME: make this nicer. we want to extend same-named callback groups.
|
33
|
+
# TODO: allow the same with contract, or better, test it!
|
34
|
+
extended = self["callback.#{name}.class"] && self["callback.#{name}.class"]
|
49
35
|
|
50
|
-
|
36
|
+
path, group_class = Trailblazer::DSL::Build.new.({ prefix: :callback, class: Disposable::Callback::Group, container: self }, name, constant, block) { |extended| extended[:group] }
|
37
|
+
|
38
|
+
self[path] = { group: group_class, context: constant ? nil : :operation }
|
39
|
+
end
|
51
40
|
end
|
52
41
|
end
|
42
|
+
|
43
|
+
def self.Callback(*args, &block)
|
44
|
+
[ Callback, args, block ]
|
45
|
+
end
|
53
46
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Best practices for using contract.
|
2
|
+
#
|
3
|
+
# * inject contract instance via constructor to #contract
|
4
|
+
# * allow contract setup and memo via #contract(model, options)
|
5
|
+
# * allow implicit automatic setup via #contract and class.contract_class
|
6
|
+
#
|
7
|
+
# Needs Operation#model.
|
8
|
+
# Needs #[], #[]= skill dependency.
|
9
|
+
module Trailblazer::Operation::Contract
|
10
|
+
module Build
|
11
|
+
# bla build contract at runtime.
|
12
|
+
def self.build_contract!(operation, options, name:"default", constant:nil, builder: nil)
|
13
|
+
# TODO: we could probably clean this up a bit at some point.
|
14
|
+
contract_class = constant || options["contract.#{name}.class"]
|
15
|
+
model = operation["model"] # FIXME: model.default
|
16
|
+
|
17
|
+
return operation["contract.#{name}"] = Uber::Option[builder].(operation, constant: contract_class, model: model) if builder
|
18
|
+
|
19
|
+
operation["contract.#{name}"] = contract_class.new(model)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.import!(operation, import, **args)
|
23
|
+
import.(:>, ->(operation, options) { build_contract!(operation, options, **args) },
|
24
|
+
name: "contract.build")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.Build(*args, &block)
|
29
|
+
[ Build, args, block ]
|
30
|
+
end
|
31
|
+
|
32
|
+
module DSL
|
33
|
+
# This is the class level DSL method.
|
34
|
+
# Op.contract #=> returns contract class
|
35
|
+
# Op.contract do .. end # defines contract
|
36
|
+
# Op.contract CommentForm # copies (and subclasses) external contract.
|
37
|
+
# Op.contract CommentForm do .. end # copies and extends contract.
|
38
|
+
def contract(name=:default, constant=nil, base: Reform::Form, &block)
|
39
|
+
heritage.record(:contract, name, constant, &block)
|
40
|
+
|
41
|
+
path, form_class = Trailblazer::DSL::Build.new.({ prefix: :contract, class: base, container: self }, name, constant, block)
|
42
|
+
|
43
|
+
self[path] = form_class
|
44
|
+
end
|
45
|
+
end # Contract
|
46
|
+
end
|
@@ -1,19 +1,17 @@
|
|
1
|
-
require "trailblazer/endpoint"
|
2
|
-
|
3
1
|
module Trailblazer::Operation::Controller
|
4
2
|
private
|
5
3
|
def form(operation_class, options={})
|
6
|
-
res,
|
7
|
-
|
4
|
+
res, options = operation_for!(operation_class, options) { |params| { operation: operation_class.present(params) } }
|
5
|
+
res.contract.prepopulate!(options) # equals to @form.prepopulate!
|
8
6
|
|
9
|
-
|
7
|
+
res.contract
|
10
8
|
end
|
11
9
|
|
12
10
|
# Provides the operation instance, model and contract without running #process.
|
13
11
|
# Returns the operation.
|
14
12
|
def present(operation_class, options={})
|
15
|
-
res,
|
16
|
-
|
13
|
+
res, options = operation_for!(operation_class, options.merge(skip_form: true)) { |params| { operation: operation_class.present(params) } }
|
14
|
+
res # FIXME.
|
17
15
|
end
|
18
16
|
|
19
17
|
def collection(*args)
|
@@ -23,11 +21,11 @@ private
|
|
23
21
|
end
|
24
22
|
|
25
23
|
def run(operation_class, options={}, &block)
|
26
|
-
res
|
24
|
+
res = operation_for!(operation_class, options) { |params| operation_class.(params) }
|
27
25
|
|
28
|
-
yield
|
26
|
+
yield res if res[:valid] and block_given?
|
29
27
|
|
30
|
-
|
28
|
+
res # FIXME.
|
31
29
|
end
|
32
30
|
|
33
31
|
# The block passed to #respond is always run, regardless of the validity result.
|
@@ -40,33 +38,53 @@ private
|
|
40
38
|
end
|
41
39
|
|
42
40
|
private
|
43
|
-
def operation!(operation_class, options={}) # or #model or #setup.
|
44
|
-
operation_for!(operation_class, options) { |params| [true, operation_class.present(params)] }
|
45
|
-
end
|
46
|
-
|
47
41
|
def process_params!(params)
|
48
42
|
end
|
49
43
|
|
50
|
-
# Override and return arbitrary params object.
|
51
|
-
def params!(params)
|
52
|
-
params
|
53
|
-
end
|
54
|
-
|
55
44
|
# Normalizes parameters and invokes the operation (including its builders).
|
56
45
|
def operation_for!(operation_class, options, &block)
|
57
46
|
params = options[:params] || self.params # TODO: test params: parameter properly in all 4 methods.
|
58
|
-
params = params!(params)
|
59
47
|
process_params!(params) # deprecate or rename to #setup_params!
|
60
48
|
|
61
|
-
res
|
62
|
-
setup_operation_instance_variables!(
|
49
|
+
res = Endpoint.new(operation_class, params, request, options).(&block)
|
50
|
+
setup_operation_instance_variables!(res, options)
|
51
|
+
|
52
|
+
[res, options.merge(params: params)]
|
53
|
+
end
|
63
54
|
|
64
|
-
|
55
|
+
def setup_operation_instance_variables!(result, options)
|
56
|
+
@operation = result # FIXME: remove!
|
57
|
+
@model = result["model"]
|
58
|
+
@form = result["contract"] unless options[:skip_form]
|
65
59
|
end
|
66
60
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
61
|
+
# Encapsulates HTTP-specific logic needed before running an operation.
|
62
|
+
# Right now, all this does is #document_body! which figures out whether or not to pass the request body
|
63
|
+
# into params, so the operation can use a representer to deserialize the original document.
|
64
|
+
# To be used in Hanami, Roda, Rails, etc.
|
65
|
+
class Endpoint
|
66
|
+
def initialize(operation_class, params, request, options)
|
67
|
+
@operation_class = operation_class
|
68
|
+
@params = params
|
69
|
+
@request = request
|
70
|
+
@is_document = options[:is_document]
|
71
|
+
end
|
72
|
+
|
73
|
+
def call
|
74
|
+
document_body! if @is_document
|
75
|
+
yield @params# Create.run(params)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
attr_reader :params, :operation_class, :request
|
80
|
+
|
81
|
+
def document_body!
|
82
|
+
# this is what happens:
|
83
|
+
# respond_with Comment::Update::JSON.run(params.merge(comment: request.body.string))
|
84
|
+
concept_name = operation_class.model_class.to_s.underscore # this could be renamed to ::concept_class soon.
|
85
|
+
request_body = request.body.respond_to?(:string) ? request.body.string : request.body.read
|
86
|
+
|
87
|
+
params.merge!(concept_name => request_body)
|
88
|
+
end
|
71
89
|
end
|
72
90
|
end
|