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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -7
  3. data/CHANGES.md +108 -0
  4. data/COMM-LICENSE +91 -0
  5. data/Gemfile +18 -4
  6. data/LICENSE.txt +7 -20
  7. data/README.md +55 -15
  8. data/Rakefile +21 -2
  9. data/draft-1.2.rb +7 -0
  10. data/lib/trailblazer.rb +17 -4
  11. data/lib/trailblazer/dsl.rb +47 -0
  12. data/lib/trailblazer/operation/auto_inject.rb +47 -0
  13. data/lib/trailblazer/operation/builder.rb +18 -18
  14. data/lib/trailblazer/operation/callback.rb +31 -38
  15. data/lib/trailblazer/operation/contract.rb +46 -0
  16. data/lib/trailblazer/operation/controller.rb +45 -27
  17. data/lib/trailblazer/operation/guard.rb +24 -0
  18. data/lib/trailblazer/operation/model.rb +41 -33
  19. data/lib/trailblazer/operation/nested.rb +43 -0
  20. data/lib/trailblazer/operation/params.rb +13 -0
  21. data/lib/trailblazer/operation/persist.rb +13 -0
  22. data/lib/trailblazer/operation/policy.rb +26 -72
  23. data/lib/trailblazer/operation/present.rb +19 -0
  24. data/lib/trailblazer/operation/procedural/contract.rb +15 -0
  25. data/lib/trailblazer/operation/procedural/validate.rb +22 -0
  26. data/lib/trailblazer/operation/pundit.rb +42 -0
  27. data/lib/trailblazer/operation/representer.rb +25 -92
  28. data/lib/trailblazer/operation/rescue.rb +23 -0
  29. data/lib/trailblazer/operation/resolver.rb +18 -24
  30. data/lib/trailblazer/operation/validate.rb +50 -0
  31. data/lib/trailblazer/operation/wrap.rb +37 -0
  32. data/lib/trailblazer/version.rb +1 -1
  33. data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
  34. data/test/docs/auto_inject_test.rb +30 -0
  35. data/test/docs/contract_test.rb +429 -0
  36. data/test/docs/dry_test.rb +31 -0
  37. data/test/docs/guard_test.rb +143 -0
  38. data/test/docs/nested_test.rb +117 -0
  39. data/test/docs/policy_test.rb +2 -0
  40. data/test/docs/pundit_test.rb +109 -0
  41. data/test/docs/representer_test.rb +268 -0
  42. data/test/docs/rescue_test.rb +153 -0
  43. data/test/docs/wrap_test.rb +174 -0
  44. data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
  45. data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
  46. data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
  47. data/test/module_test.rb +22 -15
  48. data/test/operation/builder_test.rb +66 -18
  49. data/test/operation/callback_test.rb +70 -0
  50. data/test/operation/contract_test.rb +385 -15
  51. data/test/operation/dsl/callback_test.rb +18 -30
  52. data/test/operation/dsl/contract_test.rb +209 -19
  53. data/test/operation/dsl/representer_test.rb +42 -15
  54. data/test/operation/guard_test.rb +1 -147
  55. data/test/operation/model_test.rb +105 -0
  56. data/test/operation/params_test.rb +36 -0
  57. data/test/operation/persist_test.rb +44 -0
  58. data/test/operation/pipedream_test.rb +59 -0
  59. data/test/operation/pipetree_test.rb +104 -0
  60. data/test/operation/present_test.rb +24 -0
  61. data/test/operation/pundit_test.rb +104 -0
  62. data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
  63. data/test/operation/resolver_test.rb +34 -70
  64. data/test/operation_test.rb +57 -189
  65. data/test/test_helper.rb +23 -3
  66. data/trailblazer.gemspec +8 -7
  67. metadata +91 -59
  68. data/gemfiles/Gemfile.rails.lock +0 -130
  69. data/gemfiles/Gemfile.reform-2.0 +0 -6
  70. data/gemfiles/Gemfile.reform-2.1 +0 -7
  71. data/lib/trailblazer/autoloading.rb +0 -15
  72. data/lib/trailblazer/endpoint.rb +0 -31
  73. data/lib/trailblazer/operation.rb +0 -175
  74. data/lib/trailblazer/operation/collection.rb +0 -6
  75. data/lib/trailblazer/operation/dispatch.rb +0 -3
  76. data/lib/trailblazer/operation/model/dsl.rb +0 -29
  77. data/lib/trailblazer/operation/model/external.rb +0 -34
  78. data/lib/trailblazer/operation/policy/guard.rb +0 -35
  79. data/lib/trailblazer/operation/uploaded_file.rb +0 -77
  80. data/test/callback_test.rb +0 -104
  81. data/test/collection_test.rb +0 -57
  82. data/test/model_test.rb +0 -148
  83. data/test/operation/external_model_test.rb +0 -71
  84. data/test/operation/policy_test.rb +0 -97
  85. data/test/operation/reject_test.rb +0 -34
  86. 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
