azeroth 0.10.0 → 1.0.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/.circleci/config.yml +3 -3
- data/Dockerfile +6 -9
- data/README.md +31 -1
- data/azeroth.gemspec +12 -11
- data/config/yardstick.yml +1 -0
- data/docker-compose.yml +0 -8
- data/lib/azeroth/decorator/hash_builder.rb +3 -1
- data/lib/azeroth/decorator/key_value_extractor.rb +3 -8
- data/lib/azeroth/options.rb +24 -12
- data/lib/azeroth/params_builder.rb +83 -0
- data/lib/azeroth/request_handler/update.rb +3 -1
- data/lib/azeroth/request_handler.rb +1 -2
- data/lib/azeroth/resource_builder.rb +9 -10
- data/lib/azeroth/resourceable/builder.rb +16 -34
- data/lib/azeroth/resourceable/class_methods.rb +1 -130
- data/lib/azeroth/resourceable.rb +171 -4
- data/lib/azeroth/routes_builder.rb +8 -10
- data/lib/azeroth/version.rb +1 -1
- data/lib/azeroth.rb +1 -0
- data/spec/controllers/documents_with_error_controller_spec.rb +3 -1
- data/spec/dummy/app/models/movie/simple_decorator.rb +7 -0
- data/spec/dummy/app/models/movie.rb +5 -0
- data/spec/dummy/config/environments/development.rb +1 -0
- data/spec/dummy/db/schema.rb +4 -1
- data/spec/integration/yard/controllers/paginated_documents_controller_spec.rb +33 -0
- data/spec/lib/azeroth/decorator/key_value_extractor_spec.rb +3 -1
- data/spec/lib/azeroth/params_builder_spec.rb +58 -0
- data/spec/lib/azeroth/resource_builder_spec.rb +3 -1
- data/spec/lib/azeroth/resourceable_spec.rb +108 -0
- data/spec/lib/azeroth/routes_builder_spec.rb +3 -1
- data/spec/support/app/controllers/controller.rb +1 -1
- data/spec/support/app/controllers/params_builder_controller.rb +14 -0
- data/spec/support/factories/movie.rb +8 -0
- metadata +62 -34
- data/Dockerfile.circleci +0 -5
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
|
@@ -5,15 +5,14 @@ module Azeroth
|
|
5
5
|
# @author Darthjee
|
6
6
|
#
|
7
7
|
# Builder resposible for adding routes methods to the controller
|
8
|
-
class RoutesBuilder
|
9
|
-
|
10
|
-
|
11
|
-
# @
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
8
|
+
class RoutesBuilder < Sinclair::Model
|
9
|
+
initialize_with(:model, :builder, :options, writter: false)
|
10
|
+
|
11
|
+
# @method initialize(model:, builder:, options:)
|
12
|
+
# @param model [Model] resource interface
|
13
|
+
# @param builder [Sinclair] methods builder
|
14
|
+
# @param options [Option]
|
15
|
+
# @return [RoutesBuilder]
|
17
16
|
|
18
17
|
# Append the routes methods to be built
|
19
18
|
#
|
@@ -26,7 +25,6 @@ module Azeroth
|
|
26
25
|
|
27
26
|
private
|
28
27
|
|
29
|
-
attr_reader :model, :builder, :options
|
30
28
|
# @method model
|
31
29
|
# @api private
|
32
30
|
# @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'
|
@@ -128,7 +128,9 @@ describe DocumentsWithErrorController do
|
|
128
128
|
|
129
129
|
it 'returns updated document json' do
|
130
130
|
patch :update, params: parameters
|
131
|
-
|
131
|
+
|
132
|
+
expect(JSON.parse(response.body))
|
133
|
+
.to eq(JSON.parse(expected_body))
|
132
134
|
end
|
133
135
|
|
134
136
|
it do
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -47,5 +47,8 @@ ActiveRecord::Schema.define do
|
|
47
47
|
t.index %i[pokemon_master_id favorite], unique: true
|
48
48
|
end
|
49
49
|
|
50
|
-
|
50
|
+
create_table :movies, force: true do |t|
|
51
|
+
t.string :name, null: false
|
52
|
+
t.string :director, null: false
|
53
|
+
end
|
51
54
|
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
|
@@ -4,7 +4,9 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe Azeroth::Decorator::KeyValueExtractor do
|
6
6
|
subject(:extractor) do
|
7
|
-
described_class.new(
|
7
|
+
described_class.new(
|
8
|
+
decorator: decorator, attribute: attribute, options: options
|
9
|
+
)
|
8
10
|
end
|
9
11
|
|
10
12
|
let(:decorator_class) { Class.new(Azeroth::Decorator) }
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Azeroth::ParamsBuilder do
|
6
|
+
subject(:params_builder) do
|
7
|
+
described_class.new(model: model, builder: builder)
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:model) { Azeroth::Model.new(:document, options) }
|
11
|
+
let(:options) { Azeroth::Options.new }
|
12
|
+
let(:builder) { Sinclair.new(klass) }
|
13
|
+
let(:klass) { Class.new(ParamsBuilderController) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
params_builder.append
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#append' do
|
20
|
+
it 'adds id method' do
|
21
|
+
expect { builder.build }
|
22
|
+
.to add_method(:document_id).to(klass)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'adds params method' do
|
26
|
+
expect { builder.build }
|
27
|
+
.to add_method(:document_params).to(klass)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'after the build' do
|
31
|
+
let(:controller) { klass.new(id, attributes) }
|
32
|
+
let(:document) { create(:document) }
|
33
|
+
let(:attributes) { document.attributes }
|
34
|
+
let(:id) { Random.rand(10..100) }
|
35
|
+
let(:expected_attributes) do
|
36
|
+
{
|
37
|
+
'name' => document.name,
|
38
|
+
'reference' => document.reference
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
before { builder.build }
|
43
|
+
|
44
|
+
context 'when requesting id' do
|
45
|
+
it 'returns id from request path' do
|
46
|
+
expect(controller.document_id).to eq(id)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when requesting params' do
|
51
|
+
it 'returns payload' do
|
52
|
+
expect(controller.document_params.to_h)
|
53
|
+
.to eq(expected_attributes)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -3,7 +3,9 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Azeroth::ResourceBuilder do
|
6
|
-
subject(:resource_builder)
|
6
|
+
subject(:resource_builder) do
|
7
|
+
described_class.new(model: model, builder: builder)
|
8
|
+
end
|
7
9
|
|
8
10
|
let(:model) { Azeroth::Model.new(:document, options) }
|
9
11
|
let(:options) { Azeroth::Options.new }
|
@@ -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
|
@@ -4,7 +4,9 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe Azeroth::RoutesBuilder do
|
6
6
|
subject(:routes_builder) do
|
7
|
-
described_class.new(
|
7
|
+
described_class.new(
|
8
|
+
model: model, builder: builder, options: options
|
9
|
+
)
|
8
10
|
end
|
9
11
|
|
10
12
|
let(:controller) { controller_class.new }
|