trailblazer 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -52,12 +52,24 @@ class ResponderRespondTest < ActionController::TestCase
52
52
  assert_redirected_to song_path(Song.last)
53
53
  end
54
54
 
55
+ test "Create [html/valid/location]" do
56
+ post :other_create, {song: {title: "You're Going Down"}}
57
+ assert_redirected_to other_create_songs_path
58
+ end
59
+
55
60
  test "Create [html/invalid]" do
56
61
  post :create, {song: {title: ""}}
57
62
  assert_response 200
58
63
  assert_equal @response.body, "{:title=&gt;[&quot;can&#39;t be blank&quot;]}"
59
64
  end
60
65
 
66
+ test "Create [html/invalid/action]" do
67
+ post :other_create, {song: {title: ""}}
68
+ assert_response 200
69
+ assert_equal @response.body, "OTHER SONG\n{:title=&gt;[&quot;can&#39;t be blank&quot;]}\n"
70
+ assert_template "songs/another_view"
71
+ end
72
+
61
73
  test "Delete [html/valid]" do
62
74
  song = Song::Create[song: {title: "You're Going Down"}].model
63
75
  delete :destroy, id: song.id
@@ -170,6 +182,24 @@ class ControllerPresentTest < ActionController::TestCase
170
182
  end
171
183
  end
172
184
 
185
+ #collection
186
+ class ControllerCollectionTest < ActionController::TestCase
187
+ tests BandsController
188
+
189
+ # let (:band) { }
190
+
191
+ test "#collection" do
192
+ Band.destroy_all
193
+ Band::Create[band: {name: "Nofx"}]
194
+ Band::Create[band: {name: "Ramones"}]
195
+
196
+
197
+ get :index
198
+
199
+ assert_equal "bands/index.html: Nofx Ramones \n", response.body
200
+ end
201
+ end
202
+
173
203
  # #form.
174
204
  class ControllerFormTest < ActionController::TestCase
175
205
  tests BandsController
@@ -203,4 +233,25 @@ class ActiveRecordPresentTest < ActionController::TestCase
203
233
 
204
234
  assert_equal "active_record_bands/show.html: Band, Band, true, Band::Update", response.body
205
235
  end
236
+
237
+ test "#collection" do
238
+ Band.destroy_all
239
+ Band::Create[band: {name: "Nofx"}]
240
+
241
+ get :index
242
+
243
+ assert_equal "active_record_bands/index.html: Band::ActiveRecord_Relation, Band::ActiveRecord_Relation, Band::Index", response.body
244
+ end
206
245
  end
246
+
247
+ class PrefixedTablenameControllerTest < ActionController::TestCase
248
+ tests TenantsController
249
+
250
+ test "show" do
251
+ tenant = Tenant.create(name: "My Tenant") # yepp, I am not using an operation! blasphemy!!!
252
+ get :show, id: tenant.id
253
+
254
+ assert_equal "My Tenant", response.body
255
+ end
256
+ end
257
+
@@ -0,0 +1,86 @@
1
+ require "test_helper"
2
+
3
+ module RailsEndpoint
4
+ # this tests "the rails way" where both JSON and HTML operation use the pre-parsed params hash.
5
+ class UnconfiguredTest < ActionDispatch::IntegrationTest
6
+ class Create < Trailblazer::Operation
7
+ include CRUD
8
+ model Band
9
+
10
+ def process(params)
11
+ @model = Band.create(params["band"].permit(:name)) # how ridiculous is strong_parameters?
12
+ end
13
+ end
14
+
15
+ class BandsController < ApplicationController
16
+ include Trailblazer::Operation::Controller
17
+ respond_to :html, :json
18
+ # missing document_formats.
19
+
20
+ def create
21
+ run Create if request.format == :json
22
+ run Create if request.format == :html
23
+ render text: ""
24
+ end
25
+ end
26
+
27
+ test "Create" do
28
+ post "/rails_endpoint/unconfigured_test/bands", {band: {name: "SNFU"}}
29
+ assert_response 200
30
+ assert_equal "SNFU", Band.last.name
31
+ end
32
+
33
+ test "Create: JSON" do
34
+ headers = { 'CONTENT_TYPE' => 'application/json' } # hahaha, oh god, rails, good bye!
35
+ post "/rails_endpoint/unconfigured_test/bands", {band: {name: "Strike Anywhere"}}.to_json, headers
36
+ assert_response 200
37
+ assert_equal "Strike Anywhere", Band.last.name
38
+ end
39
+ end
40
+
41
+
42
+ class ConfiguredTest < ActionDispatch::IntegrationTest
43
+ class Create < Trailblazer::Operation
44
+ include CRUD
45
+ model Band
46
+
47
+ def process(params)
48
+ @model = Band.create(params["band"].permit(:name)) # how ridiculous is strong_parameters?
49
+ end
50
+ end
51
+
52
+ class JSONCreate < Trailblazer::Operation
53
+ include CRUD
54
+ model Band
55
+
56
+ def process(params)
57
+ @model = Band.create(JSON.parse(params["band"])) # document comes in keyed as "band".
58
+ end
59
+
60
+ end
61
+
62
+ class BandsController < ApplicationController
63
+ include Trailblazer::Operation::Controller
64
+ respond_to :html, :json
65
+ operation document_formats: :json
66
+
67
+ def create
68
+ run Create if request.format == :html
69
+ run JSONCreate if request.format == :json
70
+ render text: ""
71
+ end
72
+ end
73
+
74
+ test "Create" do
75
+ post "/rails_endpoint/configured_test/bands", {band: {name: "All"}}
76
+ assert_response 200
77
+ assert_equal "All", Band.last.name
78
+ end
79
+
80
+ test "Create: JSON" do
81
+ post "/rails_endpoint/configured_test/bands.json", {name: "NOFX"}.to_json#, headers # FIXME: headers do not work
82
+ assert_response 200
83
+ assert_equal "NOFX", Band.last.name
84
+ end
85
+ end
86
+ end
@@ -4,7 +4,7 @@ class UserCell < Cell::Rails
4
4
  def show(users)
