azeroth 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afd378d756eaf524cab794de457c654237f287bee671be9e6409702528695247
4
- data.tar.gz: 6dabd7110587bb3df0dcf7239a179ea1ceaee017692262f2366a2ac8b73fe0fc
3
+ metadata.gz: 4969648cf9b578079bc4d7baca012aa82768fd37edc9279ed2be606ef11b78b4
4
+ data.tar.gz: e426759bcab101a0ef02b0b9cc68d56d2cabc43fdea9a30027974dbaebf7a4e5
5
5
  SHA512:
6
- metadata.gz: b434a935b247bce958b72abefc6dc82063df57c9ad331e3b087c246e95cebea9dc2a619964081c54c65fe5c2775b62f5fac1b58ed4122a3776850922b24096b7
7
- data.tar.gz: d12697602efd61c4932b8fddb3e6b83b273dc51c441b4f9c5a1cf07a5325933e6b3c0629f594b06921d06a8cd4f37998f75ccedbf39c3a4950f7d9353fc3bfaf
6
+ metadata.gz: 8848cd9189e14cb2ac4ce22b401b2d5c4d5a39af868a38c1746a9e05136018bbf42b5e071f4feff680b823ab70fc9f3320c60fb0b4c982dfee23a2621781bc0a
7
+ data.tar.gz: ed36e6cf227e30e6f37dbe8ca109ccfecda6f2747349a1c73e97cf7319af929785aeaa0ccb8ab08265d0524d58a39fbe6a3101f5e95cb1b39998879866f783d2
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  Azeroth
2
2
  ========
