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