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
@@ -0,0 +1,24 @@
1
+ require "trailblazer/operation/policy"
2
+ require "uber/option"
3
+
4
+ class Trailblazer::Operation
5
+ module Policy
6
+ module Guard
7
+ def self.import!(operation, import, user_proc, options={})
8
+ Policy.add!(operation, import, options) { Guard.build(user_proc) }
9
+ end
10
+
11
+ def self.build(callable)
12
+ value = Uber::Option[callable]
13
+
14
+ # call'ing the Uber::Option will run either proc or block.
15
+ # this gets wrapped in a Operation::Result object.
16
+ ->(options) { Result.new( !!value.(options), {} ) }
17
+ end
18
+ end # Guard
19
+
20
+ def self.Guard(*args, &block)
21
+ [ Guard, args, block ]
22
+ end
23
+ end
24
+ end
@@ -1,50 +1,58 @@
1
- require "trailblazer/operation/model/dsl"
2
-
3
- module Trailblazer
4
- class Operation
5
- # The Model module will automatically create/find models for the configured +action+.
6
- # It adds a public +Operation#model+ reader to access the model (after performing).
7
- module Model
8
- def self.included(base)
9
- base.extend DSL
1
+ class Trailblazer::Operation
2
+ module Model
3
+ Step = ->(operation, options) { options["model"] = operation.model!(options["params"]) }
4
+
5
+ def self.import!(operation, import, model_class, action=nil)
6
+ if import.inheriting? # not sure how to do overrides!
7
+ # FIXME: prototyping inheritance. should we handle that here?
8
+ return operation["model.action"] = model_class
10
9
  end
11
10
 
12
- # Methods to create the model according to class configuration and params.
13
- module BuildModel
14
- def model!(params)
15
- instantiate_model(params)
16
- end
11
+ import.(:&, Step, name: "model.build")
17
12
 
18
- def instantiate_model(params)
19
- send("#{action_name}_model", params)
20
- end
13
+ operation["model.class"] = model_class
14
+ operation["model.action"] = action
21
15
 
22
- def create_model(params)
23
- model_class.new
24
- end
16
+ operation.send :include, BuildMethods
17
+ end
25
18
 
26
- def update_model(params)
27
- model_class.find(params[:id])
28
- end
19
+ # Methods to create the model according to class configuration and params.
20
+ module BuildMethods
21
+ def model_class
22
+ self["model.class"] or raise "[Trailblazer] You didn't call Operation::model."
23
+ end
29
24
 
30
- alias_method :find_model, :update_model
25
+ def action_name
26
+ self["model.action"] or :new
31
27
  end
32
28
 
29
+ def model!(params)
30
+ instantiate_model(params)
31
+ end
33
32
 
34
- # #validate no longer accepts a model since this module instantiates it for you.
35
- def validate(params, model=self.model, *args)
36
- super(params, model, *args)
33
+ def instantiate_model(params)
34
+ send("#{action_name}_model", params)
37
35
  end
38
36
 
39
- private
40
- include BuildModel
37
+ def new_model(params)
38
+ model_class.new
39
+ end
41
40
 
42
- def model_class
43
- self.class.model_class
41
+ def update_model(params)
42
+ model_class.find(params[:id])
44
43
  end
45
- def action_name
46
- self.class.action_name
44
+
45
+ alias_method :find_model, :update_model
46
+
47
+ # Doesn't throw an exception and will return false to divert to Left.
48
+ def find_by_model(params)
49
+ model = model_class.find_by(id: params[:id])
50
+
51
+ self["result.model"] = Result.new(!model.nil?, {})
52
+ model
47
53
  end
48
54
  end
49
55
  end
56
+
57
+ DSL.macro!(:Model, Model)
50
58
  end