5
5
  @users = users
6
6
 
7
- render :inline => <<-ERB
7
+ render inline: <<-ERB
8
8
  <%= paginate @users %>
9
9
  ERB
10
10
  end
@@ -14,7 +14,7 @@ class ViewModelCell < Cell::ViewModel
14
14
  include Kaminari::Cells
15
15
 
16
16
  def show
17
- render :inline => <<-ERB
17
+ render inline: <<-ERB
18
18
  <%= paginate model %>
19
19
  ERB
20
20
  end
@@ -1,3 +1,3 @@
1
1
  # database
2
2
  ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
3
- ActiveRecord::Base.establish_connection('test')
3
+ ActiveRecord::Base.establish_connection(:test)
@@ -20,6 +20,10 @@ ERB
20
20
  respond Song::Create
21
21
  end
22
22
 
23
+ def other_create
24
+ respond Song::Create, params, { location: other_create_songs_path, action: :another_view }
25
+ end
26
+
23
27
  def create_with_params
24
28
  respond Song::Create, song: {title: "A Beautiful Indifference"}
25
29
  end
@@ -44,6 +48,11 @@ end
44
48
  class BandsController < ApplicationController
45
49
  include Trailblazer::Operation::Controller
46
50
  respond_to :html, :json
51
+ operation document_formats: :json
52
+
53
+ def index
54
+ collection Band::Index
55
+ end
47
56
 
48
57
  def show
49
58
  present Band::Update do |op|
@@ -110,9 +119,27 @@ class ActiveRecordBandsController < ApplicationController
110
119
  include Trailblazer::Operation::Controller::ActiveRecord
111
120
  respond_to :html
112
121
 
122
+ def index
123
+ collection Band::Index
124
+ render text: "active_record_bands/index.html: #{@collection.class}, #{@bands.class}, #{@operation.class}"
125
+ end
126
+
113
127
  def show
114
128
  present Band::Update
115
129
 
116
130
  render text: "active_record_bands/show.html: #{@model.class}, #{@band.class}, #{@form.is_a?(Reform::Form)}, #{@operation.class}"
117
131
  end
118
132
  end
133
+
134
+ require 'trailblazer/operation/controller/active_record'
135
+ class TenantsController < ApplicationController
136
+ include Trailblazer::Operation::Controller
137
+ include Trailblazer::Operation::Controller::ActiveRecord
138
+ respond_to :html
139
+
140
+ def show
141
+ present Tenant::Show
142
+ render text: "#{@tenant.name}" # model ivar doesn't contain table prefix `bla.xxx`.
143
+ end
144
+ end
145
+
@@ -6,6 +6,10 @@ class Band < ActiveRecord::Base
6
6
  has_many :songs
