azeroth 0.8.2 → 0.10.1

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