azeroth 0.10.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -3
  3. data/Dockerfile +1 -1
  4. data/README.md +43 -30
  5. data/azeroth.gemspec +8 -7
  6. data/config/check_specs.yml +1 -0
  7. data/lib/azeroth/decorator/class_methods.rb +249 -0
  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/decorator/method_builder.rb +23 -7
  11. data/lib/azeroth/decorator/options.rb +3 -1
  12. data/lib/azeroth/decorator.rb +30 -188
  13. data/lib/azeroth/params_builder.rb +11 -8
  14. data/lib/azeroth/request_handler/index.rb +7 -33
  15. data/lib/azeroth/request_handler/pagination.rb +53 -0
  16. data/lib/azeroth/request_handler.rb +8 -7
  17. data/lib/azeroth/resource_builder.rb +9 -10
  18. data/lib/azeroth/resourceable/builder.rb +15 -11
  19. data/lib/azeroth/routes_builder.rb +8 -10
  20. data/lib/azeroth/version.rb +1 -1
  21. data/spec/controllers/documents_with_error_controller_spec.rb +3 -1
  22. data/spec/dummy/app/models/website/decorator.rb +11 -0
  23. data/spec/dummy/app/models/website/with_location.rb +21 -0
  24. data/spec/dummy/app/models/website.rb +4 -0
  25. data/spec/dummy/config/environments/development.rb +1 -0
  26. data/spec/dummy/db/schema.rb +5 -1
  27. data/spec/integration/yard/azeroth/decorator_spec.rb +15 -0
  28. data/spec/lib/azeroth/decorator/key_value_extractor_spec.rb +3 -1
  29. data/spec/lib/azeroth/decorator/method_builder_spec.rb +61 -2
  30. data/spec/lib/azeroth/decorator_spec.rb +96 -3
  31. data/spec/lib/azeroth/params_builder_spec.rb +3 -1
  32. data/spec/lib/azeroth/request_handler/pagination_spec.rb +133 -0
  33. data/spec/lib/azeroth/resource_builder_spec.rb +3 -1
  34. data/spec/lib/azeroth/routes_builder_spec.rb +3 -1
  35. metadata +41 -17
@@ -40,200 +40,42 @@ module Azeroth
40
40
  # # 'pokemon' => 'Arcanine'
41
41
  # # }
42
42
  class Decorator
43
+ autoload :ClassMethods, 'azeroth/decorator/class_methods'
43
44
  autoload :HashBuilder, 'azeroth/decorator/hash_builder'
44
45
  autoload :KeyValueExtractor, 'azeroth/decorator/key_value_extractor'
45
46
  autoload :MethodBuilder, 'azeroth/decorator/method_builder'
46
47
  autoload :Options, 'azeroth/decorator/options'
47
48
 
