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