@@ -0,0 +1,43 @@
1
+ class Trailblazer::Operation
2
+ module Nested
3
+ # Please note that the instance_variable_get are here on purpose since the
4
+ # superinternal API is not entirely decided, yet.
5
+ def self.import!(operation, import, step)
6
+ import.(:&, ->(input, options) {
7
+ result = step._call(*options.to_runtime_data)
8
+
9
+ result.instance_variable_get(:@data).to_mutable_data.each do |k,v|
10
+ options[k] = v
11
+ end
12
+
13
+ result.success? # DISCUSS: what if we could simply return the result object here?
14
+ }, {} )
15
+ end
16
+ end
17
+
18
+ DSL.macro!(:Nested, Nested)
19
+
20
+ module Rescue
21
+ def self.import!(_operation, import, *exceptions, handler:->(*){}, &block)
22
+ exceptions = [StandardError] unless exceptions.any?
23
+ handler = Pipetree::DSL::Option.(handler)
24
+
25
+ rescue_block = ->(options, operation, *, &nested_pipe) {
26
+ begin
27
+ res = nested_pipe.call
28
+ res.first == ::Pipetree::Flow::Right # FIXME.
29
+ rescue *exceptions => exception
30
+ handler.call(operation, exception, options)
31
+ #options["result.model.find"] = "argh! because #{exception.class}"
32
+ false
33
+ end
34
+ }
35
+
36
+ # operation.| operation.Wrap(rescue_block, &block), name: "Rescue:#{block.source_location.last}"
37
+ Wrap.import! _operation, import, rescue_block, name: "Rescue:#{block.source_location.last}", &block
38
+ end
39
+ end
40
+
41
+ DSL.macro!(:Rescue, Rescue)
42
+ end
43
+
@@ -0,0 +1,13 @@
1
+ class Trailblazer::Operation
2
+ module Params
3
+ def self.included(includer)
4
+ includer.> Replace, after: New
5
+ end
6
+ end
7
+
8
+ # Returned object will replace "params". Original is saved in "params.original".
9
+ Params::Replace = ->(input, options) {
10
+ options["params.original"] = original = options["params"]
11
+ options["params"] = input.params!(original)
12
+ }
13
+ end
@@ -0,0 +1,13 @@
1
+ class Trailblazer::Operation
2
+ module Persist
3
+ def self.import!(operation, import, options={})
4
+ save_method = options[:method] || :save
5
+ contract_name = options[:name] || "contract.default"
6
+
7
+ import.(:&, ->(input, options) { options[contract_name].send(save_method) }, # TODO: test me.
8
+ name: "persist.save")
9
+ end
10
+ end
11
+
12
+ DSL.macro!(:Persist, Persist)
13
+ end
@@ -1,85 +1,39 @@
1
- require "trailblazer/operation/policy/guard"
1
+ class Trailblazer::Operation
2
+ module Policy
3
+ # Step: This generically `call`s a policy and then pushes its result to `options`.
4
+ # You can use any callable object as a policy with this step.
5
+ # :private:
6
+ class Eval
7
+ include Uber::Callable
2
8
 
3
- module Trailblazer
4
- class NotAuthorizedError < RuntimeError
5
- end
6
-
7
- # Adds #evaluate_policy to #setup!, and ::policy.
8
- module Operation::Policy
9
- def self.included(includer)
10
- includer.extend DSL
11
- end
12
-
13
- module DSL
14
- def self.extended(extender)
15
- extender.inheritable_attr :policy_config
16
- extender.policy_config = lambda { |*| true } # return true per default.
9
+ def initialize(name:nil, path:nil)
10
+ @name = name
11
+ @path = path
17
12
  end
18
13
 
19
- def policy(*args, &block)
20
- self.policy_config = permission_class.new(*args, &block)
21
- end
14
+ def call(input, options)
15
+ condition = options[@path] # this allows dependency injection.
16
+ result = condition.(options)
22
17
 
23
- def permission_class
24
- Permission
25
- end
26
- end
18
+ options["policy.#{@name}"] = result["policy"] # assign the policy as a skill.
19
+ options["result.policy.#{@name}"] = result
27
20
 
28
- attr_reader :policy
29
-
30
- private
31
- module Setup
32
- def setup!(params)
33
- evaluate_policy(super)
21
+ # flow control
22
+ result.success? # since we & this, it's only executed OnRight and the return boolean decides the direction, input is passed straight through.
34
23
  end
35
24
  end
36
- include Setup
37
-
38
- def evaluate_policy(params)
39
- user = params[:current_user]
40
25
 
41
- @policy = self.class.policy_config.(user, model, @policy) do |policy, action|
42
- raise policy_exception(policy, action, model)
43
- end
44
- end
26
+ # Adds the `yield` result to the pipe and treats it like a policy-compatible object at runtime.
27
+ def self.add!(operation, import, options)
28
+ name = options[:name] || :default
45
29
 
46
- def policy_exception(policy, action, model)
47
- NotAuthorizedError.new(query: action, record: model, policy: policy)
48
- end
30
+ # configure class level.
31
+ operation[path = "policy.#{name}.eval"] = yield
49
32
 
