azeroth 0.6.3 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +37 -2
  3. data/.rubocop.yml +11 -0
  4. data/Dockerfile +2 -2
  5. data/README.md +164 -1
  6. data/azeroth.gemspec +6 -5
  7. data/lib/azeroth.rb +1 -0
  8. data/lib/azeroth/decorator.rb +123 -0
  9. data/lib/azeroth/decorator/key_value_extractor.rb +2 -2
  10. data/lib/azeroth/decorator/options.rb +3 -3
  11. data/lib/azeroth/model.rb +3 -1
  12. data/lib/azeroth/options.rb +38 -1
  13. data/lib/azeroth/request_handler.rb +39 -2
  14. data/lib/azeroth/request_handler/create.rb +31 -5
  15. data/lib/azeroth/request_handler/update.rb +3 -3
  16. data/lib/azeroth/resourceable.rb +27 -0
  17. data/lib/azeroth/resourceable/class_methods.rb +120 -6
  18. data/lib/azeroth/routes_builder.rb +2 -1
  19. data/lib/azeroth/version.rb +1 -1
  20. data/spec/controllers/pokemon_masters_controller_spec.rb +56 -0
  21. data/spec/controllers/pokemons_controller_spec.rb +56 -0
  22. data/spec/dummy/app/controllers/games_controller.rb +22 -0
  23. data/spec/dummy/app/controllers/pokemon_masters_controller.rb +9 -0
  24. data/spec/dummy/app/controllers/pokemons_controller.rb +27 -0
  25. data/spec/dummy/app/controllers/publishers_controller.rb +8 -0
  26. data/spec/dummy/app/models/game.rb +5 -0
  27. data/spec/dummy/app/models/game/decorator.rb +9 -0
  28. data/spec/dummy/app/models/name_decorator.rb +5 -0
  29. data/spec/dummy/app/models/pokemon.rb +8 -0
  30. data/spec/dummy/app/models/pokemon/decorator.rb +16 -0
  31. data/spec/dummy/app/models/pokemon/favorite_decorator.rb +7 -0
  32. data/spec/dummy/app/models/pokemon_master.rb +7 -0
  33. data/spec/dummy/app/models/pokemon_master/decorator.rb +17 -0
  34. data/spec/dummy/app/models/publisher.rb +5 -0
  35. data/spec/dummy/config/routes.rb +8 -0
  36. data/spec/dummy/db/schema.rb +26 -0
  37. data/spec/integration/readme/azeroth/decorator_spec.rb +48 -0
  38. data/spec/integration/readme/controllers/games_controller_spec.rb +42 -0
  39. data/spec/integration/yard/azeroth/decorator_spec.rb +58 -17
  40. data/spec/integration/yard/controllers/games_controller_spec.rb +42 -0
  41. data/spec/lib/azeroth/decorator/key_value_extractor_spec.rb +32 -0
  42. data/spec/lib/azeroth/decorator_spec.rb +28 -2
  43. data/spec/lib/azeroth/request_handler/create_spec.rb +166 -0
  44. data/spec/lib/azeroth/request_handler/update_spec.rb +91 -0
  45. data/spec/lib/azeroth/request_handler_spec.rb +3 -1
  46. data/spec/support/app/controllers/request_handler_controller.rb +12 -1
  47. data/spec/support/factories/game.rb +8 -0
  48. data/spec/support/factories/pokemon.rb +8 -0
  49. data/spec/support/factories/pokemon_master.rb +9 -0
  50. data/spec/support/factories/publisher.rb +7 -0
  51. data/spec/support/shared_examples/request_handler.rb +5 -2
  52. metadata +70 -12
@@ -13,7 +13,7 @@ module Azeroth
13
13
  DEFAULT_OPTIONS = {
14
14
  as: nil,
15
15
  if: nil,
16
- decorator: nil
16
+ decorator: true
17
17
  }.freeze
18
18
 
19
19
  with_options DEFAULT_OPTIONS
@@ -62,8 +62,8 @@ module Azeroth
62
62
  # #as_json call on value
63
63
  #
64
64
  # @return [TrueClass,FalseClass]
65
- def any_decorator?
66
- decorator != false
65
+ def decorator_defined?
66
+ decorator.is_a?(Class)
67
67
  end
68
68
  end
69
69
  end
@@ -62,6 +62,8 @@ module Azeroth
62
62
  #
63
63
  # @return [Azeroth::Options]
64
64
 
65
+ delegate :decorator, to: :options
66
+
65
67
  # @private
66
68
  #
67
69
  # Returns decorator class for the object
@@ -85,7 +87,7 @@ module Azeroth
85
87
  # @return [Azeroth::Decorator,DummyDecorator]
86
88
  def calculate_decorator_class
87
89
  return DummyDecorator unless options.decorator
