trailblazer 0.3.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGES.md +12 -0
  4. data/Gemfile +2 -1
  5. data/README.md +73 -38
  6. data/Rakefile +1 -1
  7. data/lib/trailblazer/autoloading.rb +4 -1
  8. data/lib/trailblazer/endpoint.rb +7 -13
  9. data/lib/trailblazer/operation.rb +54 -40
  10. data/lib/trailblazer/operation/builder.rb +26 -0
  11. data/lib/trailblazer/operation/collection.rb +1 -2
  12. data/lib/trailblazer/operation/controller.rb +36 -48
  13. data/lib/trailblazer/operation/dispatch.rb +11 -11
  14. data/lib/trailblazer/operation/model.rb +50 -0
  15. data/lib/trailblazer/operation/model/dsl.rb +29 -0
  16. data/lib/trailblazer/operation/model/external.rb +34 -0
  17. data/lib/trailblazer/operation/policy.rb +87 -0
  18. data/lib/trailblazer/operation/policy/guard.rb +34 -0
  19. data/lib/trailblazer/operation/representer.rb +33 -12
  20. data/lib/trailblazer/operation/resolver.rb +30 -0
  21. data/lib/trailblazer/operation/responder.rb +0 -1
  22. data/lib/trailblazer/operation/worker.rb +24 -7
  23. data/lib/trailblazer/version.rb +1 -1
  24. data/test/collection_test.rb +2 -1
  25. data/test/{crud_test.rb → model_test.rb} +17 -35
  26. data/test/operation/builder_test.rb +41 -0
  27. data/test/operation/dsl/callback_test.rb +108 -0
  28. data/test/operation/dsl/contract_test.rb +104 -0
  29. data/test/operation/dsl/representer_test.rb +143 -0
  30. data/test/operation/external_model_test.rb +71 -0
  31. data/test/operation/guard_test.rb +97 -0
  32. data/test/operation/policy_test.rb +97 -0
  33. data/test/operation/resolver_test.rb +83 -0
  34. data/test/operation_test.rb +7 -75
  35. data/test/rails/__respond_test.rb +20 -0
  36. data/test/rails/controller_test.rb +4 -102
  37. data/test/rails/endpoint_test.rb +7 -47
  38. data/test/rails/fake_app/controllers.rb +16 -21
  39. data/test/rails/fake_app/rails_app.rb +5 -0
  40. data/test/rails/fake_app/song/operations.rb +11 -4
  41. data/test/rails/respond_test.rb +95 -0
  42. data/test/responder_test.rb +6 -6
  43. data/test/rollback_test.rb +2 -2
  44. data/test/worker_test.rb +13 -9
  45. data/trailblazer.gemspec +2 -2
  46. metadata +38 -15
  47. data/lib/trailblazer/operation/crud.rb +0 -82
  48. data/lib/trailblazer/rails/railtie.rb +0 -34
  49. data/test/rails/fake_app/views/bands/show.html.erb +0 -1
@@ -0,0 +1,26 @@
1
+ require "uber/builder"
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
8
+
9
+ def builder_class
10
+ @builders
11
+ end
12
+
13
+ def builder_class=(constant)
14
+ @builders = constant
15
+ end
16
+
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)
21
+ end
22
+
23
+ def build_operation(params, options={})
24
+ build_operation_class(params).new(params, options)
25
+ end
26
+ end
@@ -1,7 +1,6 @@
1
1
  module Trailblazer::Operation::Collection
2
2
  # Collection does not produce a contract.
3
- def present(*params)
4
- setup!(*params)
3
+ def present
5
4
  self
6
5
  end
7
6
  end
@@ -2,48 +2,25 @@ require "trailblazer/endpoint"
2
2
 
3
3
  module Trailblazer::Operation::Controller
4
4
  private
5
- def form(operation_class, params=self.params) # consider private.
6
- process_params!(params)
7
-
8
- @operation = operation_class.present(params)
9
- @form = @operation.contract
10
- @model = @operation.model
11
-
12
- yield @operation if block_given?
5
+ def form(*args)
6
+ present(*args).tap do |op|
7
+ op.contract.prepopulate! # equals to @form.prepopulate!
8
+ end
13
9
  end
14
10
 
15
- # Doesn't run #process.
11
+ # Provides the operation instance, model and contract without running #process.
12
+ # Returns the operation.
16
13
  def present(operation_class, params=self.params)
