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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -3
  3. data/CHANGES.md +33 -0
  4. data/Gemfile +4 -1
  5. data/README.md +171 -166
  6. data/Rakefile +10 -2
  7. data/gemfiles/Gemfile.rails.lock +42 -11
  8. data/lib/trailblazer/autoloading.rb +4 -3
  9. data/lib/trailblazer/endpoint.rb +40 -0
  10. data/lib/trailblazer/operation.rb +15 -8
  11. data/lib/trailblazer/operation/collection.rb +7 -0
  12. data/lib/trailblazer/operation/controller.rb +30 -22
  13. data/lib/trailblazer/operation/controller/active_record.rb +6 -2
  14. data/lib/trailblazer/operation/crud.rb +3 -5
  15. data/lib/trailblazer/operation/dispatch.rb +29 -0
  16. data/lib/trailblazer/operation/representer.rb +41 -5
  17. data/lib/trailblazer/operation/responder.rb +2 -2
  18. data/lib/trailblazer/operation/uploaded_file.rb +4 -4
  19. data/lib/trailblazer/operation/worker.rb +8 -10
  20. data/lib/trailblazer/rails/railtie.rb +10 -7
  21. data/lib/trailblazer/version.rb +1 -1
  22. data/test/collection_test.rb +56 -0
  23. data/test/crud_test.rb +23 -1
  24. data/test/dispatch_test.rb +63 -0
  25. data/test/operation_test.rb +84 -125
  26. data/test/rails/controller_test.rb +51 -0
  27. data/test/rails/endpoint_test.rb +86 -0
  28. data/test/rails/fake_app/cells.rb +2 -2
  29. data/test/rails/fake_app/config.rb +1 -1
  30. data/test/rails/fake_app/controllers.rb +27 -0
  31. data/test/rails/fake_app/models.rb +10 -1
  32. data/test/rails/fake_app/rails_app.rb +15 -1
  33. data/test/rails/fake_app/song/operations.rb +38 -2
  34. data/test/rails/fake_app/views/bands/index.html.erb +1 -0
  35. data/test/rails/fake_app/views/songs/another_view.html.erb +2 -0
  36. data/test/representer_test.rb +126 -0
  37. data/test/responder_test.rb +2 -4
  38. data/test/rollback_test.rb +47 -0
  39. data/test/test_helper.rb +43 -1
  40. data/test/uploaded_file_test.rb +4 -4
  41. data/test/worker_test.rb +13 -9
  42. data/trailblazer.gemspec +7 -3
  43. 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 => [:test]
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 }
@@ -1,15 +1,18 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- trailblazer (0.1.1)
4
+ trailblazer (0.2.2)
5
5
  actionpack (>= 3.0.0)
6
- reform (~> 1.2.0)
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
- mini_portile (0.6.1)
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.10.1)
50
- nokogiri (1.6.4.1)
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.1)
82
+ reform (1.2.6)
65
83
  activemodel
66
84
  disposable (~> 0.0.5)
67
85
  representable (~> 2.1.0)
68
- uber (~> 0.0.8)
69
- representable (2.1.3)
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.11)
115
+ uber (0.0.13)
87
116
 
88
117
  PLATFORMS
89
118
  ruby
90
119
 
91
120
  DEPENDENCIES
92
121
  activerecord
93
- bundler (~> 1.3)
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, 'trailblazer/operation/controller'
3
- autoload :Responder, 'trailblazer/operation/responder'
4
- autoload :CRUD, 'trailblazer/operation/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(:raise_on_invalid => true).run(*params).last
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 #process without validate and returns the form object.
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
- [process(*params), valid?].reverse
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) # NOT to be overridden?!! it creates Result for us.
108
+ def validate(params, model, contract_class=nil)
107
109
  @contract = contract_for(contract_class, model)
108
110
 
109
- if @valid = contract.validate(params)
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
- self
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
@@ -0,0 +1,7 @@
1
+ module Trailblazer::Operation::Collection
2
+ # Collection does not produce a contract.
3
+ def present(*params)
4
+ setup!(*params)
5
+ self
6
+ end
7
+ end
@@ -1,6 +1,6 @@
1
- module Trailblazer::Operation::Controller
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
- process_params!(params)
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
- res, @operation = yield # Create.run(params)
74
- setup_operation_instance_variables!
73
+ def setup_operation_instance_variables!(operation)
74
+ @operation = operation
75
+ @form = operation.contract
76
+ @model = operation.model
77
+ end
75
78
 
76
- [res, @operation] # DISCUSS: do we need result here? or can we just go pick op.valid?
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
- def setup_operation_instance_variables!
80
- @form = @operation.contract
81
- @model = @operation.model
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
- @model.class.table_name.singularize
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, @model, *args)
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 :representer_class
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
- build_representer_class.class_eval(&block)
17
+ representer_class.class_eval(&block)
12
18
  end
13
19
 
14
- def build_representer_class
15
- representer_class || self.representer_class= Class.new(contract_class.schema)
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