88
- return options.decorator if options.decorator.is_a?(Class)
90
+ return decorator if decorator.is_a?(Class)
89
91
 
90
92
  klass::Decorator
91
93
  rescue NameError
@@ -10,10 +10,16 @@ module Azeroth
10
10
  # Sinclair::Options
11
11
  class Options < Sinclair::Options
12
12
  # Default options
13
+ #
14
+ # @api public
15
+ #
16
+ # @see Resourceable::ClassMethods#resource_for
13
17
  DEFAULT_OPTIONS = {
14
18
  only: %i[create destroy edit index new show update],
15
19
  except: [],
16
- decorator: true
20
+ decorator: true,
21
+ before_save: nil,
22
+ build_with: nil
17
23
  }.freeze
18
24
 
19
25
  with_options DEFAULT_OPTIONS
@@ -25,6 +31,19 @@ module Azeroth
25
31
  [only].flatten.map(&:to_sym) - [except].flatten.map(&:to_sym)
26
32
  end
27
33
 
34
+ # Returns event dispatcher
35
+ #
36
+ # Event dispatcher is responsible for
37
+ # sending events such as +before_save+
38
+ # to it's correct calling point
39
+ #
40
+ # @return [Jace::Dispatcher]
41
+ def event_dispatcher(event)
42
+ Jace::Dispatcher.new(
43
+ before: try("before_#{event}")
44
+ )
45
+ end
46
+
28
47
  # @method only
29
48
  # @api private
30
49
  #
@@ -50,5 +69,23 @@ module Azeroth
50
69
  # model.as_json
51
70
  #
52
71
  # @return [Decorator,TrueClass,FalseClass]
72
+
73
+ # @method before_save
74
+ # @api private
75
+ #
76
+ # Block or method name to be run before save
77
+ #
78
+ # The given method or block will be ran
79
+ # before committing changes in models
80
+ # to database
81
+ #
82
+ # @return [Symbol,Proc]
83
+
84
+ # @method build_with
85
+ # @api private
86
+ #
87
+ # Block or method name to be ran when building the resource
88
+ #
89
+ # @return [Symbol,Proc]
53
90
  end
54
91
  end
@@ -18,9 +18,10 @@ module Azeroth
18
18
 
19
19
  # @param controller [ApplicationController]
20
20
  # @param model [Azeroth::Model]
21
- def initialize(controller, model)
21
+ def initialize(controller, model, options)
22
22
  @controller = controller
23
23
  @model = model
24
+ @options = options
24
25
  end
25
26
 
26
27
  # process the request
@@ -44,7 +45,7 @@ module Azeroth
44
45
 
45
46
  private
46
47
 
47
- attr_reader :controller, :model
48
+ attr_reader :controller, :model, :options
48
49
  # @method controller
49
50
  # @api private
50
51
  # @private
@@ -61,6 +62,14 @@ module Azeroth
61
62
  #
62
63
  # @return [Azeroth::Model]
63
64
 
65
+ # @method options
66
+ # @api private
67
+ # @private
68
+ #
69
+ # Handling options
70
+ #
71
+ # @return [Azeroth::Options]
72
+
64
73
  delegate :params, to: :controller
65
74
  # @method params
66
75
  # @api private
@@ -104,5 +113,33 @@ module Azeroth
104
113
  def status
105
114
  :ok
106
115
  end
116
+
117
+ # @private
118
+ #
119
+ # Run a block triggering the event
120
+ #
121
+ # @return [Object] Result of given block
122
+ def trigger_event(event, &block)
123
+ options.event_dispatcher(event)
124
+ .dispatch(controller, &block)
125
+ end
126
+
127
+ # @private
128
+ #
129
+ # Attributes to be used on resource creating
130
+ #
131
+ # @return [Hash]
132
+ def attributes
133
+ @attributes ||= controller.send("#{model.name}_params")
134
+ end
135
+
136
+ # @private
137
+ #
138
+ # Collection scope of the resource
139
+ #
140
+ # @return [ActiveRecord::Relation]
141
+ def collection
142
+ @collection = controller.send(model.plural)
143
+ end
107
144
  end
108
145
  end
@@ -8,6 +8,8 @@ module Azeroth
8
8
  class Create < RequestHandler
9
9
  private
10
10
 
11
+ delegate :build_with, to: :options
12
+
11
13
  # @private
12
14
  #
13
15
  # Creates and return an instance of the model
@@ -17,16 +19,40 @@ module Azeroth
17
19
  #
18
20
  # @return [Object]
19
21
  def resource
20
- @resource ||= build_resource
22
+ @resource ||= build_and_save_resource
21
23
  end
22
24
 
23
- # build resource for create
25
+ # @private
26
+ #
27
+ # build resource for create and save it
24
28
  #
