trailblazer 1.1.2 → 2.0.0.beta1
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.
- 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
|