7
7
  end
8
8
 
9
+ class Tenant < ActiveRecord::Base
10
+ self.table_name = 'public.tenants'
11
+ end
12
+
9
13
  # migrations
10
14
  class CreateAllTables < ActiveRecord::Migration
11
15
  def self.up
@@ -19,7 +23,12 @@ class CreateAllTables < ActiveRecord::Migration
19
23
  t.string :name
20
24
  t.string :locality
21
25
  end
26
+
27
+ create_table(:"public.tenants") do |t|
28
+ t.string :name
29
+ end
22
30
  end
23
31
  end
24
32
  ActiveRecord::Migration.verbose = false
25
- CreateAllTables.up
33
+ CreateAllTables.up
34
+
@@ -5,6 +5,7 @@
5
5
  require 'action_controller/railtie'
6
6
  require 'action_view/railtie'
7
7
  require 'active_record'
8
+ require 'responders'
8
9
 
9
10
  require 'fake_app/config'
10
11
 
@@ -12,7 +13,7 @@ require 'fake_app/config'
12
13
  # config
13
14
  app = Class.new(Rails::Application)
14
15
  app.config.secret_token = '3b7cd727ee24e8444053437c36cc66c4'
15
- app.config.session_store :cookie_store, :key => '_myapp_session'
16
+ app.config.session_store :cookie_store, key: '_myapp_session'
16
17
  app.config.active_support.deprecation = :log
17
18
  app.config.eager_load = false
18
19
  # Rais.root
@@ -22,12 +23,14 @@ app.initialize!
22
23
 
23
24
  # routes
24
25
  app.routes.draw do
26
+
25
27
  resources :songs do
26
28
  member do # argh.
27
29
  delete :destroy_with_formats
28
30
  end
29
31
 
30
32
  collection do
33
+ post :other_create
31
34
  post :create_with_params
32
35
  post :create_with_block
33
36
  end
@@ -45,6 +48,17 @@ app.routes.draw do
45
48
  post :update_with_block
46
49
  end
47
50
  end
51
+
52
+ namespace :rails_endpoint do
53
+ namespace :unconfigured_test do
54
+ resources :bands
55
+ end
56
+ namespace :configured_test do
57
+ resources :bands
58
+ end
59
+ end
60
+
61
+ resources :tenants, only: [:show]
48
62
  end
49
63
 
50
64
  require 'trailblazer/operation/responder'
@@ -1,3 +1,5 @@
1
+ require 'trailblazer/autoloading'
2
+
1
3
  class Song < ActiveRecord::Base
2
4
  class Create < Trailblazer::Operation
3
5
  include CRUD
@@ -30,7 +32,7 @@ end
30
32
 
31
33
  class Band < ActiveRecord::Base
32
34
  class Create < Trailblazer::Operation
33
- include CRUD, Responder, Representer
35
+ include CRUD, Responder#, Representer
34
36
  model Band, :create
35
37
 
36
38
  contract do
@@ -56,6 +58,7 @@ class Band < ActiveRecord::Base
56
58
  end
57
59
 
58
60
  class JSON < self
61
+ include Representer
59
62
  require "reform/form/json"
60
63
  contract do
61
64
  include Reform::Form::JSON # this allows deserialising JSON.
@@ -93,6 +96,7 @@ class Band < ActiveRecord::Base
93
96
 
94
97
  # TODO: infer stuff per default.
95
98
  class JSON < self
99
+ include Representer
96
100
  self.contract_class = Create::JSON.contract_class
97
101
  self.representer_class = Create::JSON.representer_class
98
102
  end
@@ -101,4 +105,36 @@ class Band < ActiveRecord::Base
101
105
  JSON if params[:format] == "json"
102
106
  end
103
107
  end