25
29
  # @return [Object]
30
+ def build_and_save_resource
31
+ @resource = build_resource
32
+ controller.instance_variable_set("@#{model.name}", resource)
33
+
34
+ trigger_event(:save) do
35
+ resource.tap(&:save)
36
+ end
37
+ end
38
+
39
+ # @private
40
+ #
41
+ # build resource without saving it
42
+ #
43
+ # when +build_with+ option is given, the proc/method
44
+ # is called instead of collection.build
45
+ #
46
+ # @return [Object] resource built
26
47
  def build_resource
27
- attributes = controller.send("#{model.name}_params")
28
- collection = controller.send(model.plural)
29
- collection.create(attributes)
48
+ return collection.build(attributes) unless build_with
49
+
50
+ case build_with
51
+ when Proc
52
+ controller.instance_eval(&build_with)
53
+ else
54
+ controller.send(build_with)
55
+ end
30
56
  end
31
57
 
32
58
  # @private
@@ -24,10 +24,10 @@ module Azeroth
24
24
  #
25
25
  # @return [Object]
26
26
  def update_resource
27
- attributes = controller.send("#{model.name}_params")
28
-
29
27
  controller.send(model.name).tap do |entry|
30
- entry.update(attributes)
28
+ trigger_event(:save) do
29
+ entry.update(attributes)
30
+ end
31
31
  end
32
32
  end
33
33
 
@@ -7,6 +7,10 @@ module Azeroth
7
7
  # @author Darthjee
8
8
  #
9
9
  # Concern for building controller methods for the routes
10
+ #
11
+ # @example (see Resourceable::ClassMethods#resource_for)
12
+ #
13
+ # @see Resourceable::ClassMethods
10
14
  module Resourceable
11
15
  extend ActiveSupport::Concern
12
16
 
@@ -17,6 +21,29 @@ module Azeroth
17
21
  autoload :Builder, 'azeroth/resourceable/builder'
18
22
  autoload :ClassMethods, 'azeroth/resourceable/class_methods'
19
23
 
24
+ class << self
25
+ # @method self.resource_for(name, **options)
26
+ # @api public
27
+ #
28
+ # @param name [String, Symbol] Name of the resource
29
+ # @param options [Hash] resource building options
30
+ # @option options only [Array<Symbol,String>] List of
31
+ # actions to be built
32
+ # @option options except [Array<Symbol,String>] List of
33
+ # actions to not to be built
34
+ # @option options decorator [Azeroth::Decorator,TrueClass,FalseClass]
35
+ # Decorator class or flag allowing/disallowing decorators
36
+ # @option options before_save [Symbol,Proc] method/block
37
+ # to be ran on the controller before saving the resource
38
+ # @option options build_with [Symbol,Proc] method/block
39
+ # to be ran when building resource
40
+ # (default proc { <resource_collection>.build(resource_params) }
41
+ #
42
+ # @return [Array<MethodDefinition>] list of methods created
43
+ #
44
+ # @see Options::DEFAULT_OPTIONS
45
+ end
46
+
20
47
  private
21
48
 
22
49
  # @api private
@@ -9,13 +9,11 @@ module Azeroth
9
9
  module ClassMethods
10
10
  # Adds resource methods for resource
11
11
  #
12
- # @param name [String, Symbol] Name of the resource
13
- # @param (see Options#initialize)
14
- # @option (see Options#initialize)
12
+ # @param (see Resourceable.resource_for)
13
+ # @option (see Resourceable.resource_for)
14
+ # @return (see Resourceable.resource_for)
15
15
  #
16
- # @return [Array<MethodDefinition>] list of methods created
17
- #
18
- # @see Options
16
+ # @see (see Resourceable.resource_for)
19
17
  #
20
18
  # @example Controller without delete
21
19
  # class DocumentsController < ApplicationController
@@ -30,6 +28,122 @@ module Azeroth
30
28
  #
31
29
  # resource_for :document, only: %w[create index show]
32
30
  # end