50
- # Encapsulate building the Policy object and calling the defined query action.
51
- # This assumes the policy class is "pundit-style", as in Policy.new(user, model).edit?.
52
- class Permission
53
- def initialize(policy_class, action)
54
- @policy_class, @action = policy_class, action
55
- end
56
-
57
- # Without a block, return the policy object (which is usually a Pundit-style class).
58
- # When block is passed evaluate the default rule and run block when false.
59
- def call(user, model, external_policy=nil)
60
- policy = build_policy(user, model, external_policy)
61
-
62
- policy.send(@action) || yield(policy, @action) if block_given?
63
- policy
64
- end
65
-
66
- private
67
- def build_policy(user, model, policy)
68
- policy or @policy_class.new(user, model)
69
- end
70
- end
71
- end
72
-
73
-
74
- module Operation::Deny
75
- def self.included(includer)
76
- includer.extend ClassMethods
77
- end
78
-
79
- module ClassMethods
80
- def deny!
81
- raise NotAuthorizedError
82
- end
33
+ # add step.
34
+ import.(:&, Eval.new( name: name, path: path ),
35
+ name: path
36
+ )
83
37
  end
84
38
  end
85
39
  end
@@ -0,0 +1,19 @@
1
+ class Trailblazer::Operation
2
+ module Present
3
+ def self.included(includer)
4
+ includer.extend PresentMethod
5
+ includer.& Stop, before: Call
6
+ end
7
+
8
+ module PresentMethod
9
+ def present(params={}, options={}, *args)
10
+ call(params, options.merge("present.stop?" => true), *args)
11
+ end
12
+ end
13
+ end
14
+
15
+ # Stops the pipeline if "present.stop?" is set, which usually happens in Operation::present.
16
+ Present::Stop = ->(input, options) { ! options["present.stop?"] } # false returns Left.
17
+ end
18
+
19
+ # TODO: another stop for present without the contract!
@@ -0,0 +1,15 @@
1
+ module Trailblazer::Operation::Procedural
2
+ # THIS IS UNTESTED, PRIVATE API AND WILL BE REMOVED SOON.
3
+ module Contract
4
+ # Instantiate the contract, either by using the user's contract passed into #validate
5
+ # or infer the Operation contract.
6
+ def contract_for(model:self["model"], options:{}, contract_class:self["contract.default.class"])
7
+ contract!(model: model, options: options, contract_class: contract_class)
8
+ end
9
+
10
+ # Override to construct your own contract.
11
+ def contract!(model:nil, options:{}, contract_class:nil)
12
+ contract_class.new(model, options)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module Trailblazer::Operation::Procedural
2
+ module Validate
3
+ def validate(params, contract:self["contract.default"], path:"contract.default") # :params
4
+ # DISCUSS: should we only have path here and then look up contract ourselves?
5
+ result = validate_contract(contract, params) # run validation. # FIXME: must be overridable.
6
+
7
+ self["result.#{path}"] = result
8
+
9
+ if valid = result.success? # FIXME: to_bool or success?
10
+ yield result if block_given?
11
+ else
12
+ # self["errors.#{path}"] = result.errors # TODO: remove me
13
+ end
14
+
15
+ valid
16
+ end
17
+
18
+ def validate_contract(contract, params)
19
+ contract.(params)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ class Trailblazer::Operation
2
+ module Policy
3
+ module Pundit
4
+ def self.import!(operation, import, policy_class, action, options={})
5
+ Policy.add!(operation, import, options) { Pundit.build(policy_class, action) }
6
+ end
7
+
8
+ def self.build(*args, &block)
9
+ Condition.new(*args, &block)
10
+ end
11
+
12
+ # Pundit::Condition is invoked at runtime when iterating the pipe.
13
+ class Condition
14
+ def initialize(policy_class, action)
15
+ @policy_class, @action = policy_class, action
16
+ end
17
+
18
+ # Instantiate the actual policy object, and call it.
19
+ def call(skills)
20
+ policy = build_policy(skills) # this translates to Pundit interface.
21
+ result!(policy.send(@action), policy)
22
+ end
23
+
24
+ private
25
+ def build_policy(skills)
26
+ @policy_class.new(skills["current_user"], skills["model"])
27
+ end
28
+
29
+ def result!(success, policy)
30
+ data = { "policy" => policy }
31
+ data["message"] = "Breach" if !success # TODO: how to allow messages here?
32
+
33
+ Result.new(success, data)
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.Pundit(*args, &block)
39
+ [ Pundit, args, block ]
40
+ end
41
+ end
42
+ end
@@ -1,98 +1,31 @@
1
- # Including this will change the way deserialization in #validate works.
2
- #
3
- # Instead of treating params as a hash and letting the form object deserialize it,
4
- # a representer will be infered from the contract. This representer is then passed as
5
- # deserializer into Form#validate.
6
- #
7
- # TODO: so far, we only support JSON, but it's two lines to change to support any kind of format.
8
- module Trailblazer::Operation::Representer
9
- def self.included(base)
10
- base.extend DSL
11
- end
12
-
13
- module DSL
14
- def self.extended(extender)
15
- extender.inheritable_attr :_representer_class
16
- end
17
-
18
- def representer(constant=nil, &block)
19
- return representer_class unless constant or block_given?
20
-
21
- self.representer_class= Class.new(constant) if constant
22
- representer_class.class_eval(&block) if block_given?
23
- end
24
-
25
- def representer_class
26
- self._representer_class ||= infer_representer_class
27
- end
28
-
29
- def representer_class=(constant)
30
- self._representer_class = constant
31
- end
32
-
33
- require "disposable/version"
34
- def infer_representer_class
35
- if Disposable::VERSION =~ /^0.1/
36
- warn "[Trailblazer] Reform 2.0 won't be supported in Trailblazer 1.2. Don't be lazy and upgrade to Reform 2.1."
37
-
38
- Disposable::Twin::Schema.from(contract_class,
39
- include: [Representable::JSON],
40
- options_from: :deserializer, # use :instance etc. in deserializer.
41
- superclass: Representable::Decorator,
42
- representer_from: lambda { |inline| inline.representer_class },
43
- )
44
- else
45
- Disposable::Rescheme.from(contract_class,
46
- include: [Representable::JSON],
47
- options_from: :deserializer, # use :instance etc. in deserializer.
48
- superclass: Representable::Decorator,
49
- definitions_from: lambda { |inline| inline.definitions },
50
- exclude_options: [:default, :populator], # TODO: test with populator: in an operation.
51
- exclude_properties: [:persisted?]
52
- )
1
+ class Trailblazer::Operation
2
+ module Representer
3
+ def self.infer(contract_class, format:Representable::JSON)
4
+ Disposable::Rescheme.from(contract_class,
5
+ include: [format],
6
+ options_from: :deserializer, # use :instance etc. in deserializer.
7
+ superclass: Representable::Decorator,
8
+ definitions_from: lambda { |inline| inline.definitions },
9
+ exclude_options: [:default, :populator], # TODO: test with populator: in an operation.
10
+ exclude_properties: [:persisted?]
11
+ )
12
+ end
13
+
14
+ module DSL
15
+ def representer(name=:default, constant=nil, &block)
16
+ heritage.record(:representer, name, constant, &block)
17
+
18
+ # FIXME: make this nicer. we want to extend same-named callback groups.
19
+ # TODO: allow the same with contract, or better, test it!
20
+ path, representer_class = Trailblazer::DSL::Build.new.({ prefix: :representer, class: representer_base_class, container: self }, name, constant, block)
21
+
22
+ self[path] = representer_class
53
23
  end
