azeroth 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -3
  3. data/Dockerfile +6 -9
  4. data/README.md +31 -1
  5. data/azeroth.gemspec +12 -11
  6. data/config/yardstick.yml +1 -0
  7. data/docker-compose.yml +0 -8
  8. data/lib/azeroth/decorator/hash_builder.rb +3 -1
  9. data/lib/azeroth/decorator/key_value_extractor.rb +3 -8
  10. data/lib/azeroth/options.rb +24 -12
  11. data/lib/azeroth/params_builder.rb +83 -0
  12. data/lib/azeroth/request_handler/update.rb +3 -1
  13. data/lib/azeroth/request_handler.rb +1 -2
  14. data/lib/azeroth/resource_builder.rb +9 -10
  15. data/lib/azeroth/resourceable/builder.rb +16 -34
  16. data/lib/azeroth/resourceable/class_methods.rb +1 -130
  17. data/lib/azeroth/resourceable.rb +171 -4
  18. data/lib/azeroth/routes_builder.rb +8 -10
  19. data/lib/azeroth/version.rb +1 -1
  20. data/lib/azeroth.rb +1 -0
  21. data/spec/controllers/documents_with_error_controller_spec.rb +3 -1
  22. data/spec/dummy/app/models/movie/simple_decorator.rb +7 -0
  23. data/spec/dummy/app/models/movie.rb +5 -0
  24. data/spec/dummy/config/environments/development.rb +1 -0
  25. data/spec/dummy/db/schema.rb +4 -1
  26. data/spec/integration/yard/controllers/paginated_documents_controller_spec.rb +33 -0
  27. data/spec/lib/azeroth/decorator/key_value_extractor_spec.rb +3 -1
  28. data/spec/lib/azeroth/params_builder_spec.rb +58 -0
  29. data/spec/lib/azeroth/resource_builder_spec.rb +3 -1
  30. data/spec/lib/azeroth/resourceable_spec.rb +108 -0
  31. data/spec/lib/azeroth/routes_builder_spec.rb +3 -1
  32. data/spec/support/app/controllers/controller.rb +1 -1
  33. data/spec/support/app/controllers/params_builder_controller.rb +14 -0
  34. data/spec/support/factories/movie.rb +8 -0
  35. metadata +62 -34
  36. data/Dockerfile.circleci +0 -5
@@ -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
@@ -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
- # @param model [Model] resource interface
10
- # @param builder [Sinclair] methods builder
11
- # @param options [Option]
12
- def initialize(model, builder, options)
13
- @model = model
14
- @builder = builder
15
- @options = options
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Azeroth
4
- VERSION = '0.10.0'
4
+ VERSION = '1.0.0'
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'
@@ -128,7 +128,9 @@ describe DocumentsWithErrorController do
128
128
 
129
129
  it 'returns updated document json' do
130
130
  patch :update, params: parameters
131
- expect(response.body).to eq(expected_body)
131
+
132
+ expect(JSON.parse(response.body))
133
+ .to eq(JSON.parse(expected_body))
132
134
  end
133
135
 
134
136
  it do
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.configure do
4
+ config.hosts << 'www.example.com'
4
5
  # Settings specified here will take precedence over
5
6
  # those in config/application.rb.
6
7
 
@@ -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
- add_foreign_key 'pokemons', 'pokemon_masters'
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(decorator, attribute, options)
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) { described_class.new(model, 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(model, builder, options)
7
+ described_class.new(
8
+ model: model, builder: builder, options: options
9
+ )
8
10
  end
9
11
 
10
12
  let(:controller) { controller_class.new }
@@ -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
@@ -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