31
+ #
32
+ # @example complete example gmaes and publishers
33
+ # class PublishersController < ApplicationController
34
+ # include Azeroth::Resourceable
35
+ # skip_before_action :verify_authenticity_token
36
+ #
37
+ # resource_for :publisher, only: %i[create index]
38
+ # end
39
+ #
40
+ # class GamesController < ApplicationController
41
+ # include Azeroth::Resourceable
42
+ # skip_before_action :verify_authenticity_token
43
+ #
44
+ # resource_for :game, except: :delete
45
+ #
46
+ # private
47
+ #
48
+ # def games
49
+ # publisher.games
50
+ # end
51
+ #
52
+ # def publisher
53
+ # @publisher ||= Publisher.find(publisher_id)
54
+ # end
55
+ #
56
+ # def publisher_id
57
+ # params.require(:publisher_id)
58
+ # end
59
+ # end
60
+ #
61
+ # ActiveRecord::Schema.define do
62
+ # self.verbose = false
63
+ #
64
+ # create_table :publishers, force: true do |t|
65
+ # t.string :name
66
+ # end
67
+ #
68
+ # create_table :games, force: true do |t|
69
+ # t.string :name
70
+ # t.integer :publisher_id
71
+ # end
72
+ # end
73
+ #
74
+ # class Publisher < ActiveRecord::Base
75
+ # has_many :games
76
+ # end
77
+ #
78
+ # class Game < ActiveRecord::Base
79
+ # belongs_to :publisher
80
+ # end
81
+ #
82
+ # class Game::Decorator < Azeroth::Decorator
83
+ # expose :id
84
+ # expose :name
85
+ # expose :publisher, decorator: NameDecorator
86
+ # end
87
+ #
88
+ # @example requesting games and publishers
89
+ # post "/publishers.json", params: {
90
+ # publisher: {
91
+ # name: 'Nintendo'
92
+ # }
93
+ # }
94
+ #
95
+ # publisher = JSON.parse(response.body)
96
+ # # returns
97
+ # # {
98
+ # # 'id' => 11,
99
+ # # 'name' => 'Nintendo'
100
+ # # }
101
+ #
102
+ # publisher = Publisher.last
103
+ # post "/publishers/#{publisher['id']}/games.json", params: {
104
+ # game: {
105
+ # name: 'Pokemon'
106
+ # }
107
+ # }
108
+ #
109
+ # game = Game.last
110
+ #
111
+ # JSON.parse(response.body)
112
+ # # returns
113
+ # # {
114
+ # # id: game.id,
115
+ # # name: 'Pokemon',
116
+ # # publisher: {
117
+ # # name: 'Nintendo'
118
+ # # }
119
+ # }
120
+ #
121
+ # @example Controller with before_save
122
+ # class PokemonsController < ApplicationController
123
+ # include Azeroth::Resourceable
124
+ #
125
+ # resource_for :pokemon,
126
+ # only: %i[create update],
127
+ # before_save: :set_favorite
128
+ #
129
+ # private
130
+ #
131
+ # def set_favorite
132
+ # pokemon.favorite = true
133
+ # end
134
+ #
135
+ # def pokemons
136
+ # master.pokemons
137
+ # end
138
+ #
139
+ # def master
140
+ # @master ||= PokemonMaster.find(master_id)
141
+ # end
142
+ #
143
+ # def master_id
144
+ # params.require(:pokemon_master_id)
145
+ # end
146
+ # end
33
147
  def resource_for(name, **options)
34
148
  Builder.new(
35
149
  self, name, Azeroth::Options.new(options)
@@ -62,13 +62,14 @@ module Azeroth
62
62
 
63
63
  def route_code(route)
64
64
  model_interface = model
65
+ options_object = options
65
66
  handler_class = Azeroth::RequestHandler.const_get(
66
67
  route.to_s.capitalize
67
68
  )
68
69
 
69
70
  proc do
70
71
  handler_class.new(
71
- self, model_interface
72
+ self, model_interface, options_object
72
73
  ).process
73
74
  end
74
75
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Azeroth
4
- VERSION = '0.6.3'
4
+ VERSION = '0.7.2'
5
5
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe PokemonMastersController do
6
+ describe 'POST create' do
7
+ let(:parameters) do
8
+ {
9
+ format: :json,
10
+ pokemon_master: {
11
+ first_name: 'Ash'
12
+ }
13
+ }
14
+ end
15
+
16
+ it 'creates pokemon master' do
17
+ expect { post :create, params: parameters }
18
+ .to change(PokemonMaster, :count)
19
+ .by(1)
20
+ end
21
+
22
+ it 'updates pokemon master age' do
23
+ post :create, params: parameters
24
+
25
+ expect(PokemonMaster.last.age).to eq(10)
26
+ end
27
+ end
28
+
29
+ describe 'POST update' do
30
+ let(:master) do
31
+ create(:pokemon_master, age: 20, last_name: nil)
32
+ end
33
+
34
+ let(:parameters) do
35
+ {
36
+ id: master.id,
37
+ format: :json,
38
+ pokemon_master: { last_name: 'Joe' }
39
+ }
40
+ end
41
+
42
+ it 'updates pokemon master' do
43
+ expect { post :update, params: parameters }
44
+ .to change { master.reload.last_name }
45
+ .from(nil)
46
+ .to('Joe')
47
+ end
48
+
49
+ it 'updates pokemon master age' do
50
+ expect { post :update, params: parameters }
51
+ .to change { master.reload.age }
52
+ .from(20)
53
+ .to(10)
54
+ end
55
+ end
56
+ end