trailblazer 1.1.1 → 2.1.0
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 +5 -5
- data/.gitignore +1 -0
- data/.rubocop-https---raw-githubusercontent-com-trailblazer-meta-master-rubocop-yml +101 -0
- data/.rubocop.yml +20 -0
- data/.rubocop_todo.yml +556 -0
- data/.travis.yml +6 -7
- data/CHANGES.md +224 -0
- data/COMM-LICENSE +62 -0
- data/CONTRIBUTING.md +179 -0
- data/Gemfile +0 -10
- data/LICENSE +9 -0
- data/README.md +68 -189
- data/Rakefile +3 -3
- data/lib/trailblazer/version.rb +3 -1
- data/lib/trailblazer.rb +3 -6
- data/test/test_helper.rb +32 -3
- data/trailblazer.gemspec +11 -15
- metadata +28 -132
- data/LICENSE.txt +0 -22
- data/TODO.md +0 -11
- data/doc/Trb-The-Stack.png +0 -0
- data/doc/trb.jpg +0 -0
- data/gemfiles/Gemfile.rails.lock +0 -130
- data/gemfiles/Gemfile.reform-2.0 +0 -6
- data/gemfiles/Gemfile.reform-2.1 +0 -7
- data/lib/trailblazer/autoloading.rb +0 -15
- data/lib/trailblazer/endpoint.rb +0 -31
- data/lib/trailblazer/operation/builder.rb +0 -26
- data/lib/trailblazer/operation/callback.rb +0 -53
- data/lib/trailblazer/operation/collection.rb +0 -6
- data/lib/trailblazer/operation/controller.rb +0 -72
- data/lib/trailblazer/operation/dispatch.rb +0 -3
- data/lib/trailblazer/operation/model/dsl.rb +0 -29
- data/lib/trailblazer/operation/model/external.rb +0 -34
- data/lib/trailblazer/operation/model.rb +0 -50
- data/lib/trailblazer/operation/module.rb +0 -29
- data/lib/trailblazer/operation/policy/guard.rb +0 -35
- data/lib/trailblazer/operation/policy.rb +0 -85
- data/lib/trailblazer/operation/representer.rb +0 -98
- data/lib/trailblazer/operation/resolver.rb +0 -30
- data/lib/trailblazer/operation/uploaded_file.rb +0 -77
- data/lib/trailblazer/operation.rb +0 -175
- data/test/callback_test.rb +0 -104
- data/test/collection_test.rb +0 -57
- data/test/model_test.rb +0 -148
- data/test/module_test.rb +0 -93
- data/test/operation/builder_test.rb +0 -41
- data/test/operation/contract_test.rb +0 -30
- data/test/operation/controller_test.rb +0 -111
- data/test/operation/dsl/callback_test.rb +0 -118
- data/test/operation/dsl/contract_test.rb +0 -104
- data/test/operation/dsl/representer_test.rb +0 -142
- data/test/operation/external_model_test.rb +0 -71
- data/test/operation/guard_test.rb +0 -152
- data/test/operation/policy_test.rb +0 -97
- data/test/operation/reject_test.rb +0 -34
- data/test/operation/resolver_test.rb +0 -83
- data/test/operation_test.rb +0 -275
- data/test/representer_test.rb +0 -238
- data/test/rollback_test.rb +0 -47
@@ -1,34 +0,0 @@
|
|
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
|
@@ -1,50 +0,0 @@
|
|
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
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Trailblazer::Operation::Module
|
2
|
-
def self.included(base)
|
3
|
-
base.extend ClassMethods
|
4
|
-
base.extend Included
|
5
|
-
end
|
6
|
-
|
7
|
-
module Included # TODO: use representable's inheritance mechanism.
|
8
|
-
def included(base)
|
9
|
-
super
|
10
|
-
instructions.each { |cfg|
|
11
|
-
method = cfg[0]
|
12
|
-
args = cfg[1].dup
|
13
|
-
block = cfg[2]
|
14
|
-
# options = args.extract_options!.dup # we need to duplicate options has as AM::Validations messes it up later.
|
15
|
-
|
16
|
-
base.send(method, *args, &block) } # property :name, {} do .. end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
module ClassMethods
|
21
|
-
def method_missing(method, *args, &block)
|
22
|
-
instructions << [method, args, block]
|
23
|
-
end
|
24
|
-
|
25
|
-
def instructions
|
26
|
-
@instructions ||= []
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module Trailblazer
|
2
|
-
# Policy::Guard is a very simple policy implementation.
|
3
|
-
# It adds #evaluate_policy to Operation#setup! and calls whatever
|
4
|
-
# you provided to ::policy.
|
5
|
-
#
|
6
|
-
# http://trailblazer.to/gems/operation/policy.html#guard
|
7
|
-
module Operation::Policy
|
8
|
-
module Guard
|
9
|
-
def self.included(includer)
|
10
|
-
includer.extend(DSL) # Provides ::policy(CallableObject)
|
11
|
-
includer.extend(ClassMethods)
|
12
|
-
includer.send(:include, Setup)
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def policy(callable=nil, &block)
|
17
|
-
self.policy_config = Uber::Options::Value.new(callable || block)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def evaluate_policy(params)
|
22
|
-
call_policy(params) or raise policy_exception
|
23
|
-
end
|
24
|
-
|
25
|
-
# Override if you want your own policy invocation, e.g. with more args.
|
26
|
-
def call_policy(params)
|
27
|
-
self.class.policy_config.(self, params)
|
28
|
-
end
|
29
|
-
|
30
|
-
def policy_exception
|
31
|
-
NotAuthorizedError.new
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,85 +0,0 @@
|
|
1
|
-
require "trailblazer/operation/policy/guard"
|
2
|
-
|
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.
|
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
|
-
evaluate_policy(super)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
include Setup
|
37
|
-
|
38
|
-
def evaluate_policy(params)
|
39
|
-
user = params[:current_user]
|
40
|
-
|
41
|
-
@policy = self.class.policy_config.(user, model, @policy) do |policy, action|
|
42
|
-
raise policy_exception(policy, action, model)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def policy_exception(policy, action, model)
|
47
|
-
NotAuthorizedError.new(query: action, record: model, policy: policy)
|
48
|
-
end
|
49
|
-
|
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
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
@@ -1,98 +0,0 @@
|
|
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
|
-
)
|
53
|
-
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
|
-
|
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
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
include Deserializer::JSON
|
98
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
require "trailblazer/operation/model/external"
|
2
|
-
require "trailblazer/operation/policy"
|
3
|
-
|
4
|
-
class Trailblazer::Operation
|
5
|
-
# Provides builds-> (model, policy, params).
|
6
|
-
module Resolver
|
7
|
-
def self.included(includer)
|
8
|
-
includer.class_eval do
|
9
|
-
include Policy # ::build_policy
|
10
|
-
include Model::External # ::build_operation_class
|
11
|
-
|
12
|
-
extend BuildOperation # ::build_operation
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
module BuildOperation
|
17
|
-
def build_operation(params, options={})
|
18
|
-
model = model!(params)
|
19
|
-
policy = policy_config.call(params[:current_user], model)
|
20
|
-
build_operation_class(model, policy, params).
|
21
|
-
new(params, options.merge(model: model, policy: policy))
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize(params, options)
|
26
|
-
@policy = options[:policy]
|
27
|
-
super
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,77 +0,0 @@
|
|
1
|
-
require 'trailblazer/operation'
|
2
|
-
require 'action_dispatch/http/upload'
|
3
|
-
require 'tempfile'
|
4
|
-
|
5
|
-
module Trailblazer
|
6
|
-
# TODO: document:
|
7
|
-
# to_hash
|
8
|
-
# from_hash
|
9
|
-
# initialize/tmp_dir
|
10
|
-
class Operation::UploadedFile
|
11
|
-
def initialize(uploaded, options={})
|
12
|
-
@uploaded = uploaded
|
13
|
-
@options = options
|
14
|
-
@tmp_dir = options[:tmp_dir]
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_hash
|
18
|
-
path = persist!
|
19
|
-
|
20
|
-
hash = {
|
21
|
-
filename: @uploaded.original_filename,
|
22
|
-
type: @uploaded.content_type,
|
23
|
-
tempfile_path: path
|
24
|
-
}
|
25
|
-
|
26
|
-
cleanup!
|
27
|
-
|
28
|
-
hash
|
29
|
-
end
|
30
|
-
|
31
|
-
# Returns a ActionDispatch::Http::UploadedFile as if the upload was in the same request.
|
32
|
-
def self.from_hash(hash)
|
33
|
-
suffix = File.extname(hash[:filename])
|
34
|
-
|
35
|
-
# we need to create a Tempfile to make Http::UploadedFile work.
|
36
|
-
tmp = Tempfile.new(["bla", suffix]) # always force file suffix to avoid problems with imagemagick etc.
|
37
|
-
file = File.open(hash[:tempfile_path])# doesn't close automatically :( # fixme: introduce strategy (Tempfile:=>slow, File:=> hopefully less memory footprint)
|
38
|
-
tmp.write(file.read) # DISCUSS: We need Tempfile.new(<File>) to avoid this slow and memory-consuming mechanics.
|
39
|
-
|
40
|
-
file.close # TODO: can we test that?
|
41
|
-
File.unlink(file)
|
42
|
-
|
43
|
-
ActionDispatch::Http::UploadedFile.new(hash.merge(tempfile: tmp))
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
attr_reader :tmp_dir
|
48
|
-
|
49
|
-
# convert Tempfile from Rails upload into persistent "temp" file so it is available in workers.
|
50
|
-
def persist!
|
51
|
-
path = @uploaded.path # original Tempfile path (from Rails).
|
52
|
-
path = path_with_tmp_dir(path)
|
53
|
-
|
54
|
-
path = path + "_trailblazer_upload"
|
55
|
-
|
56
|
-
FileUtils.mv(@uploaded.path, path) # move Rails upload file into persistent `path`.
|
57
|
-
path
|
58
|
-
end
|
59
|
-
|
60
|
-
def path_with_tmp_dir(path)
|
61
|
-
return path unless tmp_dir # if tmp_dir set, create path in it.
|
62
|
-
|
63
|
-
@with_tmp_dir = Tempfile.new(File.basename(path), tmp_dir)
|
64
|
-
@with_tmp_dir.path # use Tempfile to create nested dirs (os-dependent.)
|
65
|
-
end
|
66
|
-
|
67
|
-
def delete!(file)
|
68
|
-
file.close
|
69
|
-
file.unlink # the Rails uploaded file is already unlinked since moved.
|
70
|
-
end
|
71
|
-
|
72
|
-
def cleanup!
|
73
|
-
delete!(@uploaded.tempfile) if @uploaded.respond_to?(:tempfile) # this is Rails' uploaded file, not sure if we need to do that. in 3.2, we don't have UploadedFile#close, yet.
|
74
|
-
delete!(@with_tmp_dir) if @with_tmp_dir # we used that file to create a tmp file path below tmp_dir.
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,175 +0,0 @@
|
|
1
|
-
require "reform"
|
2
|
-
|
3
|
-
module Trailblazer
|
4
|
-
class Operation
|
5
|
-
require "trailblazer/operation/builder"
|
6
|
-
extend Builder # imports ::builder_class and ::build_operation.
|
7
|
-
|
8
|
-
extend Uber::InheritableAttr
|
9
|
-
inheritable_attr :contract_class
|
10
|
-
self.contract_class = Reform::Form.clone
|
11
|
-
|
12
|
-
class << self
|
13
|
-
def run(params, &block) # Endpoint behaviour
|
14
|
-
res, op = build_operation(params).run
|
15
|
-
|
16
|
-
if block_given?
|
17
|
-
yield op if res
|
18
|
-
return op
|
19
|
-
end
|
20
|
-
|
21
|
-
[res, op]
|
22
|
-
end
|
23
|
-
|
24
|
-
# Like ::run, but yield block when invalid.
|
25
|
-
def reject(*args)
|
26
|
-
res, op = run(*args)
|
27
|
-
yield op if res == false
|
28
|
-
op
|
29
|
-
end
|
30
|
-
|
31
|
-
# ::call only returns the Operation instance (or whatever was returned from #validate).
|
32
|
-
# This is useful in tests or in irb, e.g. when using Op as a factory and you already know it's valid.
|
33
|
-
def call(params)
|
34
|
-
build_operation(params, raise_on_invalid: true).run.last
|
35
|
-
end
|
36
|
-
|
37
|
-
def [](*args) # TODO: remove in 1.2.
|
38
|
-
warn "[Trailblazer] Operation[] is deprecated. Please use Operation.() and have a nice day."
|
39
|
-
call(*args)
|
40
|
-
end
|
41
|
-
|
42
|
-
# Runs #setup! but doesn't process the operation.
|
43
|
-
def present(params)
|
44
|
-
build_operation(params)
|
45
|
-
end
|
46
|
-
|
47
|
-
# This is a DSL method. Use ::contract_class and ::contract_class= for the explicit version.
|
48
|
-
# Op.contract #=> returns contract class
|
49
|
-
# Op.contract do .. end # defines contract
|
50
|
-
# Op.contract CommentForm # copies (and subclasses) external contract.
|
51
|
-
# Op.contract CommentForm do .. end # copies and extends contract.
|
52
|
-
def contract(constant=nil, &block)
|
53
|
-
return contract_class unless constant or block_given?
|
54
|
-
|
55
|
-
self.contract_class= Class.new(constant) if constant
|
56
|
-
contract_class.class_eval(&block) if block_given?
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
|
61
|
-
def initialize(params, options={})
|
62
|
-
@options = options
|
63
|
-
@valid = true
|
64
|
-
|
65
|
-
setup!(params) # assign/find the model
|
66
|
-
end
|
67
|
-
|
68
|
-
# Operation.run(body: "Fabulous!") #=> [true, <Comment body: "Fabulous!">]
|
69
|
-
def run
|
70
|
-
process(@params)
|
71
|
-
|
72
|
-
[valid?, self]
|
73
|
-
end
|
74
|
-
|
75
|
-
attr_reader :model
|
76
|
-
|
77
|
-
def errors
|
78
|
-
contract.errors
|
79
|
-
end
|
80
|
-
|
81
|
-
def valid?
|
82
|
-
@valid
|
83
|
-
end
|
84
|
-
|
85
|
-
def contract(*args)
|
86
|
-
contract!(*args)
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
module Setup
|
91
|
-
attr_writer :model
|
92
|
-
|
93
|
-
def setup!(params)
|
94
|
-
params = assign_params!(params)
|
95
|
-
setup_params!(params)
|
96
|
-
build_model!(params)
|
97
|
-
params # TODO: test me.
|
98
|
-
end
|
99
|
-
|
100
|
-
def assign_params!(params)
|
101
|
-
@params = params!(params)
|
102
|
-
end
|
103
|
-
|
104
|
-
# Overwrite #params! if you need to change its structure, by returning a new params object
|
105
|
-
# from this method.
|
106
|
-
# This is helpful if you don't want to change the original via #setup_params!.
|
107
|
-
def params!(params)
|
108
|
-
params
|
109
|
-
end
|
110
|
-
|
111
|
-
def setup_params!(params)
|
112
|
-
end
|
113
|
-
|
114
|
-
def build_model!(*args)
|
115
|
-
assign_model!(*args) # @model = ..
|
116
|
-
setup_model!(*args)
|
117
|
-
end
|
118
|
-
|
119
|
-
def assign_model!(*args)
|
120
|
-
@model = model!(*args)
|
121
|
-
end
|
122
|
-
|
123
|
-
# Implement #model! to find/create your operation model (if required).
|
124
|
-
def model!(params)
|
125
|
-
end
|
126
|
-
|
127
|
-
# Override to add attributes that can be infered from params.
|
128
|
-
def setup_model!(params)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
include Setup
|
132
|
-
|
133
|
-
def validate(params, model=nil, contract_class=nil)
|
134
|
-
contract!(model, contract_class)
|
135
|
-
|
136
|
-
if @valid = validate_contract(params)
|
137
|
-
yield contract if block_given?
|
138
|
-
else
|
139
|
-
raise!(contract)
|
140
|
-
end
|
141
|
-
|
142
|
-
@valid
|
143
|
-
end
|
144
|
-
|
145
|
-
def validate_contract(params)
|
146
|
-
contract.validate(params)
|
147
|
-
end
|
148
|
-
|
149
|
-
def invalid!(result=self)
|
150
|
-
@valid = false
|
151
|
-
result
|
152
|
-
end
|
153
|
-
|
154
|
-
# When using Op.(), an invalid contract will raise an exception.
|
155
|
-
def raise!(contract)
|
156
|
-
raise InvalidContract.new(contract.errors.to_s) if @options[:raise_on_invalid]
|
157
|
-
end
|
158
|
-
|
159
|
-
# Instantiate the contract, either by using the user's contract passed into #validate
|
160
|
-
# or infer the Operation contract.
|
161
|
-
def contract_for(model=nil, contract_class=nil)
|
162
|
-
model ||= self.model
|
163
|
-
contract_class ||= self.class.contract_class
|
164
|
-
|
165
|
-
contract_class.new(model)
|
166
|
-
end
|
167
|
-
|
168
|
-
def contract!(*args)
|
169
|
-
@contract ||= contract_for(*args)
|
170
|
-
end
|
171
|
-
|
172
|
-
class InvalidContract < RuntimeError
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|