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