48
- class << self
49
- # @api private
50
- #
51
- # All attributes exposed
52
- #
53
- # @return [Hash<Symbol,Hash>]
54
- def attributes_map
55
- @attributes_map ||= build_attributes_map
56
- end
57
-
58
- private
59
-
60
- # @api private
61
- # @private
62
- #
63
- # Initialize attributes to be exposed map
64
- #
65
- # When the class inherity from another
66
- # decorator, the new class should expose the
67
- # same attributes.
68
- #
69
- # @return [Hash<Symbol,Hash>]
70
- def build_attributes_map
71
- superclass.try(:attributes_map).dup || {}
72
- end
73
-
74
- # @visibility public
75
- # @api public
76
- # @private
77
- #
78
- # Expose attributes on json decorated
79
- #
80
- # @param attribute [Symbol,String] attribute to be exposed
81
- # @param options_hash [Hash] exposing options
82
- # @option options_hash as [Symbol,String] custom key
83
- # to expose
84
- # @option options_hash if [Symbol,Proc] method/block
85
- # to be called
86
- # checking if an attribute should or should not
87
- # be exposed
88
- # @option options_hash decorator [FalseClass,TrueClass,Class]
89
- # flag to use or not a decorator or decorator class to be used
90
- #
91
- # @return [Array<Symbol>]
92
- #
93
- # @example
94
- # class DummyModel
95
- # include ActiveModel::Model
96
- #
97
- # attr_accessor :id, :first_name, :last_name, :age,
98
- # :favorite_pokemon
99
- #
100
- # class Decorator < Azeroth::Decorator
101
- # expose :name
102
- # expose :age
103
- # expose :favorite_pokemon, as: :pokemon
104
- #
105
- # def name
106
- # [object.first_name, object.last_name].join(' ')
107
- # end
108
- # end
109
- # end
110
- #
111
- # @example With relations
112
- # # pokemon/decorator.rb
113
- #
114
- # class Pokemon::Decorator < Azeroth::Decorator
115
- # expose :name
116
- # expose :previous_form_name, as: :evolution_of, if: :evolution?
117
- #
118
- # def evolution?
119
- # previous_form
120
- # end
121
- #
122
- # def previous_form_name
123
- # previous_form.name
124
- # end
125
- # end
126
- #
127
- # # pokemon/favorite_decorator.rb
128
- #
129
- # class Pokemon::FavoriteDecorator < Pokemon::Decorator
130
- # expose :nickname
131
- # end
132
- #
133
- # # pokemon_master/decorator.rb
134
- #
135
- # class PokemonMaster < ActiveRecord::Base
136
- # has_one :favorite_pokemon, -> { where(favorite: true) },
137
- # class_name: 'Pokemon'
138
- # has_many :pokemons
139
- # end
140
- #
141
- # # pokemon.rb
142
- #
143
- # class Pokemon < ActiveRecord::Base
144
- # belongs_to :pokemon_master
145
- # has_one :previous_form,
146
- # class_name: 'Pokemon',
147
- # foreign_key: :previous_form_id
148
- # end
149
- #
150
- # # pokemon_master.rb
151
- #
152
- # class PokemonMaster::Decorator < Azeroth::Decorator
153
- # expose :name
154
- # expose :age
155
- # expose :favorite_pokemon, decorator: Pokemon::FavoriteDecorator
156
- # expose :pokemons
157
- #
158
- # def name
159
- # [
160
- # first_name,
161
- # last_name
162
- # ].compact.join(' ')
163
- # end
164
- # end
165
- #
166
- # # schema.rb
167
- #
168
- # ActiveRecord::Schema.define do
169
- # self.verbose = false
170
- #
171
- # create_table :pokemon_masters, force: true do |t|
172
- # t.string :first_name, null: false
173
- # t.string :last_name
174
- # t.integer :age, null: false
175
- # end
176
- #
177
- # create_table :pokemons, force: true do |t|
178
- # t.string :name, null: false
179
- # t.string :nickname
180
- # t.integer :pokemon_master_id
181
- # t.boolean :favorite
182
- # t.integer :previous_form_id
183
- # t.index %i[pokemon_master_id favorite], unique: true
184
- # end
185
- #
186
- # add_foreign_key 'pokemons', 'pokemon_masters'
187
- # end
188
- #
189
- # # test.rb
190
- #
191
- # master = PokemonMaster.create(
192
- # first_name: 'Ash',
193
- # last_name: 'Ketchum',
194
- # age: 10
195
- # )
196
- #
197
- # master.create_favorite_pokemon(
198
- # name: 'pikachu',
199
- # nickname: 'Pikachu'
200
- # )
201
- #
202
- # metapod = Pokemon.create(name: :metapod)
203
- #
204
- # master.pokemons.create(
205
- # name: 'butterfree', previous_form: metapod
206
- # )
207
- # master.pokemons.create(name: 'squirtle')
208
- #
209
- # decorator = PokemonMaster::Decorator.new(master)
210
- #
211
- # decorator.as_json
212
- # # returns
213
- # # {
214
- # # 'age' => 10,
215
- # # 'name' => 'Ash Ketchum',
216
- # # 'favorite_pokemon' => {
217
- # # 'name' => 'pikachu',
218
- # # 'nickname' => 'Pikachu'
219
- # # },
220
- # # 'pokemons' => [{
221
- # # 'name' => 'butterfree',
222
- # # 'evolution_of' => 'metapod'
223
- # # }, {
224
- # # 'name' => 'squirtle'
225
- # # }, {
226
- # # 'name' => 'pikachu'
227
- # # }]
228
- # # }
229
- def expose(attribute, **options_hash)
230
- options = Decorator::Options.new(options_hash)
49
+ extend ClassMethods
231
50
 
