trailblazer 0.2.2 → 0.3.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 +4 -4
- data/.travis.yml +1 -3
- data/CHANGES.md +33 -0
- data/Gemfile +4 -1
- data/README.md +171 -166
- data/Rakefile +10 -2
- data/gemfiles/Gemfile.rails.lock +42 -11
- data/lib/trailblazer/autoloading.rb +4 -3
- data/lib/trailblazer/endpoint.rb +40 -0
- data/lib/trailblazer/operation.rb +15 -8
- data/lib/trailblazer/operation/collection.rb +7 -0
- data/lib/trailblazer/operation/controller.rb +30 -22
- data/lib/trailblazer/operation/controller/active_record.rb +6 -2
- data/lib/trailblazer/operation/crud.rb +3 -5
- data/lib/trailblazer/operation/dispatch.rb +29 -0
- data/lib/trailblazer/operation/representer.rb +41 -5
- data/lib/trailblazer/operation/responder.rb +2 -2
- data/lib/trailblazer/operation/uploaded_file.rb +4 -4
- data/lib/trailblazer/operation/worker.rb +8 -10
- data/lib/trailblazer/rails/railtie.rb +10 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +56 -0
- data/test/crud_test.rb +23 -1
- data/test/dispatch_test.rb +63 -0
- data/test/operation_test.rb +84 -125
- data/test/rails/controller_test.rb +51 -0
- data/test/rails/endpoint_test.rb +86 -0
- data/test/rails/fake_app/cells.rb +2 -2
- data/test/rails/fake_app/config.rb +1 -1
- data/test/rails/fake_app/controllers.rb +27 -0
- data/test/rails/fake_app/models.rb +10 -1
- data/test/rails/fake_app/rails_app.rb +15 -1
- data/test/rails/fake_app/song/operations.rb +38 -2
- data/test/rails/fake_app/views/bands/index.html.erb +1 -0
- data/test/rails/fake_app/views/songs/another_view.html.erb +2 -0
- data/test/representer_test.rb +126 -0
- data/test/responder_test.rb +2 -4
- data/test/rollback_test.rb +47 -0
- data/test/test_helper.rb +43 -1
- data/test/uploaded_file_test.rb +4 -4
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +7 -3
- metadata +68 -29
data/Rakefile
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
task :default => [:
|
4
|
+
task :default => [:build]
|
5
|
+
default_task = Rake::Task[:build]
|
6
|
+
|
5
7
|
Rake::TestTask.new(:test) do |test|
|
6
8
|
test.libs << 'test'
|
7
9
|
test.test_files = FileList['test/*_test.rb']
|
8
10
|
test.verbose = true
|
9
11
|
end
|
10
12
|
|
13
|
+
# this how the rails test must be run: BUNDLE_GEMFILE=gemfiles/Gemfile.rails bundle exec rake rails
|
11
14
|
Rake::TestTask.new(:rails) do |test|
|
12
15
|
test.libs << 'test/rails'
|
13
16
|
test.test_files = FileList['test/rails/*_test.rb']
|
14
17
|
test.verbose = true
|
15
|
-
end
|
18
|
+
end
|
19
|
+
|
20
|
+
rails_task = Rake::Task["rails"]
|
21
|
+
test_task = Rake::Task["test"]
|
22
|
+
default_task.enhance { test_task.invoke }
|
23
|
+
default_task.enhance { rails_task.invoke }
|
data/gemfiles/Gemfile.rails.lock
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../
|
3
3
|
specs:
|
4
|
-
trailblazer (0.
|
4
|
+
trailblazer (0.2.2)
|
5
5
|
actionpack (>= 3.0.0)
|
6
|
-
reform (
|
7
|
-
representable (>= 2.1.1, < 2.2.0)
|
6
|
+
reform (>= 1.2.0)
|
8
7
|
uber (>= 0.0.10)
|
9
8
|
|
10
9
|
GEM
|
11
10
|
remote: http://rubygems.org/
|
12
11
|
specs:
|
12
|
+
actionmailer (4.1.1)
|
13
|
+
actionpack (= 4.1.1)
|
14
|
+
actionview (= 4.1.1)
|
15
|
+
mail (~> 2.5.4)
|
13
16
|
actionpack (4.1.1)
|
14
17
|
actionview (= 4.1.1)
|
15
18
|
activesupport (= 4.1.1)
|
@@ -44,14 +47,29 @@ GEM
|
|
44
47
|
hitimes (1.2.2)
|
45
48
|
i18n (0.6.11)
|
46
49
|
json (1.8.1)
|
47
|
-
|
50
|
+
mail (2.5.4)
|
51
|
+
mime-types (~> 1.16)
|
52
|
+
treetop (~> 1.4.8)
|
53
|
+
mime-types (1.25.1)
|
54
|
+
mini_portile (0.6.2)
|
48
55
|
minitest (5.4.2)
|
49
|
-
multi_json (1.
|
50
|
-
nokogiri (1.6.
|
56
|
+
multi_json (1.11.0)
|
57
|
+
nokogiri (1.6.6.2)
|
51
58
|
mini_portile (~> 0.6.0)
|
59
|
+
polyglot (0.3.5)
|
52
60
|
rack (1.5.2)
|
53
61
|
rack-test (0.6.2)
|
54
62
|
rack (>= 1.0)
|
63
|
+
rails (4.1.1)
|
64
|
+
actionmailer (= 4.1.1)
|
65
|
+
actionpack (= 4.1.1)
|
66
|
+
actionview (= 4.1.1)
|
67
|
+
activemodel (= 4.1.1)
|
68
|
+
activerecord (= 4.1.1)
|
69
|
+
activesupport (= 4.1.1)
|
70
|
+
bundler (>= 1.3.0, < 2.0)
|
71
|
+
railties (= 4.1.1)
|
72
|
+
sprockets-rails (~> 2.0)
|
55
73
|
railties (4.1.1)
|
56
74
|
actionpack (= 4.1.1)
|
57
75
|
activesupport (= 4.1.1)
|
@@ -61,39 +79,52 @@ GEM
|
|
61
79
|
redis (3.1.0)
|
62
80
|
redis-namespace (1.5.1)
|
63
81
|
redis (~> 3.0, >= 3.0.4)
|
64
|
-
reform (1.2.
|
82
|
+
reform (1.2.6)
|
65
83
|
activemodel
|
66
84
|
disposable (~> 0.0.5)
|
67
85
|
representable (~> 2.1.0)
|
68
|
-
uber (~> 0.0.
|
69
|
-
representable (2.1.
|
86
|
+
uber (~> 0.0.11)
|
87
|
+
representable (2.1.8)
|
70
88
|
multi_json
|
71
89
|
nokogiri
|
72
90
|
uber (~> 0.0.7)
|
91
|
+
responders (1.1.2)
|
92
|
+
railties (>= 3.2, < 4.2)
|
73
93
|
sidekiq (3.1.4)
|
74
94
|
celluloid (>= 0.15.2)
|
75
95
|
connection_pool (>= 2.0.0)
|
76
96
|
json
|
77
97
|
redis (>= 3.0.6)
|
78
98
|
redis-namespace (>= 1.3.1)
|
99
|
+
sprockets (3.1.0)
|
100
|
+
rack (~> 1.0)
|
101
|
+
sprockets-rails (2.3.1)
|
102
|
+
actionpack (>= 3.0)
|
103
|
+
activesupport (>= 3.0)
|
104
|
+
sprockets (>= 2.8, < 4.0)
|
79
105
|
sqlite3 (1.3.9)
|
80
106
|
thor (0.19.1)
|
81
107
|
thread_safe (0.3.4)
|
82
108
|
timers (4.0.1)
|
83
109
|
hitimes
|
110
|
+
treetop (1.4.15)
|
111
|
+
polyglot
|
112
|
+
polyglot (>= 0.3.1)
|
84
113
|
tzinfo (1.2.2)
|
85
114
|
thread_safe (~> 0.1)
|
86
|
-
uber (0.0.
|
115
|
+
uber (0.0.13)
|
87
116
|
|
88
117
|
PLATFORMS
|
89
118
|
ruby
|
90
119
|
|
91
120
|
DEPENDENCIES
|
92
121
|
activerecord
|
93
|
-
bundler
|
122
|
+
bundler
|
94
123
|
minitest
|
124
|
+
rails
|
95
125
|
railties
|
96
126
|
rake
|
127
|
+
responders
|
97
128
|
sidekiq (~> 3.1.0)
|
98
129
|
sqlite3
|
99
130
|
trailblazer!
|
@@ -1,5 +1,6 @@
|
|
1
1
|
Trailblazer::Operation.class_eval do
|
2
|
-
autoload :Controller,
|
3
|
-
autoload :Responder,
|
4
|
-
autoload :CRUD,
|
2
|
+
autoload :Controller, "trailblazer/operation/controller"
|
3
|
+
autoload :Responder, "trailblazer/operation/responder"
|
4
|
+
autoload :CRUD, "trailblazer/operation/crud"
|
5
|
+
autoload :Collection, "trailblazer/operation/collection"
|
5
6
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
# To be used in Lotus, Roda, Rails, etc.
|
3
|
+
class Endpoint
|
4
|
+
def initialize(controller, operation_class, params, request, config)
|
5
|
+
@controller = controller
|
6
|
+
@operation_class = operation_class
|
7
|
+
@params = params
|
8
|
+
@request = request
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
@controller.send(:process_params!, params) # FIXME.
|
14
|
+
|
15
|
+
document_body! if document_request?
|
16
|
+
|
17
|
+
res, operation = yield # Create.run(params)
|
18
|
+
@controller.send(:setup_operation_instance_variables!, operation)
|
19
|
+
|
20
|
+
[res, operation] # DISCUSS: do we need result here? or can we just go pick op.valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
attr_reader :params, :operation_class, :request, :controller
|
25
|
+
|
26
|
+
def document_request?
|
27
|
+
# request.format == :html
|
28
|
+
@config[:document_formats][request.format.to_sym]
|
29
|
+
end
|
30
|
+
|
31
|
+
def document_body!
|
32
|
+
# this is what happens:
|
33
|
+
# respond_with Comment::Update::JSON.run(params.merge(comment: request.body.string))
|
34
|
+
concept_name = operation_class.model_class.to_s.underscore # this could be renamed to ::concept_class soon.
|
35
|
+
request_body = request.body.respond_to?(:string) ? request.body.string : request.body.read
|
36
|
+
|
37
|
+
params.merge!(concept_name => request_body)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -33,11 +33,11 @@ module Trailblazer
|
|
33
33
|
# ::call only returns the Operation instance (or whatever was returned from #validate).
|
34
34
|
# This is useful in tests or in irb, e.g. when using Op as a factory and you already know it's valid.
|
35
35
|
def call(*params)
|
36
|
-
build_operation_class(*params).new(:
|
36
|
+
build_operation_class(*params).new(raise_on_invalid: true).run(*params).last
|
37
37
|
end
|
38
|
-
alias_method :[], :call
|
38
|
+
alias_method :[], :call # TODO: deprecate #[] in favor of .().
|
39
39
|
|
40
|
-
# Runs #
|
40
|
+
# Runs #setup! and returns the form object.
|
41
41
|
def present(*params)
|
42
42
|
build_operation_class(*params).new.present(*params)
|
43
43
|
end
|
@@ -56,7 +56,6 @@ module Trailblazer
|
|
56
56
|
|
57
57
|
def initialize(options={})
|
58
58
|
@valid = true
|
59
|
-
# DISCUSS: use reverse_merge here?
|
60
59
|
@raise_on_invalid = options[:raise_on_invalid] || false
|
61
60
|
end
|
62
61
|
|
@@ -64,7 +63,9 @@ module Trailblazer
|
|
64
63
|
def run(*params)
|
65
64
|
setup!(*params) # where do we assign/find the model?
|
66
65
|
|
67
|
-
|
66
|
+
process(*params)
|
67
|
+
|
68
|
+
[valid?, self]
|
68
69
|
end
|
69
70
|
|
70
71
|
def present(*params)
|
@@ -75,6 +76,7 @@ module Trailblazer
|
|
75
76
|
end
|
76
77
|
|
77
78
|
attr_reader :contract
|
79
|
+
attr_reader :model
|
78
80
|
|
79
81
|
def errors
|
80
82
|
contract.errors
|
@@ -103,16 +105,20 @@ module Trailblazer
|
|
103
105
|
def setup_params!(*params)
|
104
106
|
end
|
105
107
|
|
106
|
-
def validate(params, model, contract_class=nil)
|
108
|
+
def validate(params, model, contract_class=nil)
|
107
109
|
@contract = contract_for(contract_class, model)
|
108
110
|
|
109
|
-
if @valid =
|
111
|
+
if @valid = validate_contract(params)
|
110
112
|
yield contract if block_given?
|
111
113
|
else
|
112
114
|
raise!(contract)
|
113
115
|
end
|
114
116
|
|
115
|
-
|
117
|
+
@valid
|
118
|
+
end
|
119
|
+
|
120
|
+
def validate_contract(params)
|
121
|
+
contract.validate(params)
|
116
122
|
end
|
117
123
|
|
118
124
|
def invalid!(result=self)
|
@@ -137,6 +143,7 @@ module Trailblazer
|
|
137
143
|
end
|
138
144
|
|
139
145
|
require 'trailblazer/operation/crud'
|
146
|
+
require "trailblazer/operation/dispatch"
|
140
147
|
|
141
148
|
# run
|
142
149
|
# setup
|
@@ -1,6 +1,6 @@
|
|
1
|
-
|
2
|
-
# TODO: test me.
|
1
|
+
require "trailblazer/endpoint"
|
3
2
|
|
3
|
+
module Trailblazer::Operation::Controller
|
4
4
|
private
|
5
5
|
def form(operation_class, params=self.params) # consider private.
|
6
6
|
process_params!(params)
|
@@ -23,6 +23,14 @@ private
|
|
23
23
|
# TODO: implement respond(present: true)
|
24
24
|
end
|
25
25
|
|
26
|
+
def collection(operation_class, params=self.params)
|
27
|
+
# TODO: merge with #present.
|
28
|
+
res, op = operation!(operation_class, params) { [true, operation_class.present(params)] }
|
29
|
+
@collection = @model
|
30
|
+
|
31
|
+
yield op if block_given?
|
32
|
+
end
|
33
|
+
|
26
34
|
# full-on Op[]
|
27
35
|
# Note: this is not documented on purpose as this concept is experimental. I don't like it too much and prefer
|
28
36
|
# returns in the valid block.
|
@@ -47,37 +55,37 @@ private
|
|
47
55
|
end
|
48
56
|
|
49
57
|
# The block passed to #respond is always run, regardless of the validity result.
|
50
|
-
def respond(operation_class, params=self.params, &block)
|
58
|
+
def respond(operation_class, params=self.params, respond_options = {}, &block)
|
51
59
|
res, op = operation!(operation_class, params) { operation_class.run(params) }
|
52
60
|
|
53
|
-
return respond_with op if not block_given?
|
54
|
-
respond_with op, &Proc.new { |formats| block.call(op, formats) } if block_given?
|
61
|
+
return respond_with op, respond_options if not block_given?
|
62
|
+
respond_with op, respond_options, &Proc.new { |formats| block.call(op, formats) } if block_given?
|
55
63
|
end
|
56
64
|
|
57
65
|
def process_params!(params)
|
58
66
|
end
|
59
67
|
|
60
68
|
# Normalizes parameters and invokes the operation (including its builders).
|
61
|
-
def operation!(operation_class, params)
|
62
|
-
|
63
|
-
|
64
|
-
unless request.format == :html
|
65
|
-
# this is what happens:
|
66
|
-
# respond_with Comment::Update::JSON.run(params.merge(comment: request.body.string))
|
67
|
-
concept_name = operation_class.model_class.to_s.underscore # this could be renamed to ::concept_class soon.
|
68
|
-
request_body = request.body.respond_to?(:string) ? request.body.string : request.body.read
|
69
|
-
|
70
|
-
params.merge!(concept_name => request_body)
|
71
|
-
end
|
69
|
+
def operation!(operation_class, params, &block)
|
70
|
+
Trailblazer::Endpoint.new(self, operation_class, params, request, self.class._operation).(&block)
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
73
|
+
def setup_operation_instance_variables!(operation)
|
74
|
+
@operation = operation
|
75
|
+
@form = operation.contract
|
76
|
+
@model = operation.model
|
77
|
+
end
|
75
78
|
|
76
|
-
|
79
|
+
def self.included(includer)
|
80
|
+
includer.extend Uber::InheritableAttr
|
81
|
+
includer.inheritable_attr :_operation
|
82
|
+
includer._operation = {document_formats: {}}
|
83
|
+
includer.extend ClassMethods
|
77
84
|
end
|
78
85
|
|
79
|
-
|
80
|
-
|
81
|
-
|
86
|
+
module ClassMethods
|
87
|
+
def operation(options)
|
88
|
+
_operation[:document_formats][options[:document_formats]] = true
|
89
|
+
end
|
82
90
|
end
|
83
91
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
# Assigns an additional instance variable for +@model+ named after the model's table name (e.g. @comment).
|
2
2
|
module Trailblazer::Operation::Controller::ActiveRecord
|
3
3
|
private
|
4
|
-
def setup_operation_instance_variables!
|
4
|
+
def setup_operation_instance_variables!(operation)
|
5
5
|
super
|
6
6
|
instance_variable_set(:"@#{operation_model_name}", @model)
|
7
7
|
end
|
8
8
|
|
9
9
|
def operation_model_name
|
10
|
-
|
10
|
+
# set the right variable name if collection
|
11
|
+
if @operation.is_a?(Trailblazer::Operation::Collection)
|
12
|
+
return @model.model.table_name.split(".").last
|
13
|
+
end
|
14
|
+
@model.class.table_name.split(".").last.singularize
|
11
15
|
end
|
12
16
|
end
|
@@ -3,8 +3,6 @@ module Trailblazer
|
|
3
3
|
# The CRUD module will automatically create/find models for the configured +action+.
|
4
4
|
# It adds a public +Operation#model+ reader to access the model (after performing).
|
5
5
|
module CRUD
|
6
|
-
attr_reader :model
|
7
|
-
|
8
6
|
module Included
|
9
7
|
def included(base)
|
10
8
|
base.extend Uber::InheritableAttr
|
@@ -39,8 +37,8 @@ module Trailblazer
|
|
39
37
|
|
40
38
|
|
41
39
|
# #validate no longer accepts a model since this module instantiates it for you.
|
42
|
-
def validate(params, *args)
|
43
|
-
super(params,
|
40
|
+
def validate(params, model=self.model, *args)
|
41
|
+
super(params, model, *args)
|
44
42
|
end
|
45
43
|
|
46
44
|
private
|
@@ -81,4 +79,4 @@ module Trailblazer
|
|
81
79
|
end
|
82
80
|
end
|
83
81
|
end
|
84
|
-
end
|
82
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "disposable/callback"
|
2
|
+
|
3
|
+
module Trailblazer::Operation::Dispatch
|
4
|
+
def dispatch!(name=:default)
|
5
|
+
group = self.class.callbacks[name].new(contract)
|
6
|
+
group.(context: self)
|
7
|
+
|
8
|
+
invocations[name] = group
|
9
|
+
end
|
10
|
+
|
11
|
+
def invocations
|
12
|
+
@invocations ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
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
|
+
|
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
|
28
|
+
end
|
29
|
+
end
|
@@ -1,18 +1,54 @@
|
|
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.
|
1
8
|
module Trailblazer::Operation::Representer
|
2
9
|
def self.included(base)
|
3
10
|
base.extend Uber::InheritableAttr
|
4
|
-
base.inheritable_attr :
|
5
|
-
# TODO: allow representer without contract?!
|
11
|
+
base.inheritable_attr :_representer_class
|
6
12
|
base.extend ClassMethods
|
7
13
|
end
|
8
14
|
|
9
15
|
module ClassMethods
|
10
16
|
def representer(&block)
|
11
|
-
|
17
|
+
representer_class.class_eval(&block)
|
12
18
|
end
|
13
19
|
|
14
|
-
def
|
15
|
-
|
20
|
+
def representer_class
|
21
|
+
self._representer_class ||= infer_representer_class
|
22
|
+
end
|
23
|
+
|
24
|
+
def representer_class=(constant)
|
25
|
+
self._representer_class = constant
|
26
|
+
end
|
27
|
+
|
28
|
+
def infer_representer_class
|
29
|
+
Disposable::Twin::Schema.from(contract_class,
|
30
|
+
include: [Representable::JSON],
|
31
|
+
options_from: :deserializer, # use :instance etc. in deserializer.
|
32
|
+
superclass: Representable::Decorator,
|
33
|
+
representer_from: lambda { |inline| inline.representer_class },
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
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)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
module Rendering
|
49
|
+
def to_json(*)
|
50
|
+
self.class.representer_class.new(contract).to_json
|
16
51
|
end
|
17
52
|
end
|
53
|
+
include Rendering
|
18
54
|
end
|