54
- end
55
- end
56
-
57
- private
58
- module Rendering
59
- # Override this if you need to pass options to the rendering.
60
- #
61
- # def to_json(*)
62
- # super(include: @params[:include])
63
- # end
64
- def to_json(options={})
65
- self.class.representer_class.new(represented).to_json(options)
66
- end
67
-
68
- # Override this if you want to render something else, e.g. the contract.
69
- def represented
70
- model
71
- end
72
- end
73
- include Rendering
74
-
75
-
76
- module Deserializer
77
- module Hash
78
- def validate_contract(params)
79
- # use the inferred representer from the contract for deserialization in #validate.
80
- contract.validate(params) do |document|
81
- self.class.representer_class.new(contract).from_hash(document)
82
- end
83
- end
84
- end
85
24
 
86
- # This looks crazy, but all it does is using a Reform hook in #validate where we can use
87
- # our own representer for deserialization. After the object graph is set up, Reform will
88
- # run its validation without even knowing this came from JSON.
89
- module JSON
90
- def validate_contract(params)
91
- contract.validate(params) do |document|
92
- self.class.representer_class.new(contract).from_json(document)
93
- end
25
+ # TODO: make engine configurable?
26
+ def representer_base_class
27
+ Class.new(Representable::Decorator) { include Representable::JSON; self }
94
28
  end
95
29
  end
96
30
  end
97
- include Deserializer::JSON
98
31
  end