@@ -0,0 +1,7 @@
1
+ representer :deserializer, Representer::Create (do)
2
+ or
3
+ Op.(representer.deserializer.class: Representer::Create)
4
+
5
+
6
+ representer!(name)
7
+ @options[representer.deserializer.class] || self.class[representer.deserializer.class]
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
- module Trailblazer
6
- # Your code goes here...
7
- end
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
- # Allows to add builders via ::builds.
4
- module Trailblazer::Operation::Builder
5
- def self.extended(extender)
6
- extender.send(:include, Uber::Builder)
7
- end
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
- def builder_class
10
- @builders
11
- end
11
+ false # suppress inheritance. dislike. FIXME at some point.
12
+ end
12
13
 
13
- def builder_class=(constant)
14
- @builders = constant
15
- end
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
- 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)
20
+ DSL = Uber::Builder::DSL
21
21
  end
22
22
 
23
- def build_operation(params, options={})
24
- build_operation_class(params).new(params, options)
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
- module Trailblazer::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)
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
- invocations[name] = group
20
- end
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
- def dispatch!(*args, &block)
23
- callback!(*args, &block)
24
- end
18
+ options[:context] ||= (config[:context] == :operation ? self : group)
19
+ group.(options)
25
20
 
26
- def invocations
27
- @invocations ||= {}
28
- end
29
-
30
- module ClassMethods
31
- def callbacks
32
- @callbacks ||= {}
21
+ invocations[name] = group
33
22
  end
34
23
 
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)
24
+ def invocations
25
+ @invocations ||= {}
39
26
  end
40
27
 
41
- private
42
- def add_callback(name, constant, &block)
43
- heritage.record(:add_callback, name, constant, &block)
28
+ module DSL
29
+ def callback(name=:default, constant=nil, &block)
30
+ heritage.record(:callback, name, constant, &block)
44
31
 
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
- }
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
- callbacks[name][:group].class_eval(&block) if block_given?
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, op, options = operation!(operation_class, options)
7
- op.contract.prepopulate!(options) # equals to @form.prepopulate!
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
- op.contract
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, op = operation!(operation_class, options.merge(skip_form: true))
16
- op
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, op = operation_for!(operation_class, options) { |params| operation_class.run(params) }
24
+ res = operation_for!(operation_class, options) { |params| operation_class.(params) }
27
25
 
28
- yield op if res and block_given?
26
+ yield res if res[:valid] and block_given?
29
27
 
30
- op
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, op = Trailblazer::Endpoint.new(operation_class, params, request, options).(&block)
62
- setup_operation_instance_variables!(op, options)
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
- [res, op, options.merge(params: params)]
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
- def setup_operation_instance_variables!(operation, options)
68
- @operation = operation
69
- @model = operation.model
70
- @form = operation.contract unless options[:skip_form]
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