232
- MethodBuilder.build_reader(self, attribute)
233
-
234
- attributes_map[attribute] = options
235
- end
236
- end
51
+ # @method self.expose(attribute, **options_hash)
52
+ # @visibility public
53
+ # @api public
54
+ # @private
55
+ #
56
+ # Expose attributes on json decorated
57
+ #
58
+ # @param attribute [Symbol,String] attribute to be exposed
59
+ # @param options_hash [Hash] exposing options
60
+ # @option options_hash as [Symbol,String] custom key
61
+ # to expose the value as
62
+ # @option options_hash if [Symbol,Proc] method/block
63
+ # to be called
64
+ # checking if an attribute should or should not
65
+ # be exposed
66
+ # @option options_hash decorator [FalseClass,TrueClass,Class]
67
+ # flag to use or not a decorator or decorator class to be used
68
+ # @option options_hash reader [Boolean] Flag indicating if a reader
69
+ # to access the attribute should be created. usefull if you want
70
+ # method_missing to take over
71
+ # @option options_hash override [Boolean] Flag indicating if an
72
+ # existing method should be overriden.
73
+ # This is useful when a method acessor was included from another module
74
+ #
75
+ # @return [Array<Symbol>]
76
+ #
77
+ # @see Decorator::ClassMethods#expose Decorator::ClassMethods#expose
78
+ # for examples
237
79
 
238
80
  # @api private
239
81
  #
@@ -5,13 +5,15 @@ module Azeroth
5
5
  # @author Darthjee
6
6
  #
7
7
  # Class responsible for adding params handling methods to a controller
8
- class ParamsBuilder
9
- # @param model [Model] resource interface
10
- # @param builder [Sinclair] methods builder
11
- def initialize(model, builder)
12
- @model = model
13
- @builder = builder
14
- end
8
+ class ParamsBuilder < Sinclair::Model
9
+ # @!method initialize(model:, builder:)
10
+ # @api private
11
+ #
12
+ # @param model [Model] Resource interface
13
+ # @param builder [Sinclair] Methods builder
14
+ #
15
+ # @return [ParamsBuilder]
16
+ initialize_with(:model, :builder, writter: false)
15
17
 
16
18
  # Append the params methods to be built
17
19
  #
@@ -27,7 +29,8 @@ module Azeroth
27
29
  end
28
30
  end
29
31
 
30
- attr_reader :model, :builder
32
+ private
33
+
31
34
  # @method model
32
35
  # @api private
33
36
  # @private
@@ -8,7 +8,13 @@ module Azeroth
8
8
  class Index < RequestHandler
9
9
  private
10
10
 
11
- delegate :paginated?, :per_page, to: :options
11
+ delegate :per_page, :offset, :limit,
12
+ :current_page, to: :pagination
13
+ delegate :paginated?, to: :options
14
+
15
+ def pagination
16
+ @pagination ||= Pagination.new(params, options)
17
+ end
12
18
 
13
19
  # @private
14
20
  #
@@ -45,29 +51,6 @@ module Azeroth
45
51
  scoped_entries.offset(offset).limit(limit)
46
52
  end
47
53
 
48
- # @private
49
- #
50
- # offest used in pagination
51
- #
52
- # offset is retrieved from +params[:per_page]+
53
- # or +options.per_page+
54
- #
55
- # @return [Integer]
56
- def offset
57
- (current_page - 1) * limit
58
- end
59
-
60
- # @private
61
- #
62
- # limit used in pagination
63
- #
64
- # limit is retrieved from +params[:page]+
65
- #
66
- # @return [Integer]
67
- def limit
68
- (params[:per_page] || per_page).to_i
69
- end
70
-
71
54
  # @private
72
55
  #
73
56
  # default scope of elements
@@ -77,15 +60,6 @@ module Azeroth
77
60
  controller.send(model.plural)
78
61
  end
79
62
 
