trailblazer 0.3.3 → 1.0.0.rc1

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