17
14
  res, op = operation!(operation_class, params) { [true, operation_class.present(params)] }
18
-
19
- yield op if block_given?
20
- # respond_with op
21
- # TODO: implement respond(present: true)
22
- end
23
-
24
- def collection(operation_class, params=self.params)
25
- # TODO: merge with #present.
26
- res, op = operation!(operation_class, params) { [true, operation_class.present(params)] }
27
- @collection = @model
28
-
29
- yield op if block_given?
15
+ op
30
16
  end
31
17
 
32
- # full-on Op[]
33
- # Note: this is not documented on purpose as this concept is experimental. I don't like it too much and prefer
34
- # returns in the valid block.
35
- class Else
36
- def initialize(op, run)
37
- @op = op
38
- @run = run
39
- end
40
-
41
- def else
42
- yield @op if @run
18
+ def collection(*args)
19
+ present(*args).tap do |op|
20
+ @collection = op.model
43
21
  end
44
22
  end
45
23
 
46
- # Endpoint::Invocation
47
24
  def run(operation_class, params=self.params, &block)
48
25
  res, op = operation!(operation_class, params) { operation_class.run(params) }
49
26
 
@@ -53,19 +30,29 @@ private
53
30
  end
54
31
 
55
32
  # The block passed to #respond is always run, regardless of the validity result.
56
- def respond(operation_class, params=self.params, respond_options = {}, &block)
57
- res, op = operation!(operation_class, params) { operation_class.run(params) }
33
+ def respond(operation_class, options={}, params=self.params, &block)
34
+ res, op = operation!(operation_class, params, options) { operation_class.run(params) }
35
+ namespace = options.delete(:namespace) || []
58
36
 
59
- return respond_with op, respond_options if not block_given?
60
- respond_with op, respond_options, &Proc.new { |formats| block.call(op, formats) } if block_given?
37
+ return respond_with *namespace, op, options if not block_given?
38
+ respond_with *namespace, op, options, &Proc.new { |formats| block.call(op, formats) } if block_given?
61
39
  end
62
40
 
41
+ private
42
+
63
43
  def process_params!(params)
64
44
  end
65
45
 
66
46
  # Normalizes parameters and invokes the operation (including its builders).
67
- def operation!(operation_class, params, &block)
68
- Trailblazer::Endpoint.new(self, operation_class, params, request, self.class._operation).(&block)
47
+ def operation!(operation_class, params, options={}, &block)
48
+ # Per default, only treat :html as non-document.
49
+ options[:is_document] ||= request.format == :html ? false : true
50
+
51
+ process_params!(params)
52
+ res, op = Trailblazer::Endpoint.new(operation_class, params, request, options).(&block)
53
+ setup_operation_instance_variables!(op)
54
+
55
+ [res, op]
69
56
  end
70
57
 
71
58
  def setup_operation_instance_variables!(operation)
@@ -74,16 +61,17 @@ private
74
61
  @model = operation.model
75
62
  end
76
63
 
77
- def self.included(includer)
78
- includer.extend Uber::InheritableAttr
79
- includer.inheritable_attr :_operation
80
- includer._operation = {document_formats: {}}
81
- includer.extend ClassMethods
82
- end
83
64
 
84
- module ClassMethods
85
- def operation(options)
86
- _operation[:document_formats][options[:document_formats]] = true
65
+ # Note: this is not documented on purpose as this concept is experimental. I don't like it too much and prefer
66
+ # returns in the valid block.
67
+ class Else
68
+ def initialize(op, run)
69
+ @op = op
70
+ @run = run
71
+ end
72
+
73
+ def else
74
+ yield @op if @run
87
75
  end
88
76
  end
89
77
  end
@@ -1,6 +1,12 @@
1
1
  require "disposable/callback"
2
2
 
3
3
  module Trailblazer::Operation::Dispatch
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.inheritable_attr :callbacks
7
+ base.callbacks = Representable::Cloneable::Hash.new
8
+ end
9
+
4
10
  def dispatch!(name=:default)
5
11
  group = self.class.callbacks[name].new(contract)
6
12
  group.(context: self)
@@ -12,18 +18,12 @@ module Trailblazer::Operation::Dispatch
12
18
  @invocations ||= {}
13
19
  end
14
20
 
15
-
16
21
  module ClassMethods