104
- end
108
+
109
+ class Index < Trailblazer::Operation
110
+ include Collection
111
+
112
+ def model!(params)
113
+ Band.all
114
+ end
115
+
116
+ builds do |params|
117
+ JSON if params[:format] == "json"
118
+ end
119
+
120
+ class JSON < self
121
+ include Representer
122
+
123
+ module BandRepresenter
124
+ include Representable::JSON
125
+ property :name
126
+ property :locality
127
+ end
128
+
129
+ self.representer_class = BandRepresenter
130
+ end
131
+ end
132
+ end
133
+
134
+ class Tenant < ActiveRecord::Base
135
+ class Show < Trailblazer::Operation
136
+ include CRUD
137
+ model Tenant, :update
138
+ end
139
+ end
140
+
@@ -0,0 +1 @@
1
+ bands/index.html: <% @collection.each do |element| %><%= "#{element.name} " %><% end %>
@@ -0,0 +1,2 @@
1
+ OTHER SONG
2
+ <%= @form.errors.to_s %>
@@ -0,0 +1,126 @@
1
+ require "test_helper"
2
+
3
+ require "representable/json"
4
+
5
+ class RepresenterTest < MiniTest::Spec
6
+ Album = Struct.new(:title, :artist)
7
+ Artist = Struct.new(:name)
8
+
9
+ class Create < Trailblazer::Operation
10
+ require "trailblazer/operation/representer"
11
+ include Representer
12
+
13
+ contract do
14
+ property :title
15
+ validates :title, presence: true
16
+ property :artist, populate_if_empty: Artist do
17
+ property :name
18
+ validates :name, presence: true
19
+ end
20
+ end
21
+
22
+ def process(params)
23
+ @model = Album.new # NO artist!!!
24
+ validate(params[:album], @model)
25
+ end
26
+ end
27
+
28
+
29
+ # Infers representer from contract, no customization.
30
+ class Show < Create
31
+ def process(params)
32
+ @model = Album.new("After The War", Artist.new("Gary Moore"))
33
+ @contract = @model
34
+ end
35
+ end
36
+
37
+
38
+ # Infers representer, adds hypermedia.
39
+ require "roar/json/hal"
40
+ class HypermediaCreate < Create
41
+ representer do
42
+ include Roar::JSON::HAL
43
+
44
+ link(:self) { "//album/#{represented.title}" }
45
+ end
46
+ end
47
+
48
+ class HypermediaShow < HypermediaCreate
49
+ def process(params)
50
+ @model = Album.new("After The War", Artist.new("Gary Moore"))
51
+ @contract = @model
52
+ end
53
+ end
54
+
55
+
56
+ # rendering
57
+ # generic contract -> representer
58
+ it do
59
+ res, op = Show.run({})
60
+ op.to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"}}}
61
+ end
62
+
63
+ # contract -> representer with hypermedia
64
+ it do
65
+ res, op = HypermediaShow.run({})
66
+ op.to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}}
67
+ end
68
+
69
+
70
+ # parsing
71
+ it do
72
+ res, op = Create.run(album: %{{"title":"Run For Cover","artist":{"name":"Gary Moore"}}})
73
+ op.contract.title.must_equal "Run For Cover"
74
+ op.contract.artist.name.must_equal "Gary Moore"
75
+ end
76
+
77
+ it do
78
+ res, op = HypermediaCreate.run(album: %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}})
79
+ op.contract.title.must_equal "After The War"
80
+ op.contract.artist.name.must_equal "Gary Moore"
81
+ end
82
+
83
+
84
+
85
+
86
+
87
+ # explicit representer set with ::representer_class=.
88
+ require "roar/decorator"
89
+ class JsonApiCreate < Trailblazer::Operation
90
+ include Representer
91
+
92
+ contract do # we still need contract as the representer writes to the contract twin.
93
+ property :title
94
+ end
95
+
96
+ class AlbumRepresenter < Roar::Decorator
97
+ include Roar::JSON
98
+ property :title
99
+ end
100
+ self.representer_class = AlbumRepresenter
101
+
102
+ def process(params)
103
+ @model = Album.new # NO artist!!!
104
+ validate(params[:album], @model)
105
+ end
106
+ end
107
+
108
+ class JsonApiShow < JsonApiCreate
109
+ def process(params)
110
+ @model = Album.new("After The War", Artist.new("Gary Moore"))
111
+ @contract = @model
112
+ end
113
+ end
114
+
115
+ # render.
116
+ it do
117
+ res, op = JsonApiShow.run({})
118
+ op.to_json.must_equal %{{"title":"After The War"}}
119
+ end
120
+
121
+ # parse.
122
+ it do
123
+ res, op = JsonApiCreate.run(album: %{{"title":"Run For Cover"}})
124
+ op.contract.title.must_equal "Run For Cover"
125
+ end
126
+ end