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
@@ -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