trailblazer 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -3
- data/CHANGES.md +33 -0
- data/Gemfile +4 -1
- data/README.md +171 -166
- data/Rakefile +10 -2
- data/gemfiles/Gemfile.rails.lock +42 -11
- data/lib/trailblazer/autoloading.rb +4 -3
- data/lib/trailblazer/endpoint.rb +40 -0
- data/lib/trailblazer/operation.rb +15 -8
- data/lib/trailblazer/operation/collection.rb +7 -0
- data/lib/trailblazer/operation/controller.rb +30 -22
- data/lib/trailblazer/operation/controller/active_record.rb +6 -2
- data/lib/trailblazer/operation/crud.rb +3 -5
- data/lib/trailblazer/operation/dispatch.rb +29 -0
- data/lib/trailblazer/operation/representer.rb +41 -5
- data/lib/trailblazer/operation/responder.rb +2 -2
- data/lib/trailblazer/operation/uploaded_file.rb +4 -4
- data/lib/trailblazer/operation/worker.rb +8 -10
- data/lib/trailblazer/rails/railtie.rb +10 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +56 -0
- data/test/crud_test.rb +23 -1
- data/test/dispatch_test.rb +63 -0
- data/test/operation_test.rb +84 -125
- data/test/rails/controller_test.rb +51 -0
- data/test/rails/endpoint_test.rb +86 -0
- data/test/rails/fake_app/cells.rb +2 -2
- data/test/rails/fake_app/config.rb +1 -1
- data/test/rails/fake_app/controllers.rb +27 -0
- data/test/rails/fake_app/models.rb +10 -1
- data/test/rails/fake_app/rails_app.rb +15 -1
- data/test/rails/fake_app/song/operations.rb +38 -2
- data/test/rails/fake_app/views/bands/index.html.erb +1 -0
- data/test/rails/fake_app/views/songs/another_view.html.erb +2 -0
- data/test/representer_test.rb +126 -0
- data/test/responder_test.rb +2 -4
- data/test/rollback_test.rb +47 -0
- data/test/test_helper.rb +43 -1
- data/test/uploaded_file_test.rb +4 -4
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +7 -3
- 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=>["can't be blank"]}"
|
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=>["can't be blank"]}\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 :
|
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 :
|
17
|
+
render inline: <<-ERB
|
18
18
|
<%= paginate model %>
|
19
19
|
ERB
|
20
20
|
end
|
@@ -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, :
|
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
|
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
|
-
|
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,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
|