17
- def callback(name=:default, *args, &block)
18
- callbacks[name] ||= Class.new(Disposable::Callback::Group).extend(Representable::Cloneable)
19
- callbacks[name].class_eval(&block)
20
- end
21
- end
22
+ def callback(name=:default, constant=nil, &block)
23
+ return callbacks[name] unless constant or block_given?
22
24
 
23
- def self.included(base)
24
- base.extend ClassMethods
25
- base.extend Uber::InheritableAttr
26
- base.inheritable_attr :callbacks
27
- base.callbacks = Representable::Cloneable::Hash.new
25
+ callbacks[name] ||= Class.new(constant || Disposable::Callback::Group).extend(Representable::Cloneable) # FIXME: why Representable?
26
+ callbacks[name].class_eval(&block) if block_given?
27
+ end
28
28
  end
29
29
  end
@@ -0,0 +1,50 @@
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
10
+ end
11
+
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
17
+
18
+ def instantiate_model(params)
19
+ send("#{action_name}_model", params)
20
+ end
21
+
22
+ def create_model(params)
23
+ model_class.new
24
+ end
25
+
26
+ def update_model(params)
27
+ model_class.find(params[:id])
28
+ end
29
+
30
+ alias_method :find_model, :update_model
31
+ end
32
+
33
+
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)
37
+ end
38
+
39
+ private
40
+ include BuildModel
41
+
42
+ def model_class
43
+ self.class.model_class
44
+ end
45
+ def action_name
46
+ self.class.action_name
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ class Trailblazer::Operation
2
+ module Model
3
+ # Imports ::model and ::action into an operation.
4
+ module DSL
5
+ def self.extended(extender)
6
+ extender.extend Uber::InheritableAttr
7
+ extender.inheritable_attr :config
8
+ extender.config = {}
9
+ end
10
+
11
+ def model(name, action=nil)
12
+ self.config[:model] = name
13
+ action(action) if action # coolest line ever.
14
+ end
15
+
16
+ def action(name)
17
+ self.config[:action] = name
18
+ end
19
+
20
+ def action_name # considered private.
21
+ self.config[:action] or :create
22
+ end
23
+
24
+ def model_class # considered private.
25
+ self.config[:model] or raise "[Trailblazer] You didn't call Operation::model."
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require "trailblazer/operation/model"
2
+
3
+ class Trailblazer::Operation
4
+ module Model
5
+ # Builds (finds or creates) the model _before_ the operation is instantiated.
6
+ # Passes the model instance into the builder with the following signature.
7
+ #
8
+ # builds ->(model, params)
9
+ #
10
+ # The initializer will now expect you to pass the model in via options[:model]. This
11
+ # happens automatically when coming from a builder.
12
+ module External
13
+ def self.included(includer)
14
+ includer.extend Model::DSL
15
+ includer.extend Model::BuildModel
16
+ includer.extend ClassMethods
17
+ end
18
+
19
+ def assign_model!(*) # i don't like to "disable" the `@model =` like this but it's the simplest for now.
20
+ @model = @options[:model]
21
+ end
22
+
23
+
24
+ module ClassMethods
25
+ private
26
+ def build_operation(params, options={}) # TODO: merge with Resolver::build_operation.
27
+ model = model!(params)
28
+ build_operation_class(model, params). # calls builds->(model, params).
29
+ new(params, options.merge(model: model))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,87 @@
1
+ module Trailblazer
2
+ class NotAuthorizedError < RuntimeError
3
+ end
4
+
5
+ # Adds #evaluate_policy to #setup!, and ::policy.
6
+ module Operation::Policy
7
+ require "trailblazer/operation/policy/guard"
8
+
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 = Guard::Permission.new { true } # return true per default.
17
+ end
18
+
19
+ def policy(*args, &block)
20
+ self.policy_config = permission_class.new(*args, &block)
21
+ end
22
+
23
+ def permission_class
24
+ Permission
25
+ end
26
+ end
27
+
28
+ attr_reader :policy
29
+
30
+ private
31
+ module Setup
32
+ def setup!(params)
33
+ super
34
+ evaluate_policy(params)
35
+ end
36
+ end
37
+ include Setup
38
+
39
+
40
+ private
41
+ def evaluate_policy(params)
42
+ user = params[:current_user]
43
+
44
+ @policy = self.class.policy_config.(user, model, @policy) do |policy, action|
45
+ raise policy_exception(policy, action, model)
46
+ end
47
+ end
48
+
49
+ def policy_exception(policy, action, model)
50
+ NotAuthorizedError.new(query: action, record: model, policy: policy)
51
+ end
52
+
53
+ # Encapsulate building the Policy object and calling the defined query action.
54
+ # This assumes the policy class is "pundit-style", as in Policy.new(user, model).edit?.
55
+ class Permission
56
+ def initialize(policy_class, action)
57
+ @policy_class, @action = policy_class, action
58
+ end
59
+
60
+ # Without a block, return the policy object (which is usually a Pundit-style class).
61
+ # When block is passed evaluate the default rule and run block when false.
62
+ def call(user, model, external_policy=nil)
63
+ build_policy(user, model, external_policy).tap do |policy|
64
+ policy.send(@action) || yield(policy, @action) if block_given?
65
+ end
66
+ end
67
+
68
+ private
69
+ def build_policy(user, model, policy)
70
+ policy or @policy_class.new(user, model)
71
+ end
72
+ end
73
+ end
74
+
75
+
76
+ module Operation::Deny
77
+ def self.included(includer)
78
+ includer.extend ClassMethods
79
+ end
80
+
81
+ module ClassMethods
82
+ def deny!
83
+ raise NotAuthorizedError
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ module Trailblazer
2
+ # Adds #evaluate_policy to Operation#setup!
3
+ module Operation::Policy
4
+ module Guard
5
+ def self.included(includer)
6
+ includer.extend(DSL)
7
+ includer.extend(ClassMethods)
8
+ includer.send(:include, Setup)
9
+ end
10
+
11
+ module ClassMethods
12
+ # Use Guard::Permission.
13
+ def permission_class
14
+ Permission
15
+ end
16
+ end
17
+
18
+ def evaluate_policy(params)
19
+ self.class.policy_config.(self, params) or raise NotAuthorizedError.new
20
+ end
21
+
22
+ # Encapsulates the operation's policy which is usually called in Op#setup!.
23
+ class Permission
24
+ def initialize(*args, &block)
25
+ @callable, @args = Uber::Options::Value.new(block), args
26
+ end
27
+
28
+ def call(context, *args)
29
+ @callable.(context, *args)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -7,14 +7,16 @@
7
7
  # TODO: so far, we only support JSON, but it's two lines to change to support any kind of format.