3
+ [![Build Status](https://circleci.com/gh/darthjee/azeroth.svg?style=shield)](https://circleci.com/gh/darthjee/azeroth)
3
4
  [![Code Climate](https://codeclimate.com/github/darthjee/azeroth/badges/gpa.svg)](https://codeclimate.com/github/darthjee/azeroth)
4
5
  [![Test Coverage](https://codeclimate.com/github/darthjee/azeroth/badges/coverage.svg)](https://codeclimate.com/github/darthjee/azeroth/coverage)
5
6
  [![Issue Count](https://codeclimate.com/github/darthjee/azeroth/badges/issue_count.svg)](https://codeclimate.com/github/darthjee/azeroth)
6
7
  [![Gem Version](https://badge.fury.io/rb/azeroth.svg)](https://badge.fury.io/rb/azeroth)
8
+ [![Inline docs](http://inch-ci.org/github/darthjee/azeroth.svg)](http://inch-ci.org/github/darthjee/azeroth)
7
9
 
8
10
  ![azeroth](https://raw.githubusercontent.com/darthjee/azeroth/master/azeroth.jpg)
9
11
 
10
12
  Yard Documentation
11
13
  -------------------
12
- https://www.rubydoc.info/gems/azeroth/0.1.0
14
+ [https://www.rubydoc.info/gems/azeroth/0.2.0](https://www.rubydoc.info/gems/azeroth/0.2.0)
@@ -23,6 +23,7 @@ rules:
23
23
  enabled: true
24
24
  exclude:
25
25
  - Azeroth::Decorator#initialize
26
+ - Azeroth::Decorator::HashBuilder#initialize
26
27
  - Azeroth::Model#initialize
27
28
  - Azeroth::RequestHandler#initialize
28
29
  - Azeroth::ResourceBuilder#initialize
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sinclair'
4
+
3
5
  # @api public
4
6
  #
5
7
  # Lib for easily developing controllers
6
8
  #
9
+ # @example (see Resourceable)
10
+ #
7
11
  # @see Resourceable
8
-
9
- require 'sinclair'
10
-
11
12
  module Azeroth
12
13
  autoload :Decorator, 'azeroth/decorator'
13
14
  autoload :Model, 'azeroth/model'
@@ -40,20 +40,20 @@ module Azeroth
40
40
  # # 'pokemon' => 'Arcanine'
41
41
  # # }
42
42
  class Decorator
43
+ autoload :HashBuilder, 'azeroth/decorator/hash_builder'
44
+
43
45
  class << self
44
46
  # @api private
45
47
  #
46
48
  # All attributes exposed
47
49
  #
48
50
  # @return [Array<Symbol>]
49
- def attributes
50
- @attributes ||= []
51
+ def attributes_map
52
+ @attributes_map ||= {}
51
53
  end
52
54
 
53
55
  private
54
56
 
55
- # rubocop:disable Naming/UncommunicativeMethodParamName
56
-
57
57
  # @visibility public
58
58
  # @api public
59
59
  # @private
@@ -61,7 +61,12 @@ module Azeroth
61
61
  # Expose attributes on json decorated
62
62
  #
63
63
  # @param attribute [Symbol,String] attribute to be exposed
64
- # @param as [Symbol,String] name of the attribute on the json
64
+ # @param options [Hash] exposing options
65
+ # @option options as [Symbol,String] custom key
66
+ # to expose
67
+ # @option options if [Symbol,Proc] method/block to be called
68
+ # checking if an attribute should or should not
69
+ # be exposed
65
70
  #
66
71
  # @return [Array<Symbol>]
67
72
  #
@@ -82,14 +87,13 @@ module Azeroth
82
87
  # end
83
88
  # end
84
89
  # end
85
- def expose(attribute, as: attribute)
90
+ def expose(attribute, **options)
86
91
  builder = Sinclair.new(self)
87
- builder.add_method(as, "@object.#{attribute}")
92
+ builder.add_method(attribute, "@object.#{attribute}")
88
93
  builder.build
89
94
 
90
- attributes << as.to_sym
95
+ attributes_map[attribute] = options
91
96
  end
92
- # rubocop:enable Naming/UncommunicativeMethodParamName
93
97
  end
94
98
 
95
99
  # @api private
@@ -109,17 +113,13 @@ module Azeroth
109
113
  # When object is an iterator, decoration is applied to each
110
114
  # and an array is returned
111
115
  #
112
- # @param *args [Hash] options (to be implemented)
116
+ # @param args [Hash] options (to be implemented)
113
117
  #
114
118
  # @return [Hash]
115
119
  def as_json(*args)
116
120
  return array_as_json(*args) if enum?
117
121
 
118
- {}.tap do |hash|
119
- self.class.attributes.each do |method|
120
- hash[method.to_s] = public_send(method)
121
- end
122
- end
122
+ HashBuilder.new(self).as_json
123
123
  end
124
124
 
125
125
  private
@@ -153,5 +153,40 @@ module Azeroth
153
153
  self.class.new(item).as_json(*args)
154
154
  end
155
155
  end
156
+
157
+ # @api private
158
+ # @private
159
+ # Method called when method is missing
160
+ #
161
+ # This delegates method calls to the given object
162
+ #
163
+ # @param method_name [Symbol] name of the method
164
+ # called
165
+ # @param args [Array<Object>] arguments of the
166
+ # method called
167
+ #
168
+ # @return [Object]
169
+ def method_missing(method_name, *args)
170
+ if object.respond_to?(method_name)
171
+ object.public_send(method_name, *args)
172
+ else
173
+ super
174
+ end
175
+ end
176
+
177
+ # @api private
178
+ # @private
179
+ # Checks if it would respond to a method
180
+ #
181
+ # The decision is delegated to the object
182
+ #
183
+ # @param method_name [Symbol] name of the method checked
184
+ # @param include_private [TrueClass,FalseClass] flag
185
+ # indicating if private methods should be included
186
+ #
187
+ # @return [TrueClass,FalseClass]
188
+ def respond_to_missing?(method_name, include_private)
189
+ object.respond_to?(method_name, include_private)
190
+ end
156
191
  end
157
192
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinclair'
4
+
5
+ module Azeroth
6
+ class Decorator
7
+ # @api private
8
+ #
9
+ # Class responsible for building the hash on
10
+ # Decorator#as_json calls
11
+ class HashBuilder
12
+ # @param decorator [Decorator] decorator that
13
+ # will receive all method calls
14
+ def initialize(decorator)
15
+ @decorator = decorator
16
+ end
17
+
18
+ # Build hash as defined by decorator
19
+ #
20
+ # The built of the hash keys is defined
21
+ # by decorator attributes and method calls
22
+ # to the same decorator that will then delgate,
23
+ # when needed, to its object
24
+ #
25
+ # @return [Hash]
26
+ def as_json
27
+ attributes_map.each do |method, options|
28
+ add_attribute(method, options)
29
+ end
30
+ hash
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :decorator
36
+ # @method decorator
37
+ # @api private
38
+ # @private
39
+ #
40
+ # decorator with object to be decorated
41
+ #
42
+ # @return [Decorator]
43
+
44
+ # Adds an attribute to the exposed hash
45
+ #
46
+ # @param method [Symbol,String] method to be called
47
+ # from the decorator
48
+ # @param options [Hash] exposing options
49
+ # @option options as [Symbol,String] custom key
50
+ # to expose
51
+ # @option options if [Symbol,Proc] method/block to be called
52
+ # checking if an attribute should or should not
53
+ # be exposed
54
+ #
55
+ # @return [Object] result of method call from decorator
56
+ def add_attribute(method, options)
57
+ return unless add_attribute?(options)
58
+
59
+ key = options[:as] || method
60
+
61
+ hash[key.to_s] = decorator.public_send(method)
62
+ end
63
+
64
+ # Check if an attribute should be added to the hash
65
+ #
66
+ # @param options [Hash] exposing options
67
+ # @option options if [Symbol,Proc] method/block to be called
68
+ # checking if an attribute should or should not
69
+ # be exposed
70
+ #
71
+ # @return [Object] result of method call from decorator
72
+ def add_attribute?(options)
73
+ conditional = options[:if]
74
+ return true unless conditional.present?
75
+
76
+ block = proc(&conditional)
77
+
78
+ block.call(decorator)
79
+ end
80
+
81
+ # attributes to be built to hash
82
+ #
83
+ # The keys are the methods to be called and
84
+ # the values are the exposing options
85
+ #
86
+ # @return [Hash] hash of pairs method,options
87
+ def attributes_map
88
+ decorator.class.attributes_map
89
+ end
90
+
91
+ # Hash being built
92
+ #
93
+ # @return [Hash]
94
+ def hash
95
+ @hash ||= {}
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Azeroth
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -3,5 +3,10 @@
3
3
  class Document
4
4
  class Decorator < Azeroth::Decorator
5
5
  expose :name
6
+ expose :reference, if: proc(&:magic?)
7
+
8
+ def magic?
9
+ reference&.match(/^X-MAGIC/)
10
+ end
6
11
  end
7
12
  end
@@ -5,4 +5,12 @@ class DummyModel
5
5
 
6
6
  attr_accessor :id, :first_name, :last_name, :age,
7
7
  :favorite_pokemon
8
+
9
+ validates_presence_of :first_name
10
+
11
+ private
12
+
13
+ def private_name
14
+ "Secret #{last_name}"
15
+ end
8
16
  end
@@ -5,9 +5,15 @@ class DummyModel
5
5
  expose :name
6
6
  expose :age
7
7
  expose :favorite_pokemon, as: :pokemon
8
+ expose :errors, if: :invalid?
8
9
 
9
10
  def name
10
- [object.first_name, object.last_name].join(' ')
11
+ [object.first_name, object.last_name].compact.join(' ')
12
+ end
13
+
14
+ def errors
15
+ object.valid?
16
+ object.errors.messages
11
17
  end
12
18
  end
13
19
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Azeroth::Decorator::HashBuilder do
6
+ subject(:builder) { described_class.new(decorator) }
7
+
8
+ let(:decorator_class) { DummyModel::Decorator }
9
+ let(:decorator) { decorator_class.new(model) }
10
+ let(:model) { build(:dummy_model) }
11
+
12
+ describe '#as_json' do
13
+ let(:expected_json) do
14
+ {
15
+ name: "#{model.first_name} #{model.last_name}",
16
+ age: model.age,
17
+ pokemon: model.favorite_pokemon
18
+ }.stringify_keys
19
+ end
20
+
21
+ it 'returns meta data defined json' do
22
+ expect(decorator.as_json).to eq(expected_json)
23
+ end
24
+
25
+ context 'when conditional attibute is exposed' do
26
+ let(:model) { build(:dummy_model, first_name: nil) }
27
+
28
+ let(:expected_json) do
29
+ {
30
+ name: model.last_name,
31
+ age: model.age,
32
+ pokemon: model.favorite_pokemon,
33
+ errors: {
34
+ first_name: ["can't be blank"]
35
+ }
36
+ }.stringify_keys
37
+ end
38
+
39
+ it 'include the conditional attributes' do
40
+ expect(decorator.as_json).to eq(expected_json)
41
+ end
42
+ end
43
+
44
+ context 'when decorator has an expose block conditional' do
45
+ let(:decorator_class) { Document::Decorator }
46
+ let(:model) { create(:document) }
47
+
48
+ let(:expected_json) do
49
+ {
50
+ name: model.name
51
+ }.stringify_keys
52
+ end
53
+
54
+ it 'returns meta data defined json' do
55
+ expect(decorator.as_json).to eq(expected_json)
56
+ end
57
+
58
+ context 'when conditional returns true' do
59
+ let(:model) { create(:document, reference: 'X-MAGIC-01') }
60
+
61
+ let(:expected_json) do
62
+ {
63
+ name: model.name,
64
+ reference: 'X-MAGIC-01'
65
+ }.stringify_keys
66
+ end
67
+
68
+ it 'returns meta data defined json' do
69
+ expect(decorator.as_json).to eq(expected_json)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ 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
  subject(:decorator) { DummyModel::Decorator.new(object) }
5
7
 
@@ -19,6 +21,25 @@ describe Azeroth::Decorator do
19
21
  it 'returns meta data defined json' do
20
22
  expect(decorator.as_json).to eq(expected_json)
21
23
  end
24
+
25
+ context 'when conditional attibute is exposed' do
26
+ let(:model) { build(:dummy_model, first_name: nil) }
27
+
28
+ let(:expected_json) do
29
+ {
30
+ name: model.last_name,
31
+ age: model.age,
32
+ pokemon: model.favorite_pokemon,
33
+ errors: {
34
+ first_name: ["can't be blank"]
35
+ }
36
+ }.stringify_keys
37
+ end
38
+
39
+ it 'include the conditional attributes' do
40
+ expect(decorator.as_json).to eq(expected_json)
41
+ end
42
+ end
22
43
  end
23
44
 
24
45
  context 'when object is an array' do
@@ -92,4 +113,59 @@ describe Azeroth::Decorator do
92
113
  end
93
114
  end
94
115
  end
116
+
117
+ describe '#method_missing' do
118
+ subject(:decorator) { decorator_class.new(object) }
119
+
120
+ let(:decorator_class) { Class.new(described_class) }
121
+ let(:model) { build(:dummy_model) }
122
+
123
+ it 'delegates methods to object' do
124
+ expect(decorator.first_name).not_to be_nil
125
+ end
126
+
127
+ context 'when object does not respond to method' do
128
+ it do
129
+ expect { decorator.bad_method }
130
+ .to raise_error(NoMethodError)
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '#respond_to_missing?' do
136
+ subject(:decorator) { decorator_class.new(object) }
137
+
138
+ let(:decorator_class) { Class.new(described_class) }
139
+ let(:model) { build(:dummy_model) }
140
+
141
+ context 'when object responds to it' do
142
+ it do
143
+ expect(decorator)
144
+ .to respond_to(:first_name)
145
+ end
146
+ end
147
+
148
+ context 'when method is private' do
149
+ it do
150
+ expect(decorator)
151
+ .not_to respond_to(:private_name)
152
+ end
153
+ end
154
+
155
+ context 'when method is private and passing include_private' do
156
+ # rubocop:disable RSpec/PredicateMatcher
157
+ it do
158
+ expect(decorator.respond_to?(:private_name, true))
159
+ .to be_truthy
160
+ end
161
+ # rubocop:enable RSpec/PredicateMatcher
162
+ end
163
+
164
+ context 'when object does not respond to it' do
165
+ it do
166
+ expect(decorator)
167
+ .not_to respond_to(:no_name)
168
+ end
169
+ end
170
+ end
95
171
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: azeroth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Darthjee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-16 00:00:00.000000000 Z
11
+ date: 2019-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -401,6 +401,7 @@ files:
401
401
  - docker-compose.yml
402
402
  - lib/azeroth.rb
403
403
  - lib/azeroth/decorator.rb
404
+ - lib/azeroth/decorator/hash_builder.rb
404
405
  - lib/azeroth/model.rb
405
406
  - lib/azeroth/options.rb
406
407
  - lib/azeroth/request_handler.rb
@@ -491,6 +492,7 @@ files:
491
492
  - spec/dummy/tmp/.keep
492
493
  - spec/dummy/tmp/storage/.keep
493
494
  - spec/integration/yard/azeroth/decorator_spec.rb
495
+ - spec/lib/azeroth/decorator/hash_builder_spec.rb
494
496
  - spec/lib/azeroth/decorator_spec.rb
495
497
  - spec/lib/azeroth/model_spec.rb
496
498
  - spec/lib/azeroth/request_handler/create_spec.rb
@@ -613,6 +615,7 @@ test_files:
613
615
  - spec/dummy/tmp/.keep
614
616
  - spec/dummy/tmp/storage/.keep
615
617
  - spec/integration/yard/azeroth/decorator_spec.rb
618
+ - spec/lib/azeroth/decorator/hash_builder_spec.rb
616
619
  - spec/lib/azeroth/decorator_spec.rb
617
620
  - spec/lib/azeroth/model_spec.rb
618
621
  - spec/lib/azeroth/request_handler/create_spec.rb