trailblazer 0.0.1 → 0.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 +7 -0
- data/.gitignore +1 -1
- data/.travis.yml +5 -0
- data/CHANGES.md +3 -0
- data/Gemfile +4 -0
- data/README.md +417 -16
- data/Rakefile +14 -0
- data/THOUGHTS +12 -0
- data/TODO.md +6 -0
- data/doc/Trb-The-Stack.png +0 -0
- data/doc/trb.jpg +0 -0
- data/gemfiles/Gemfile.rails +7 -0
- data/gemfiles/Gemfile.rails.lock +99 -0
- data/lib/trailblazer.rb +2 -0
- data/lib/trailblazer/autoloading.rb +5 -0
- data/lib/trailblazer/operation.rb +124 -0
- data/lib/trailblazer/operation/controller.rb +76 -0
- data/lib/trailblazer/operation/crud.rb +61 -0
- data/lib/trailblazer/operation/representer.rb +18 -0
- data/lib/trailblazer/operation/responder.rb +24 -0
- data/lib/trailblazer/operation/uploaded_file.rb +77 -0
- data/lib/trailblazer/operation/worker.rb +96 -0
- data/lib/trailblazer/version.rb +1 -1
- data/test/crud_test.rb +115 -0
- data/test/fixtures/apotomo.png +0 -0
- data/test/fixtures/cells.png +0 -0
- data/test/operation_test.rb +334 -0
- data/test/rails/controller_test.rb +175 -0
- data/test/rails/fake_app/app-cells/.gitkeep +0 -0
- data/test/rails/fake_app/cells.rb +21 -0
- data/test/rails/fake_app/config.rb +3 -0
- data/test/rails/fake_app/controllers.rb +101 -0
- data/test/rails/fake_app/models.rb +13 -0
- data/test/rails/fake_app/rails_app.rb +57 -0
- data/test/rails/fake_app/song/operations.rb +63 -0
- data/test/rails/fake_app/views/bands/show.html.erb +1 -0
- data/test/rails/fake_app/views/songs/new.html.erb +1 -0
- data/test/rails/test_helper.rb +4 -0
- data/test/responder_test.rb +77 -0
- data/test/test_helper.rb +15 -0
- data/test/uploaded_file_test.rb +85 -0
- data/test/worker_test.rb +116 -0
- data/trailblazer.gemspec +10 -2
- metadata +160 -23
data/Rakefile
CHANGED
@@ -1 +1,15 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
task :default => [:test]
|
5
|
+
Rake::TestTask.new(:test) do |test|
|
6
|
+
test.libs << 'test'
|
7
|
+
test.test_files = FileList['test/*_test.rb']
|
8
|
+
test.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:rails) do |test|
|
12
|
+
test.libs << 'test/rails'
|
13
|
+
test.test_files = FileList['test/rails/*_test.rb']
|
14
|
+
test.verbose = true
|
15
|
+
end
|
data/THOUGHTS
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
by adding new abstraction layers
|
2
|
+
* automatical think about interfaces
|
3
|
+
* testability
|
4
|
+
* more optional features, e.g. by using a representer you get HAL etc for free once included. how would that work with rabl? or easy nested forms with reform
|
5
|
+
|
6
|
+
the less associations in models the better
|
7
|
+
|
8
|
+
hi-level APIs: Invoice::Flow instead of creating I:Item out-of-sync
|
9
|
+
if your factories have 5 lines or more, your API is wrong, (Invoice::Flow)
|
10
|
+
|
11
|
+
|
12
|
+
i don't wanna show anti-patterns, i wanna show how you can make things work.
|
data/TODO.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
* allow `Op[{body: "Great!"}, {additional: true}]` to save merge.
|
2
|
+
* make `Op[]` not require wrap like `comment: {}`
|
3
|
+
* in tests, make Op[].model return the reloaded model!
|
4
|
+
|
5
|
+
* don't pass contract in validate, we have #contract.
|
6
|
+
* abstract validate/success/fail into methods to make it easily overrideable.
|
Binary file
|
data/doc/trb.jpg
ADDED
Binary file
|
@@ -0,0 +1,99 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../
|
3
|
+
specs:
|
4
|
+
trailblazer (0.0.1)
|
5
|
+
actionpack (>= 3.0.0)
|
6
|
+
reform (= 1.2.0.beta1)
|
7
|
+
representable (>= 2.1.1, < 2.2.0)
|
8
|
+
uber (>= 0.0.10)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
actionpack (4.1.1)
|
14
|
+
actionview (= 4.1.1)
|
15
|
+
activesupport (= 4.1.1)
|
16
|
+
rack (~> 1.5.2)
|
17
|
+
rack-test (~> 0.6.2)
|
18
|
+
actionview (4.1.1)
|
19
|
+
activesupport (= 4.1.1)
|
20
|
+
builder (~> 3.1)
|
21
|
+
erubis (~> 2.7.0)
|
22
|
+
activemodel (4.1.1)
|
23
|
+
activesupport (= 4.1.1)
|
24
|
+
builder (~> 3.1)
|
25
|
+
activerecord (4.1.1)
|
26
|
+
activemodel (= 4.1.1)
|
27
|
+
activesupport (= 4.1.1)
|
28
|
+
arel (~> 5.0.0)
|
29
|
+
activesupport (4.1.1)
|
30
|
+
i18n (~> 0.6, >= 0.6.9)
|
31
|
+
json (~> 1.7, >= 1.7.7)
|
32
|
+
minitest (~> 5.1)
|
33
|
+
thread_safe (~> 0.1)
|
34
|
+
tzinfo (~> 1.1)
|
35
|
+
arel (5.0.1.20140414130214)
|
36
|
+
builder (3.2.2)
|
37
|
+
celluloid (0.16.0)
|
38
|
+
timers (~> 4.0.0)
|
39
|
+
connection_pool (2.0.0)
|
40
|
+
disposable (0.0.8)
|
41
|
+
representable (~> 2.0)
|
42
|
+
uber
|
43
|
+
erubis (2.7.0)
|
44
|
+
hitimes (1.2.2)
|
45
|
+
i18n (0.6.11)
|
46
|
+
json (1.8.1)
|
47
|
+
mini_portile (0.6.0)
|
48
|
+
minitest (5.4.2)
|
49
|
+
multi_json (1.10.1)
|
50
|
+
nokogiri (1.6.3.1)
|
51
|
+
mini_portile (= 0.6.0)
|
52
|
+
rack (1.5.2)
|
53
|
+
rack-test (0.6.2)
|
54
|
+
rack (>= 1.0)
|
55
|
+
railties (4.1.1)
|
56
|
+
actionpack (= 4.1.1)
|
57
|
+
activesupport (= 4.1.1)
|
58
|
+
rake (>= 0.8.7)
|
59
|
+
thor (>= 0.18.1, < 2.0)
|
60
|
+
rake (10.3.2)
|
61
|
+
redis (3.1.0)
|
62
|
+
redis-namespace (1.5.1)
|
63
|
+
redis (~> 3.0, >= 3.0.4)
|
64
|
+
reform (1.2.0.beta1)
|
65
|
+
activemodel
|
66
|
+
disposable (~> 0.0.5)
|
67
|
+
representable (~> 2.1.0)
|
68
|
+
uber (~> 0.0.8)
|
69
|
+
representable (2.1.3)
|
70
|
+
multi_json
|
71
|
+
nokogiri
|
72
|
+
uber (~> 0.0.7)
|
73
|
+
sidekiq (3.1.4)
|
74
|
+
celluloid (>= 0.15.2)
|
75
|
+
connection_pool (>= 2.0.0)
|
76
|
+
json
|
77
|
+
redis (>= 3.0.6)
|
78
|
+
redis-namespace (>= 1.3.1)
|
79
|
+
sqlite3 (1.3.9)
|
80
|
+
thor (0.19.1)
|
81
|
+
thread_safe (0.3.4)
|
82
|
+
timers (4.0.1)
|
83
|
+
hitimes
|
84
|
+
tzinfo (1.2.2)
|
85
|
+
thread_safe (~> 0.1)
|
86
|
+
uber (0.0.10)
|
87
|
+
|
88
|
+
PLATFORMS
|
89
|
+
ruby
|
90
|
+
|
91
|
+
DEPENDENCIES
|
92
|
+
activerecord
|
93
|
+
bundler (~> 1.3)
|
94
|
+
minitest
|
95
|
+
railties
|
96
|
+
rake
|
97
|
+
sidekiq (~> 3.1.0)
|
98
|
+
sqlite3
|
99
|
+
trailblazer!
|
data/lib/trailblazer.rb
CHANGED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'uber/builder'
|
2
|
+
# TODO: extract all reform-contract stuff into optional module.
|
3
|
+
require 'reform'
|
4
|
+
|
5
|
+
# TODO: OP[] without wrapper, OP.run with (eg for params)
|
6
|
+
|
7
|
+
module Trailblazer
|
8
|
+
class Operation
|
9
|
+
extend Uber::InheritableAttr
|
10
|
+
inheritable_attr :contract_class
|
11
|
+
self.contract_class = Reform::Form.clone
|
12
|
+
self.contract_class.class_eval do
|
13
|
+
def self.name # FIXME: don't use ActiveModel::Validations in Reform, it sucks.
|
14
|
+
# for whatever reason, validations climb up the inheritance tree and require _every_ class to have a name (4.1).
|
15
|
+
"Reform::Form"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def run(*params, &block) # Endpoint behaviour
|
21
|
+
res, op = build_operation_class(*params).new.run(*params)
|
22
|
+
|
23
|
+
if block_given?
|
24
|
+
yield op if res
|
25
|
+
return op
|
26
|
+
end
|
27
|
+
|
28
|
+
[res, 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_class(*params).new(:raise_on_invalid => true).run(*params).last
|
35
|
+
end
|
36
|
+
alias_method :[], :call
|
37
|
+
|
38
|
+
# Runs #process without validate and returns the form object.
|
39
|
+
def present(*params)
|
40
|
+
build_operation_class(*params).new(:validate => false).run(*params).last
|
41
|
+
end
|
42
|
+
|
43
|
+
def contract(&block)
|
44
|
+
contract_class.class_eval(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
def build_operation_class(*params)
|
50
|
+
class_builder.call(*params) # Uber::Builder::class_builder
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
include Uber::Builder
|
55
|
+
|
56
|
+
|
57
|
+
def initialize(options={})
|
58
|
+
@valid = true
|
59
|
+
# DISCUSS: use reverse_merge here?
|
60
|
+
@validate = options[:validate] == false ? false : true
|
61
|
+
@raise_on_invalid = options[:raise_on_invalid] || false
|
62
|
+
end
|
63
|
+
|
64
|
+
# Operation.run(body: "Fabulous!") #=> [true, <Comment body: "Fabulous!">]
|
65
|
+
def run(*params)
|
66
|
+
setup!(*params) # where do we assign/find the model?
|
67
|
+
|
68
|
+
[process(*params), valid?].reverse
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_reader :contract
|
72
|
+
|
73
|
+
def valid?
|
74
|
+
@valid
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def setup!(*params)
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate(params, model, contract_class=nil) # NOT to be overridden?!! it creates Result for us.
|
83
|
+
|
84
|
+
@contract = contract_for(contract_class, model)
|
85
|
+
|
86
|
+
return self unless @validate # Op.contract will return here.
|
87
|
+
|
88
|
+
if @valid = contract.validate(params)
|
89
|
+
yield contract if block_given?
|
90
|
+
else
|
91
|
+
raise!(contract)
|
92
|
+
end
|
93
|
+
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def invalid!(result=self)
|
98
|
+
@valid = false
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
# When using Op::[], an invalid contract will raise an exception.
|
103
|
+
def raise!(contract)
|
104
|
+
raise InvalidContract.new(contract.errors.to_s) if @raise_on_invalid
|
105
|
+
end
|
106
|
+
|
107
|
+
# Instantiate the contract, either by using the user's contract passed into #validate
|
108
|
+
# or infer the Operation contract.
|
109
|
+
def contract_for(contract_class, *model)
|
110
|
+
(contract_class || self.class.contract_class).new(*model)
|
111
|
+
end
|
112
|
+
|
113
|
+
class InvalidContract < RuntimeError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
require 'trailblazer/operation/crud'
|
119
|
+
|
120
|
+
# run
|
121
|
+
# setup
|
122
|
+
# process
|
123
|
+
# contract
|
124
|
+
# validate
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Trailblazer::Operation::Controller
|
2
|
+
# TODO: test me.
|
3
|
+
|
4
|
+
private
|
5
|
+
def form(operation_class, params=self.params) # consider private.
|
6
|
+
process_params!(params)
|
7
|
+
|
8
|
+
@operation = operation_class.new(:validate => false).run(params).last # FIXME: make that available via Operation.
|
9
|
+
@form = @operation.contract
|
10
|
+
@model = @operation.model
|
11
|
+
|
12
|
+
yield @operation if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
# Doesn't run #validate.
|
16
|
+
# TODO: allow only_setup.
|
17
|
+
# TODO: dependency to CRUD (::model_name)
|
18
|
+
def present(operation_class, params=self.params)
|
19
|
+
res, op = operation!(operation_class, params) { [true, operation_class.present(params)] }
|
20
|
+
|
21
|
+
yield op if block_given?
|
22
|
+
respond_with op
|
23
|
+
end
|
24
|
+
|
25
|
+
# full-on Op[]
|
26
|
+
# Note: this is not documented on purpose as this concept is experimental. I don't like it too much and prefer
|
27
|
+
# returns in the valid block.
|
28
|
+
class Else
|
29
|
+
def initialize(op, run)
|
30
|
+
@op = op
|
31
|
+
@run = run
|
32
|
+
end
|
33
|
+
|
34
|
+
def else
|
35
|
+
yield @op if @run
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Endpoint::Invocation
|
40
|
+
def run(operation_class, params=self.params, &block)
|
41
|
+
res, op = operation!(operation_class, params) { operation_class.run(params) }
|
42
|
+
|
43
|
+
yield op if res and block_given?
|
44
|
+
|
45
|
+
Else.new(op, !res)
|
46
|
+
end
|
47
|
+
|
48
|
+
# The block passed to #respond is always run, regardless of the validity result.
|
49
|
+
def respond(operation_class, params=self.params, &block)
|
50
|
+
res, op = operation!(operation_class, params) { operation_class.run(params) }
|
51
|
+
|
52
|
+
return respond_with op if not block_given?
|
53
|
+
respond_with op, &Proc.new { |formats| block.call(op, formats) } if block_given?
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_params!(params)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Normalizes parameters and invokes the operation (including its builders).
|
60
|
+
def operation!(operation_class, params)
|
61
|
+
process_params!(params)
|
62
|
+
|
63
|
+
unless request.format == :html
|
64
|
+
# this is what happens:
|
65
|
+
# respond_with Comment::Update::JSON.run(params.merge(comment: request.body.string))
|
66
|
+
concept_name = operation_class.model_class # this could be renamed to ::concept_class soon.
|
67
|
+
params.merge!(concept_name => request.body.string)
|
68
|
+
end
|
69
|
+
|
70
|
+
res, @operation = yield # Create.run(params)
|
71
|
+
@form = @operation.contract
|
72
|
+
@model = @operation.model
|
73
|
+
|
74
|
+
[res, @operation] # DISCUSS: do we need result here? or can we just go pick op.valid?
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Operation
|
3
|
+
# The CRUD module will automatically create/find models for the configured +action+.
|
4
|
+
# It adds a public +Operation#model+ reader to access the model (after performing).
|
5
|
+
module CRUD
|
6
|
+
attr_reader :model
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend Uber::InheritableAttr
|
10
|
+
base.inheritable_attr :config
|
11
|
+
base.config = {}
|
12
|
+
|
13
|
+
base.extend ClassMethods
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def model(name, action=nil)
|
18
|
+
self.config[:model] = name
|
19
|
+
action(action) if action # coolest line ever.
|
20
|
+
end
|
21
|
+
|
22
|
+
def action(name)
|
23
|
+
self.config[:action] = name
|
24
|
+
end
|
25
|
+
|
26
|
+
def action_name # considered private.
|
27
|
+
self.config[:action] or :create
|
28
|
+
end
|
29
|
+
|
30
|
+
def model_class # considered private.
|
31
|
+
self.config[:model] or raise "[Trailblazer] You didn't call Operation::model." # TODO: infer model name.
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# #validate no longer accepts a model since this module instantiates it for you.
|
37
|
+
def validate(params, *args)
|
38
|
+
super(params, @model, *args)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def setup!(params)
|
43
|
+
@model ||= instantiate_model(params)
|
44
|
+
end
|
45
|
+
|
46
|
+
def instantiate_model(params)
|
47
|
+
send("#{self.class.action_name}_model", params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_model(params)
|
51
|
+
self.class.model_class.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_model(params)
|
55
|
+
self.class.model_class.find(params[:id])
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :find_model, :update_model
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Trailblazer::Operation::Representer
|
2
|
+
# TODO: test me.
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend Uber::InheritableAttr
|
6
|
+
base.inheritable_attr :representer_class
|
7
|
+
# TODO: allow representer without contract?!
|
8
|
+
# TODO: we have to extract the schema here, not subclass the contract.
|
9
|
+
base.representer_class = Class.new(base.contract_class.representer_class)
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def representer(&block)
|
15
|
+
representer_class.class_eval(&block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|