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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGES.md +12 -0
- data/Gemfile +2 -1
- data/README.md +73 -38
- data/Rakefile +1 -1
- data/lib/trailblazer/autoloading.rb +4 -1
- data/lib/trailblazer/endpoint.rb +7 -13
- data/lib/trailblazer/operation.rb +54 -40
- data/lib/trailblazer/operation/builder.rb +26 -0
- data/lib/trailblazer/operation/collection.rb +1 -2
- data/lib/trailblazer/operation/controller.rb +36 -48
- data/lib/trailblazer/operation/dispatch.rb +11 -11
- data/lib/trailblazer/operation/model.rb +50 -0
- data/lib/trailblazer/operation/model/dsl.rb +29 -0
- data/lib/trailblazer/operation/model/external.rb +34 -0
- data/lib/trailblazer/operation/policy.rb +87 -0
- data/lib/trailblazer/operation/policy/guard.rb +34 -0
- data/lib/trailblazer/operation/representer.rb +33 -12
- data/lib/trailblazer/operation/resolver.rb +30 -0
- data/lib/trailblazer/operation/responder.rb +0 -1
- data/lib/trailblazer/operation/worker.rb +24 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +2 -1
- data/test/{crud_test.rb → model_test.rb} +17 -35
- data/test/operation/builder_test.rb +41 -0
- data/test/operation/dsl/callback_test.rb +108 -0
- data/test/operation/dsl/contract_test.rb +104 -0
- data/test/operation/dsl/representer_test.rb +143 -0
- data/test/operation/external_model_test.rb +71 -0
- data/test/operation/guard_test.rb +97 -0
- data/test/operation/policy_test.rb +97 -0
- data/test/operation/resolver_test.rb +83 -0
- data/test/operation_test.rb +7 -75
- data/test/rails/__respond_test.rb +20 -0
- data/test/rails/controller_test.rb +4 -102
- data/test/rails/endpoint_test.rb +7 -47
- data/test/rails/fake_app/controllers.rb +16 -21
- data/test/rails/fake_app/rails_app.rb +5 -0
- data/test/rails/fake_app/song/operations.rb +11 -4
- data/test/rails/respond_test.rb +95 -0
- data/test/responder_test.rb +6 -6
- data/test/rollback_test.rb +2 -2
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +2 -2
- metadata +38 -15
- data/lib/trailblazer/operation/crud.rb +0 -82
- data/lib/trailblazer/rails/railtie.rb +0 -34
- 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
|
@@ -2,48 +2,25 @@ require "trailblazer/endpoint"
|
|
2
2
|
|
3
3
|
module Trailblazer::Operation::Controller
|
4
4
|
private
|
5
|
-
def form(
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
#
|
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
|
-
|
33
|
-
|
34
|
-
|
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,
|
57
|
-
res, op
|
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,
|
60
|
-
respond_with op,
|
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
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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,
|
18
|
-
callbacks[name]
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
49
|
-
|
50
|
-
|
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
|
74
|
+
include Deserializer::JSON
|
54
75
|
end
|