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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -1
  3. data/.travis.yml +5 -0
  4. data/CHANGES.md +3 -0
  5. data/Gemfile +4 -0
  6. data/README.md +417 -16
  7. data/Rakefile +14 -0
  8. data/THOUGHTS +12 -0
  9. data/TODO.md +6 -0
  10. data/doc/Trb-The-Stack.png +0 -0
  11. data/doc/trb.jpg +0 -0
  12. data/gemfiles/Gemfile.rails +7 -0
  13. data/gemfiles/Gemfile.rails.lock +99 -0
  14. data/lib/trailblazer.rb +2 -0
  15. data/lib/trailblazer/autoloading.rb +5 -0
  16. data/lib/trailblazer/operation.rb +124 -0
  17. data/lib/trailblazer/operation/controller.rb +76 -0
  18. data/lib/trailblazer/operation/crud.rb +61 -0
  19. data/lib/trailblazer/operation/representer.rb +18 -0
  20. data/lib/trailblazer/operation/responder.rb +24 -0
  21. data/lib/trailblazer/operation/uploaded_file.rb +77 -0
  22. data/lib/trailblazer/operation/worker.rb +96 -0
  23. data/lib/trailblazer/version.rb +1 -1
  24. data/test/crud_test.rb +115 -0
  25. data/test/fixtures/apotomo.png +0 -0
  26. data/test/fixtures/cells.png +0 -0
  27. data/test/operation_test.rb +334 -0
  28. data/test/rails/controller_test.rb +175 -0
  29. data/test/rails/fake_app/app-cells/.gitkeep +0 -0
  30. data/test/rails/fake_app/cells.rb +21 -0
  31. data/test/rails/fake_app/config.rb +3 -0
  32. data/test/rails/fake_app/controllers.rb +101 -0
  33. data/test/rails/fake_app/models.rb +13 -0
  34. data/test/rails/fake_app/rails_app.rb +57 -0
  35. data/test/rails/fake_app/song/operations.rb +63 -0
  36. data/test/rails/fake_app/views/bands/show.html.erb +1 -0
  37. data/test/rails/fake_app/views/songs/new.html.erb +1 -0
  38. data/test/rails/test_helper.rb +4 -0
  39. data/test/responder_test.rb +77 -0
  40. data/test/test_helper.rb +15 -0
  41. data/test/uploaded_file_test.rb +85 -0
  42. data/test/worker_test.rb +116 -0
  43. data/trailblazer.gemspec +10 -2
  44. 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,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: '../'
4
+
5
+ gem "railties"
6
+ gem "activerecord"
7
+ gem "sqlite3"
@@ -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
@@ -1,4 +1,6 @@
1
+ require "trailblazer/operation"
1
2
  require "trailblazer/version"
3
+ require "uber/inheritable_attr"
2
4
 
3
5
  module Trailblazer
4
6
  # Your code goes here...
@@ -0,0 +1,5 @@
1
+ Trailblazer::Operation.class_eval do
2
+ autoload :Controller, 'trailblazer/operation/controller'
3
+ autoload :Responder, 'trailblazer/operation/responder'
4
+ autoload :CRUD, 'trailblazer/operation/crud'
5
+ end
@@ -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