80
- # @private
81
- #
82
- # calculates current page
83
- #
84
- # @return [Integer]
85
- def current_page
86
- (params[:page] || 1).to_i
87
- end
88
-
89
63
  # @private
90
64
  #
91
65
  # calculates how many pages are there
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Azeroth
4
+ class RequestHandler
5
+ # @api private
6
+ #
7
+ # Wrapper for request finding pagination attributes
8
+ class Pagination
9
+ def initialize(params, options)
10
+ @params = params
11
+ @options = options
12
+ end
13
+
14
+ delegate :per_page, to: :options
15
+
16
+ # @private
17
+ #
18
+ # offest used in pagination
19
+ #
20
+ # offset is retrieved from +params[:per_page]+
21
+ # or +options.per_page+
22
+ #
23
+ # @return [Integer]
24
+ def offset
25
+ (current_page - 1) * limit
26
+ end
27
+
28
+ # @private
29
+ #
30
+ # limit used in pagination
31
+ #
32
+ # limit is retrieved from +params[:page]+
33
+ #
34
+ # @return [Integer]
35
+ def limit
36
+ (params[:per_page] || per_page).to_i
37
+ end
38
+
39
+ # @private
40
+ #
41
+ # calculates current page
42
+ #
43
+ # @return [Integer]
44
+ def current_page
45
+ (params[:page] || 1).to_i
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :params, :options
51
+ end
52
+ end
53
+ end
@@ -8,13 +8,14 @@ module Azeroth
8
8
  # Request handler sends messages to the controller
9
9
  # in order to get the resource and rendering the response
10
10
  class RequestHandler
11
- autoload :Create, 'azeroth/request_handler/create'
12
- autoload :Destroy, 'azeroth/request_handler/destroy'
13
- autoload :Edit, 'azeroth/request_handler/edit'
14
- autoload :Index, 'azeroth/request_handler/index'
15
- autoload :New, 'azeroth/request_handler/new'
16
- autoload :Show, 'azeroth/request_handler/show'
17
- autoload :Update, 'azeroth/request_handler/update'
11
+ autoload :Create, 'azeroth/request_handler/create'
12
+ autoload :Destroy, 'azeroth/request_handler/destroy'
13
+ autoload :Edit, 'azeroth/request_handler/edit'
14
+ autoload :Index, 'azeroth/request_handler/index'
15
+ autoload :New, 'azeroth/request_handler/new'
16
+ autoload :Pagination, 'azeroth/request_handler/pagination'
17
+ autoload :Show, 'azeroth/request_handler/show'
18
+ autoload :Update, 'azeroth/request_handler/update'
18
19
 
19
20
  # @param controller [ApplicationController]
20
21
  # @param model [Azeroth::Model]
@@ -9,13 +9,15 @@ module Azeroth
9
9
  # The builder adds 2 methods, one for listing all
10
10
  # entries of a resource, and one for fetching an specific
11
11
  # entry
12
- class ResourceBuilder
13
- # @param model [Model] Resource model interface
14
- # @param builder [Sinclair] method builder
15
- def initialize(model, builder)
16
- @model = model
17
- @builder = builder
18
- end
12
+ class ResourceBuilder < Sinclair::Model
13
+ # @!method initialize(model:, builder:)
14
+ # @api private
15
+ #
16
+ # @param model [Model] Resource model interface
17
+ # @param builder [Sinclair] method builder
18
+ #
19
+ # @return [ResourceBuilder]
20
+ initialize_with(:model, :builder, writter: false)
19
21
 
20
22
  # Append methods to be built to the builder
21
23
  #
@@ -28,9 +30,6 @@ module Azeroth
28
30
  add_method(name, "@#{name} ||= #{plural}.find(#{name}_id)")
29
31
  end
30
32
 
31
- private
32
-
33
- attr_reader :model, :builder
34
33
  # @method model
35
34
  # @api private
36
35
  # @private
@@ -13,12 +13,12 @@ module Azeroth
13
13
  # @see ResourceRouteBuilder
14
14
  # @see RoutesBuilder
15
15
  class Builder
16
- # @param clazz [ActionController::Base] Controller to
16
+ # @param klass [ActionController::Base] Controller to
17
17
  # to be changed
