azeroth 0.8.2 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +3 -3
- data/.gitignore +1 -0
- data/Dockerfile +6 -9
- data/README.md +31 -1
- data/azeroth.gemspec +22 -20
- data/config/yardstick.yml +1 -0
- data/lib/azeroth/controller_interface.rb +15 -0
- data/lib/azeroth/options.rb +30 -10
- data/lib/azeroth/params_builder.rb +80 -0
- data/lib/azeroth/request_handler/create.rb +1 -6
- data/lib/azeroth/request_handler/update.rb +23 -6
- data/lib/azeroth/request_handler.rb +1 -2
- data/lib/azeroth/resourceable/builder.rb +2 -24
- data/lib/azeroth/resourceable/class_methods.rb +1 -130
- data/lib/azeroth/resourceable.rb +171 -4
- data/lib/azeroth/version.rb +1 -1
- data/lib/azeroth.rb +1 -0
- data/spec/dummy/app/models/movie/simple_decorator.rb +7 -0
- data/spec/dummy/app/models/movie.rb +5 -0
- data/spec/dummy/db/schema.rb +5 -0
- data/spec/integration/yard/controllers/paginated_documents_controller_spec.rb +33 -0
- data/spec/lib/azeroth/params_builder_spec.rb +56 -0
- data/spec/lib/azeroth/request_handler/update_spec.rb +112 -0
- data/spec/lib/azeroth/resourceable_spec.rb +108 -0
- data/spec/support/app/controllers/controller.rb +1 -1
- data/spec/support/app/controllers/params_builder_controller.rb +14 -0
- data/spec/support/app/controllers/request_handler_controller.rb +6 -0
- data/spec/support/factories/movie.rb +8 -0
- metadata +92 -50
data/lib/azeroth/resourceable.rb
CHANGED
@@ -8,8 +8,6 @@ module Azeroth
|
|
8
8
|
#
|
9
9
|
# Concern for building controller methods for the routes
|
10
10
|
#
|
11
|
-
# @example (see Resourceable::ClassMethods#resource_for)
|
12
|
-
#
|
13
11
|
# @see Resourceable::ClassMethods
|
14
12
|
module Resourceable
|
15
13
|
extend ActiveSupport::Concern
|
@@ -27,21 +25,190 @@ module Azeroth
|
|
27
25
|
#
|
28
26
|
# @param name [String, Symbol] Name of the resource
|
29
27
|
# @param options [Hash] resource building options
|
30
|
-
# @option options only [Array<Symbol,String
|
28
|
+
# @option options only [Array<Symbol,String>,Symbol,String] List of
|
31
29
|
# actions to be built
|
32
|
-
# @option options except [Array<Symbol,String
|
30
|
+
# @option options except [Array<Symbol,String>,Symbol,String] List of
|
33
31
|
# actions to not to be built
|
34
32
|
# @option options decorator [Azeroth::Decorator,TrueClass,FalseClass]
|
35
33
|
# Decorator class or flag allowing/disallowing decorators
|
36
34
|
# @option options before_save [Symbol,Proc] method/block
|
37
35
|
# to be ran on the controller before saving the resource
|
36
|
+
# @option options after_save [Symbol,Proc] method/block
|
37
|
+
# to be ran on the controller after saving the resource
|
38
38
|
# @option options build_with [Symbol,Proc] method/block
|
39
39
|
# to be ran when building resource
|
40
40
|
# (default proc { <resource_collection>.build(resource_params) }
|
41
|
+
# @option options update_with [Symbol,Proc] method/block
|
42
|
+
# to be ran when updating resource
|
43
|
+
# (default proc { <resource>.update(resource_params) }
|
44
|
+
# @option options paginated [TrueClass,FalseClass] flag defining if index
|
45
|
+
# endpoint should be paginated
|
46
|
+
# @option options per_page [Integer] number of entries returned per
|
47
|
+
# page on index
|
41
48
|
#
|
42
49
|
# @return [Array<MethodDefinition>] list of methods created
|
43
50
|
#
|
44
51
|
# @see Options::DEFAULT_OPTIONS
|
52
|
+
#
|
53
|
+
# @example Controller without delete
|
54
|
+
# class DocumentsController < ApplicationController
|
55
|
+
# include Azeroth::Resourceable
|
56
|
+
#
|
57
|
+
# resource_for :document, except: :delete
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @example Controller with only create, show and list
|
61
|
+
# class DocumentsController < ApplicationController
|
62
|
+
# include Azeroth::Resourceable
|
63
|
+
#
|
64
|
+
# resource_for :document, only: %w[create index show]
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# @example complete example gmaes and publishers
|
68
|
+
# class PublishersController < ApplicationController
|
69
|
+
# include Azeroth::Resourceable
|
70
|
+
# skip_before_action :verify_authenticity_token
|
71
|
+
#
|
72
|
+
# resource_for :publisher, only: %i[create index]
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# class GamesController < ApplicationController
|
76
|
+
# include Azeroth::Resourceable
|
77
|
+
# skip_before_action :verify_authenticity_token
|
78
|
+
#
|
79
|
+
# resource_for :game, except: :delete
|
80
|
+
#
|
81
|
+
# private
|
82
|
+
#
|
83
|
+
# def games
|
84
|
+
# publisher.games
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# def publisher
|
88
|
+
# @publisher ||= Publisher.find(publisher_id)
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# def publisher_id
|
92
|
+
# params.require(:publisher_id)
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# ActiveRecord::Schema.define do
|
97
|
+
# self.verbose = false
|
98
|
+
#
|
99
|
+
# create_table :publishers, force: true do |t|
|
100
|
+
# t.string :name
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# create_table :games, force: true do |t|
|
104
|
+
# t.string :name
|
105
|
+
# t.integer :publisher_id
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# class Publisher < ActiveRecord::Base
|
110
|
+
# has_many :games
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# class Game < ActiveRecord::Base
|
114
|
+
# belongs_to :publisher
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# class Game::Decorator < Azeroth::Decorator
|
118
|
+
# expose :id
|
119
|
+
# expose :name
|
120
|
+
# expose :publisher, decorator: NameDecorator
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# @example requesting games and publishers
|
124
|
+
# post "/publishers.json", params: {
|
125
|
+
# publisher: {
|
126
|
+
# name: 'Nintendo'
|
127
|
+
# }
|
128
|
+
# }
|
129
|
+
#
|
130
|
+
# publisher = JSON.parse(response.body)
|
131
|
+
# # returns
|
132
|
+
# # {
|
133
|
+
# # 'id' => 11,
|
134
|
+
# # 'name' => 'Nintendo'
|
135
|
+
# # }
|
136
|
+
#
|
137
|
+
# publisher = Publisher.last
|
138
|
+
# post "/publishers/#{publisher['id']}/games.json", params: {
|
139
|
+
# game: {
|
140
|
+
# name: 'Pokemon'
|
141
|
+
# }
|
142
|
+
# }
|
143
|
+
#
|
144
|
+
# game = Game.last
|
145
|
+
#
|
146
|
+
# JSON.parse(response.body)
|
147
|
+
# # returns
|
148
|
+
# # {
|
149
|
+
# # id: game.id,
|
150
|
+
# # name: 'Pokemon',
|
151
|
+
# # publisher: {
|
152
|
+
# # name: 'Nintendo'
|
153
|
+
# # }
|
154
|
+
# # }
|
155
|
+
#
|
156
|
+
# @example Controller with before_save
|
157
|
+
# class PokemonsController < ApplicationController
|
158
|
+
# include Azeroth::Resourceable
|
159
|
+
#
|
160
|
+
# resource_for :pokemon,
|
161
|
+
# only: %i[create update],
|
162
|
+
# before_save: :set_favorite
|
163
|
+
#
|
164
|
+
# private
|
165
|
+
#
|
166
|
+
# def set_favorite
|
167
|
+
# pokemon.favorite = true
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# def pokemons
|
171
|
+
# master.pokemons
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# def master
|
175
|
+
# @master ||= PokemonMaster.find(master_id)
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# def master_id
|
179
|
+
# params.require(:pokemon_master_id)
|
180
|
+
# end
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# @example Controller with paginated index response
|
184
|
+
#
|
185
|
+
# class PaginatedDocumentsController < ApplicationController
|
186
|
+
# include Azeroth::Resourceable
|
187
|
+
#
|
188
|
+
# resource_for :document, only: 'index', paginated: true
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# 30.times { create(:document) }
|
192
|
+
#
|
193
|
+
# get '/paginated_documents.json'
|
194
|
+
#
|
195
|
+
# # returns Array with 20 first documents
|
196
|
+
# # returns in the headers pagination headers
|
197
|
+
# {
|
198
|
+
# 'pages' => 2,
|
199
|
+
# 'per_page' => 20,
|
200
|
+
# 'page' => 1
|
201
|
+
# }
|
202
|
+
#
|
203
|
+
# get '/paginated_documents.json?page=2'
|
204
|
+
#
|
205
|
+
# # returns Array with 10 next documents
|
206
|
+
# # returns in the headers pagination headers
|
207
|
+
# {
|
208
|
+
# 'pages' => 2,
|
209
|
+
# 'per_page' => 20,
|
210
|
+
# 'page' => 2
|
211
|
+
# }
|
45
212
|
end
|
46
213
|
|
47
214
|
private
|
data/lib/azeroth/version.rb
CHANGED
data/lib/azeroth.rb
CHANGED
@@ -15,6 +15,7 @@ module Azeroth
|
|
15
15
|
autoload :Decorator, 'azeroth/decorator'
|
16
16
|
autoload :DummyDecorator, 'azeroth/dummy_decorator'
|
17
17
|
autoload :Model, 'azeroth/model'
|
18
|
+
autoload :ParamsBuilder, 'azeroth/params_builder'
|
18
19
|
autoload :RequestHandler, 'azeroth/request_handler'
|
19
20
|
autoload :Resourceable, 'azeroth/resourceable'
|
20
21
|
autoload :ResourceBuilder, 'azeroth/resource_builder'
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -47,5 +47,10 @@ ActiveRecord::Schema.define do
|
|
47
47
|
t.index %i[pokemon_master_id favorite], unique: true
|
48
48
|
end
|
49
49
|
|
50
|
+
create_table :movies, force: true do |t|
|
51
|
+
t.string :name, null: false
|
52
|
+
t.string :director, null: false
|
53
|
+
end
|
54
|
+
|
50
55
|
add_foreign_key 'pokemons', 'pokemon_masters'
|
51
56
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe PaginatedDocumentsController, controller: true do
|
6
|
+
describe 'yard' do
|
7
|
+
describe 'GET index' do
|
8
|
+
before { create_list(:document, 30) }
|
9
|
+
|
10
|
+
it 'list documents with pagination page' do
|
11
|
+
get '/paginated_documents.json'
|
12
|
+
|
13
|
+
documents = JSON.parse(response.body)
|
14
|
+
expect(documents)
|
15
|
+
.to have(20).items
|
16
|
+
|
17
|
+
expect(response.headers['pages']).to eq(2)
|
18
|
+
expect(response.headers['per_page']).to eq(20)
|
19
|
+
expect(response.headers['page']).to eq(1)
|
20
|
+
|
21
|
+
get '/paginated_documents.json?page=2'
|
22
|
+
|
23
|
+
documents = JSON.parse(response.body)
|
24
|
+
expect(documents)
|
25
|
+
.to have(10).items
|
26
|
+
|
27
|
+
expect(response.headers['pages']).to eq(2)
|
28
|
+
expect(response.headers['per_page']).to eq(20)
|
29
|
+
expect(response.headers['page']).to eq(2)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Azeroth::ParamsBuilder do
|
6
|
+
subject(:params_builder) { described_class.new(model, builder) }
|
7
|
+
|
8
|
+
let(:model) { Azeroth::Model.new(:document, options) }
|
9
|
+
let(:options) { Azeroth::Options.new }
|
10
|
+
let(:builder) { Sinclair.new(klass) }
|
11
|
+
let(:klass) { Class.new(ParamsBuilderController) }
|
12
|
+
|
13
|
+
before do
|
14
|
+
params_builder.append
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#append' do
|
18
|
+
it 'adds id method' do
|
19
|
+
expect { builder.build }
|
20
|
+
.to add_method(:document_id).to(klass)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'adds params method' do
|
24
|
+
expect { builder.build }
|
25
|
+
.to add_method(:document_params).to(klass)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'after the build' do
|
29
|
+
let(:controller) { klass.new(id, attributes) }
|
30
|
+
let(:document) { create(:document) }
|
31
|
+
let(:attributes) { document.attributes }
|
32
|
+
let(:id) { Random.rand(10..100) }
|
33
|
+
let(:expected_attributes) do
|
34
|
+
{
|
35
|
+
'name' => document.name,
|
36
|
+
'reference' => document.reference
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
before { builder.build }
|
41
|
+
|
42
|
+
context 'when requesting id' do
|
43
|
+
it 'returns id from request path' do
|
44
|
+
expect(controller.document_id).to eq(id)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when requesting params' do
|
49
|
+
it 'returns payload' do
|
50
|
+
expect(controller.document_params.to_h)
|
51
|
+
.to eq(expected_attributes)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -30,6 +30,118 @@ describe Azeroth::RequestHandler::Update do
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
+
context 'when update_with option is given ' do
|
34
|
+
context 'with block' do
|
35
|
+
it_behaves_like 'a request handler' do
|
36
|
+
let(:block) do
|
37
|
+
proc do
|
38
|
+
document.assign_attributes(document_params)
|
39
|
+
document.name = "#{document.name}!"
|
40
|
+
document.save
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:options_hash) do
|
45
|
+
{
|
46
|
+
update_with: block
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
let(:expected_resource) { document }
|
51
|
+
|
52
|
+
let(:extra_params) do
|
53
|
+
{
|
54
|
+
id: document.id,
|
55
|
+
document: {
|
56
|
+
name: 'New Name'
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
let(:expected_json) do
|
62
|
+
{
|
63
|
+
'name' => 'New Name!'
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'updates the values given by request' do
|
68
|
+
expect { handler.process }
|
69
|
+
.to change { document.reload.name }
|
70
|
+
.from(document.name)
|
71
|
+
.to('New Name!')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with symbol' do
|
77
|
+
it_behaves_like 'a request handler' do
|
78
|
+
let(:options_hash) do
|
79
|
+
{
|
80
|
+
update_with: :add_bang_name
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
let(:expected_resource) { document }
|
85
|
+
|
86
|
+
let(:extra_params) do
|
87
|
+
{
|
88
|
+
id: document.id,
|
89
|
+
document: {
|
90
|
+
name: 'New Name'
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
let(:expected_json) do
|
96
|
+
{
|
97
|
+
'name' => 'New Name!'
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'updates the values given by request' do
|
102
|
+
expect { handler.process }
|
103
|
+
.to change { document.reload.name }
|
104
|
+
.from(document.name)
|
105
|
+
.to('New Name!')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with string' do
|
111
|
+
it_behaves_like 'a request handler' do
|
112
|
+
let(:options_hash) do
|
113
|
+
{
|
114
|
+
update_with: 'add_bang_name'
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
let(:expected_resource) { document }
|
119
|
+
|
120
|
+
let(:extra_params) do
|
121
|
+
{
|
122
|
+
id: document.id,
|
123
|
+
document: {
|
124
|
+
name: 'New Name'
|
125
|
+
}
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
let(:expected_json) do
|
130
|
+
{
|
131
|
+
'name' => 'New Name!'
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'updates the values given by request' do
|
136
|
+
expect { handler.process }
|
137
|
+
.to change { document.reload.name }
|
138
|
+
.from(document.name)
|
139
|
+
.to('New Name!')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
33
145
|
context 'with before_save block option' do
|
34
146
|
it_behaves_like 'a request handler' do
|
35
147
|
let(:block) do
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Azeroth::Resourceable do
|
6
|
+
let(:controller_class) do
|
7
|
+
Class.new(Controller) do
|
8
|
+
include Azeroth::Resourceable
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.resource_for' do
|
13
|
+
let(:params) { { id: model.id, format: :json } }
|
14
|
+
let(:model_name) { :document }
|
15
|
+
let(:model) { create(model_name) }
|
16
|
+
let(:controller) { controller_class.new(params) }
|
17
|
+
let(:decorator) { Document::Decorator }
|
18
|
+
|
19
|
+
context 'when no special option is given' do
|
20
|
+
%i[index show new edit update destroy].each do |method_name|
|
21
|
+
it do
|
22
|
+
expect { controller_class.resource_for(model_name) }
|
23
|
+
.to add_method(method_name).to(controller_class)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when passing the only option' do
|
29
|
+
let(:options) { { only: %i[index show] } }
|
30
|
+
|
31
|
+
%i[index show].each do |method_name|
|
32
|
+
it do
|
33
|
+
expect { controller_class.resource_for(model_name, **options) }
|
34
|
+
.to add_method(method_name).to(controller_class)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
%i[new edit update destroy].each do |method_name|
|
39
|
+
it do
|
40
|
+
expect { controller_class.resource_for(model_name, **options) }
|
41
|
+
.not_to add_method(method_name).to(controller_class)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when passing the except option' do
|
47
|
+
let(:options) { { except: %i[index show] } }
|
48
|
+
|
49
|
+
%i[index show].each do |method_name|
|
50
|
+
it do
|
51
|
+
expect { controller_class.resource_for(model_name, **options) }
|
52
|
+
.not_to add_method(method_name).to(controller_class)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
%i[new edit update destroy].each do |method_name|
|
57
|
+
it do
|
58
|
+
expect { controller_class.resource_for(model_name, **options) }
|
59
|
+
.to add_method(method_name).to(controller_class)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when passing decorator option' do
|
65
|
+
let(:model_name) { :movie }
|
66
|
+
let(:decorator) { Movie::SimpleDecorator }
|
67
|
+
let(:rendered) { {} }
|
68
|
+
|
69
|
+
before do
|
70
|
+
controller_class.resource_for(model_name, decorator: decorator)
|
71
|
+
|
72
|
+
allow(controller).to receive(:render) do |args|
|
73
|
+
rendered.merge!(args[:json])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'decorates the model' do
|
78
|
+
controller.show
|
79
|
+
expect(rendered).to eq(decorator.new(model).as_json)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when the method is called' do
|
84
|
+
let(:rendered) { {} }
|
85
|
+
|
86
|
+
before do
|
87
|
+
controller_class.resource_for(model_name)
|
88
|
+
allow(controller).to receive(:render) do |args|
|
89
|
+
rendered.merge!(args[:json])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'decorates the model' do
|
94
|
+
controller.show
|
95
|
+
expect(rendered).to eq(decorator.new(model).as_json)
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when the model does not have a decorator' do
|
99
|
+
let(:model_name) { :movie }
|
100
|
+
|
101
|
+
it 'renders model as json' do
|
102
|
+
controller.show
|
103
|
+
expect(rendered).to eq(model.as_json)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -28,4 +28,10 @@ class RequestHandlerController < ActionController::Base
|
|
28
28
|
documents.where(reference: 'X-MAGIC-15')
|
29
29
|
.build(document_params)
|
30
30
|
end
|
31
|
+
|
32
|
+
def add_bang_name
|
33
|
+
document.assign_attributes(document_params)
|
34
|
+
document.name = "#{document.name}!"
|
35
|
+
document.save
|
36
|
+
end
|
31
37
|
end
|