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