azeroth 0.8.2 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>] List of
28
+ # @option options only [Array<Symbol,String>,Symbol,String] List of
31
29
  # actions to be built
32
- # @option options except [Array<Symbol,String>] List of
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Azeroth
4
- VERSION = '0.8.2'
4
+ VERSION = '0.10.1'
5
5
  end
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'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Movie
4
+ class SimpleDecorator < Azeroth::Decorator
5
+ expose :name
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Movie < ActiveRecord::Base
4
+ validates :name, :director, presence: true
5
+ end
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'action_controller'
4
4
 
5
- class Controller
5
+ class Controller < ActionController::Base
6
6
  def initialize(params = {})
7
7
  @params = ActionController::Parameters.new(params)
8
8
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ParamsBuilderController
4
+ def initialize(id, attributes)
5
+ @params = ActionController::Parameters.new(
6
+ id: id,
7
+ document: attributes
8
+ )
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :params
14
+ 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :movie, class: '::Movie' do
5
+ sequence(:name) { |n| "Name-#{n}" }
6
+ sequence(:director) { |n| "Director-#{n}" }
7
+ end
8
+ end