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