8
8
  module Trailblazer::Operation::Representer
9
9
  def self.included(base)
10
- base.extend Uber::InheritableAttr
11
10
  base.inheritable_attr :_representer_class
12
11
  base.extend ClassMethods
13
12
  end
14
13
 
15
14
  module ClassMethods
16
- def representer(&block)
17
- representer_class.class_eval(&block)
15
+ def representer(constant=nil, &block)
16
+ return representer_class unless constant or block_given?
17
+
18
+ self.representer_class= Class.new(constant) if constant
19
+ representer_class.class_eval(&block) if block_given?
18
20
  end
19
21
 
20
22
  def representer_class
@@ -35,20 +37,39 @@ module Trailblazer::Operation::Representer
35
37
  end
36
38
  end
37
39
 
38
-
39
40
  private
40
- def validate_contract(params)
41
- # use the inferred representer from the contract for deserialization in #validate.
42
- contract.validate(params) do |document|
43
- self.class.representer_class.new(contract).from_json(document)
41
+ module Rendering
42
+ def to_json(*)
43
+ self.class.representer_class.new(represented).to_json
44
+ end
45
+
46
+ def represented
47
+ contract
44
48
  end
45
49
  end
50
+ include Rendering
46
51
 
47
52
 
48
- module Rendering
49
- def to_json(*)
50
- self.class.representer_class.new(contract).to_json
53
+ module Deserializer
54
+ module Hash
55
+ def validate_contract(params)
56
+ # use the inferred representer from the contract for deserialization in #validate.
57
+ contract.validate(params) do |document|
58
+ self.class.representer_class.new(contract).from_hash(document)
59
+ end
60
+ end
61
+ end
62
+
63
+ # This looks crazy, but all it does is using a Reform hook in #validate where we can use
64
+ # our own representer for deserialization. After the object graph is set up, Reform will
65
+ # run its validation without even knowing this came from JSON.
66
+ module JSON
67
+ def validate_contract(params)
68
+ contract.validate(params) do |document|
69
+ self.class.representer_class.new(contract).from_json(document)
70
+ end
71
+ end
51
72
  end
52
73
  end
53
- include Rendering
74
+ include Deserializer::JSON
54
75
  end