18
18
  # @param model_name [Symbol,String]
19
19
  # @param options [Options]
20
- def initialize(clazz, model_name, options)
21
- @clazz = clazz
20
+ def initialize(klass, model_name, options)
21
+ @klass = klass
22
22
  @options = options
23
23
  @model = Azeroth::Model.new(model_name, options)
24
24
 
@@ -30,8 +30,8 @@ module Azeroth
30
30
 
31
31
  private
32
32
 
33
- attr_reader :clazz, :model, :options
34
- # @method clazz
33
+ attr_reader :klass, :model, :options
34
+ # @method klass
35
35
  # @api private
36
36
  # @private
37
37
  #
@@ -89,36 +89,40 @@ module Azeroth
89
89
  #
90
90
  # @see https://www.rubydoc.info/gems/sinclair Sinclair
91
91
  def builder
92
- @builder ||= Sinclair.new(clazz)
92
+ @builder ||= Sinclair.new(klass)
93
93
  end
94
94
 
95
95
  # Add methods for id and parameters
96
96
  #
97
97
  # @return [Array<Sinclair::MethodDefinition>]
98
98
  def add_params
99
- ParamsBuilder.new(model, builder).append
99
+ ParamsBuilder.new(
100
+ model: model, builder: builder
101
+ ).append
100
102
  end
101
103
 
102
104
  # Add methods for resource fetching
103
105
  #
104
106
  # @return [Array<Sinclair::MethodDefinition>]
105
107
  def add_resource
106
- ResourceBuilder.new(model, builder).append
108
+ ResourceBuilder.new(model: model, builder: builder).append
107
109
  end
108
110
 
109
111
  # Add metohods for each route
110
112
  #
111
113
  # @return [Array<Sinclair::MethodDefinition>]
112
114
  def add_routes
113
- RoutesBuilder.new(model, builder, options).append
115
+ RoutesBuilder.new(
116
+ model: model, builder: builder, options: options
117
+ ).append
114
118
  end
115
119
 
116
120
  # Add helpers to render objects on template
117
121
  #
118
122
  # @return [String]
119
123
  def add_helpers
120
- clazz.public_send(:helper_method, model.name)
121
- clazz.public_send(:helper_method, model.plural)
124
+ klass.public_send(:helper_method, model.name)
125
+ klass.public_send(:helper_method, model.plural)
122
126
  end
123
127
  end
124
128
  end
@@ -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.1'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Website < ActiveRecord::Base
4
+ class Decorator < Azeroth::Decorator
5
+ include WithLocation
6
+
7
+ expose :location, override: false
8
+
9
+ alias website object
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Website < ActiveRecord::Base
4
+ module WithLocation
5
+ def location
6
+ "#{protocol}://#{domain}:#{port}"
7
+ end
8
+
9
+ def protocol
10
+ website.protocol || '*'
11
+ end
12
+
13
+ def domain
14
+ website.domain || '*'
15
+ end
16
+
17
+ def port
18
+ website.port || '*'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Website < ActiveRecord::Base
4
+ 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
 
@@ -52,5 +52,9 @@ ActiveRecord::Schema.define do
52
52
  t.string :director, null: false
53
53
  end
54
54
 
55
- add_foreign_key 'pokemons', 'pokemon_masters'
55
+ create_table :websites, force: true do |t|
56
+ t.string :domain, null: false
57
+ t.integer :port, limit: 2, unsigned: true
58
+ t.string :protocol, limit: 5
59
+ end
56
60
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'spec_helper'
4
+
3
5
  describe Azeroth::Decorator do
4
6
  describe 'yard' do
5
7
  describe '#as_json' do
@@ -60,6 +62,19 @@ describe Azeroth::Decorator do
60
62
  }
61
63
  )
62
64
  end
65
+
66
+ it 'example without override' do
67
+ website = Website.create(
68
+ protocol: :http,
69
+ domain: 'google.com'
70
+ )
71
+
72
+ decorator = Website::Decorator.new(website)
73
+
74
+ expect(decorator.as_json).to eq(
75
+ 'location' => 'http://google.com:*'
76
+ )
77
+ end
63
78
  end
64
79
  end
65
80
  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) }