abstractable 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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +10 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +235 -0
- data/Rakefile +7 -0
- data/abstractable.gemspec +23 -0
- data/lib/abstractable/not_implemented_info_finder.rb +49 -0
- data/lib/abstractable/pedigree_stream.rb +63 -0
- data/lib/abstractable/version.rb +3 -0
- data/lib/abstractable.rb +128 -0
- data/spec/abstractable_spec.rb +310 -0
- data/spec/spec_helper.rb +13 -0
- data/tasks/flay.rake +11 -0
- data/tasks/flog.rake +18 -0
- data/tasks/rspec.rake +3 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8bd935180782c9239522ca5b1620e0df57ed3711
|
4
|
+
data.tar.gz: 783fb41773e04a0dab097fe83b1f3917c46d2dba
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4961394ef67b57d5844633e51b9c152dbe47cb6d1dc4ddad9f150ec853676be4a0f353b4b7e7ac6009119422f121c56ff3324861d262a4cd936bc8e318b30117
|
7
|
+
data.tar.gz: c71bf70ef9481d8e9786d3471c806c98c4ed856f21df818d89f088b2b4ab5d72ccd99cdd2f7174f78f48184b59f5f4291c51bc32d3ab3a400111dd8a49e45cb6
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
HashSyntax:
|
2
|
+
EnforcedStyle: ruby19
|
3
|
+
SupportedStyles:
|
4
|
+
- ruby19
|
5
|
+
- hash_rockets
|
6
|
+
|
7
|
+
Documentation:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
LineLength:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
ClassLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
RegexpLiteral:
|
17
|
+
MaxSlashes: 0
|
18
|
+
|
19
|
+
GuardClause:
|
20
|
+
MinBodyLength: 3
|
21
|
+
|
22
|
+
TrivialAccessors:
|
23
|
+
AllowPredicates: true
|
24
|
+
|
25
|
+
RedundantReturn:
|
26
|
+
AllowMultipleReturnValues: true
|
27
|
+
|
28
|
+
RedundantBegin:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
SpaceInsideHashLiteralBraces:
|
32
|
+
EnforcedStyle: no_space
|
33
|
+
|
34
|
+
Style/Lambda:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Style/Proc:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
StringLiterals:
|
41
|
+
EnforcedStyle: double_quotes
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem "rake", ">= 0.9"
|
4
|
+
gem "rdoc", ">= 3.9"
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem "guard-rubocop"
|
8
|
+
gem "terminal-notifier-guard"
|
9
|
+
gem "flay"
|
10
|
+
gem "flog"
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem "coveralls", require: false
|
15
|
+
gem "rubocop", ">= 0.19"
|
16
|
+
gem "rspec", ">= 3"
|
17
|
+
gem "simplecov", ">= 0.9"
|
18
|
+
end
|
19
|
+
|
20
|
+
gemspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 pujoheadsoft
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
# Abstractable
|
2
|
+
|
3
|
+
[][travis]
|
4
|
+
[][coveralls]
|
5
|
+
[][codeclimate]
|
6
|
+
|
7
|
+
[travis]: http://travis-ci.org/pujoheadsoft/abstractable
|
8
|
+
[coveralls]: https://coveralls.io/r/pujoheadsoft/abstractable
|
9
|
+
[codeclimate]: https://codeclimate.com/github/pujoheadsoft/abstractable
|
10
|
+
|
11
|
+
## Overview
|
12
|
+
Abstractable is Library for define abstract method.
|
13
|
+
Can know unimplemented abstract methods by fail fast as possible.
|
14
|
+
This mechanism is very useful for prevent the implementation leakage.
|
15
|
+
```ruby
|
16
|
+
require "abstractable"
|
17
|
+
class AbstractDriver
|
18
|
+
extend Abstractable
|
19
|
+
abstract :open, :close
|
20
|
+
end
|
21
|
+
|
22
|
+
class AbstractIODriver < AbstractDriver
|
23
|
+
abstract :read, :write
|
24
|
+
end
|
25
|
+
|
26
|
+
class NotImplIODriver < AbstractIODriver; end
|
27
|
+
|
28
|
+
NotImplIODriver.new
|
29
|
+
# => following abstract methods are not implemented. (NotImplementedError)
|
30
|
+
# [:open, :close] defined in AbstractDriver
|
31
|
+
# [:read, :write] defined in AbstractIODriver
|
32
|
+
```
|
33
|
+
See sample above. error occurred at call *new* method.
|
34
|
+
Info of unimplemented abstract method have been included in the error message.
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
gem install abstractable
|
38
|
+
|
39
|
+
## How to use
|
40
|
+
First, call *require "abstractable"*.
|
41
|
+
Then, *extend Abstractable*.
|
42
|
+
```ruby
|
43
|
+
require "abstractable"
|
44
|
+
class AbstractClass
|
45
|
+
extend Abstractable
|
46
|
+
end
|
47
|
+
```
|
48
|
+
**Note:** This document omit the *require "abstractable"* from now on.
|
49
|
+
|
50
|
+
### Define abstract method
|
51
|
+
|
52
|
+
1. **Specify *Symbol* (can specify multiple)**
|
53
|
+
```ruby
|
54
|
+
class AbstractList
|
55
|
+
extend Abstractable
|
56
|
+
|
57
|
+
abstract :add, :insert, :empty?
|
58
|
+
end
|
59
|
+
```
|
60
|
+
If want to express the arguments then can write be as follows.
|
61
|
+
```ruby
|
62
|
+
class AbstractList
|
63
|
+
extend Abstractable
|
64
|
+
|
65
|
+
def add(value); end
|
66
|
+
def insert(index, value); end
|
67
|
+
def empty?; end
|
68
|
+
|
69
|
+
abstract :add, :insert, :empty?
|
70
|
+
end
|
71
|
+
```
|
72
|
+
2. **In block**
|
73
|
+
Defined methods in block is all treated as an abstract method.
|
74
|
+
```ruby
|
75
|
+
class AbstractList
|
76
|
+
extend Abstractable
|
77
|
+
|
78
|
+
abstract do
|
79
|
+
def add(value); end
|
80
|
+
def insert(index, value); end
|
81
|
+
def empty?; end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
Can also be written as follows.
|
86
|
+
```ruby
|
87
|
+
class AbstractList
|
88
|
+
extend Abstractable
|
89
|
+
|
90
|
+
abstract { def add(value); end }
|
91
|
+
abstract { def insert(index, value); end }
|
92
|
+
abstract { def empty?; end }
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### Get defined abstract methods
|
97
|
+
*abstract_methods* returns an array containing the names of abstract methods in the receiver.
|
98
|
+
if set *true* to args include ancestors abstract methods (default *true*).
|
99
|
+
```ruby
|
100
|
+
class AbstractDriver
|
101
|
+
extend Abstractable
|
102
|
+
abstract :open, :close
|
103
|
+
end
|
104
|
+
|
105
|
+
class AbstractIODriver < AbstractDriver
|
106
|
+
abstract :read, :write
|
107
|
+
end
|
108
|
+
|
109
|
+
AbstractIODriver.abstract_methods # => [:read, :write, :open, :close]
|
110
|
+
```
|
111
|
+
If specify *false* for the argument, only abstract methods defined in the receiver is returned.
|
112
|
+
```ruby
|
113
|
+
AbstractIODriver.abstract_methods(false) # => [:read, :write]
|
114
|
+
```
|
115
|
+
### Undefine of abstract method
|
116
|
+
Can undefine and if you call *Module#undef_method*.
|
117
|
+
However, the receiver must be a class that defines an abstract method.
|
118
|
+
(Can't be undefine of the abstract methods of the parent class from a subclass.)
|
119
|
+
```ruby
|
120
|
+
class Parent
|
121
|
+
extend Abstractable
|
122
|
+
abstract :greet
|
123
|
+
end
|
124
|
+
|
125
|
+
class Child < Parent
|
126
|
+
undef_method :greet
|
127
|
+
end
|
128
|
+
|
129
|
+
begin
|
130
|
+
Child.new # => NotImplementedError
|
131
|
+
rescue NotImplementedError
|
132
|
+
end
|
133
|
+
|
134
|
+
class Parent
|
135
|
+
undef_method :greet
|
136
|
+
end
|
137
|
+
|
138
|
+
Child.new # => OK
|
139
|
+
```
|
140
|
+
### Validation of unimplemented abstract method do always called?
|
141
|
+
Once you have confirmed that the unimplemented methods do not exist, validation does not take place.
|
142
|
+
if defined abstract method later, then perform validation once more.
|
143
|
+
*required_validate?* returns true if required unimplemented abstract methods validation.
|
144
|
+
```ruby
|
145
|
+
class AbstractParent
|
146
|
+
extend Abstractable
|
147
|
+
abstract :greet
|
148
|
+
end
|
149
|
+
|
150
|
+
class Child < AbstractParent
|
151
|
+
def greet; end
|
152
|
+
end
|
153
|
+
|
154
|
+
Child.required_validate? # => true
|
155
|
+
Child.new
|
156
|
+
Child.required_validate? # => false
|
157
|
+
|
158
|
+
# define abstract method
|
159
|
+
AbstractParent.abstract :execute
|
160
|
+
|
161
|
+
Child.required_validate? # => true
|
162
|
+
```
|
163
|
+
#### If define environment variable *ABSTRACTABLE_IGNORE_VALIDATE*
|
164
|
+
This case is *required_validate?* returns true always.
|
165
|
+
*ABSTRACTABLE_IGNORE_VALIDATE* value allow any value.
|
166
|
+
```
|
167
|
+
# Linux
|
168
|
+
export ABSTRACTABLE_IGNORE_VALIDATE=true
|
169
|
+
# windows
|
170
|
+
set ABSTRACTABLE_IGNORE_VALIDATE=true
|
171
|
+
```
|
172
|
+
### Abstract method of singleton class
|
173
|
+
Abstract method of singleton class can't validation at new.
|
174
|
+
Therefore throw error at abstract method call.
|
175
|
+
But can find unimplemented abstract methods.
|
176
|
+
```ruby
|
177
|
+
class AbstractParent
|
178
|
+
class << self
|
179
|
+
extend Abstractable
|
180
|
+
abstract :greet
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class Child < AbstractParent
|
185
|
+
end
|
186
|
+
|
187
|
+
Child.greet # => greet is abstract method defined in
|
188
|
+
# #<Class:AbstractParent>, and must implement.
|
189
|
+
# (NotImplementedError)
|
190
|
+
```
|
191
|
+
### Do explicitly validation
|
192
|
+
if call *validate_not_implemented_abstract_methods* then can do explicitly validation.
|
193
|
+
```ruby
|
194
|
+
class AbstractParent
|
195
|
+
extend Abstractable
|
196
|
+
abstract :greet
|
197
|
+
end
|
198
|
+
|
199
|
+
class Child < AbstractParent; end
|
200
|
+
|
201
|
+
# explicitly validation
|
202
|
+
Child.validate_not_implemented_abstract_methods
|
203
|
+
# => following abstract methods are not implemented. (NotImplementedError)
|
204
|
+
# [:greet] defined in AbstractParent
|
205
|
+
```
|
206
|
+
### Find unimplemented abstract methods
|
207
|
+
*abstractable#find_not_implemented_info* returns a following format Hash
|
208
|
+
```ruby
|
209
|
+
{abstract_class => array of unimplemented abstract methods, ...}
|
210
|
+
```
|
211
|
+
```ruby
|
212
|
+
class AbstractParent
|
213
|
+
extend Abstractable
|
214
|
+
abstract :greet
|
215
|
+
end
|
216
|
+
|
217
|
+
class Child < AbstractParent; end
|
218
|
+
|
219
|
+
Abstractable.find_not_implemented_info(Child) # => {AbstractParent=>[:greet]}
|
220
|
+
```
|
221
|
+
If call *find_not_implemented_info_from_singleton*,
|
222
|
+
Then can find unimplemented abstract methods of singleton class.
|
223
|
+
```ruby
|
224
|
+
class AbstractParent
|
225
|
+
class << self
|
226
|
+
extend Abstractable
|
227
|
+
abstract :greet
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class Child < AbstractParent; end
|
232
|
+
|
233
|
+
Abstractable.find_not_implemented_info_from_singleton(Child)
|
234
|
+
# => {#<Class:AbstractParent>=>[:greet]}
|
235
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "abstractable/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "abstractable"
|
8
|
+
spec.version = Abstractable::VERSION
|
9
|
+
spec.authors = ["Kenji Suzuki"]
|
10
|
+
spec.email = ["pujoheadsoft@gmail.com"]
|
11
|
+
spec.summary = "Library for define abstract method."
|
12
|
+
spec.description = "Library for define abstract method. Can know unimplemented abstract methods by fail fast as possible. This mechanism is very useful for prevent the implementation leakage."
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "abstractable"
|
2
|
+
require "abstractable/pedigree_stream"
|
3
|
+
|
4
|
+
module Abstractable
|
5
|
+
# this class can find to information of unimplemented abstract method
|
6
|
+
class NotImplementedInfoFinder
|
7
|
+
# find(klass) -> hash
|
8
|
+
#
|
9
|
+
# Returns an hash as information of unimplemented abstract method.
|
10
|
+
# hash format is {class => array of unimplemented method in class, ... }
|
11
|
+
def find(klass)
|
12
|
+
find_from_pedigree_stream(PedigreeStream.new(klass))
|
13
|
+
end
|
14
|
+
|
15
|
+
# find_from_singleton(klass) -> hash
|
16
|
+
# singleton_class version find.
|
17
|
+
def find_from_singleton(klass)
|
18
|
+
find_from_pedigree_stream(SingletonPedigreeStream.new(klass))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def find_from_pedigree_stream(pedigree_stream)
|
24
|
+
pedigree_stream.each_with_descendants_and_object(create_deny_empty_array_hash) do |klass, descendants, hash|
|
25
|
+
hash[klass] = find_from_ancestor_and_descendants(klass, descendants) if need_find?(klass, descendants)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_deny_empty_array_hash
|
30
|
+
hash = {}
|
31
|
+
def hash.[]=(key, value)
|
32
|
+
super(key, value) unless value.empty?
|
33
|
+
end
|
34
|
+
hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def need_find?(ancestor, descendants)
|
38
|
+
ancestor.is_a?(Abstractable) && 0 < descendants.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_from_ancestor_and_descendants(ancestor, descendants)
|
42
|
+
ancestor.abstract_methods(false).reject { |method| descendants.any?(&individual_method_defined?(method)) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def individual_method_defined?(method)
|
46
|
+
lambda { |klass| klass.instance_methods(false).include? method }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Abstractable
|
2
|
+
# this class represent stream of pedigree to myself from an ancestor.
|
3
|
+
class PedigreeStream
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(klass)
|
7
|
+
fail ArgumentError, "wrong type argument #{klass} (should be Class) " unless klass.is_a? Class
|
8
|
+
self.pedigree_stream = pedigree_stream_of(klass)
|
9
|
+
end
|
10
|
+
|
11
|
+
# each for Enumerable.
|
12
|
+
def each
|
13
|
+
pedigree_stream.each { |klass| yield klass }
|
14
|
+
end
|
15
|
+
|
16
|
+
# each with descendants
|
17
|
+
#
|
18
|
+
# Example of use:
|
19
|
+
# each_with_descendants do |klass, descendants_of_klass|
|
20
|
+
# your code...
|
21
|
+
# end
|
22
|
+
def each_with_descendants
|
23
|
+
each do |klass|
|
24
|
+
yield(klass, descendants_of(klass))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# each_with_object with descendants.
|
29
|
+
#
|
30
|
+
# Example of use:
|
31
|
+
# each_with_descendants_and_object([]) do |klass, descendants_of_klass, array|
|
32
|
+
# your code...
|
33
|
+
# end
|
34
|
+
def each_with_descendants_and_object(object)
|
35
|
+
each_with_descendants do |klass, descendants_of_klass|
|
36
|
+
yield(klass, descendants_of_klass, object)
|
37
|
+
end
|
38
|
+
object
|
39
|
+
end
|
40
|
+
|
41
|
+
# descendants_of(klass) -> array
|
42
|
+
# Returns an array of descendants name of class.
|
43
|
+
def descendants_of(klass)
|
44
|
+
i = pedigree_stream.index(klass)
|
45
|
+
i ? pedigree_stream.drop(i + 1) : []
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
attr_accessor :pedigree_stream
|
51
|
+
|
52
|
+
def pedigree_stream_of(klass)
|
53
|
+
klass.ancestors.reverse
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# PedigreeStream of Singleton Class.
|
58
|
+
class SingletonPedigreeStream < PedigreeStream
|
59
|
+
def pedigree_stream_of(klass)
|
60
|
+
klass.ancestors.reverse.map(&:singleton_class)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/abstractable.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require "abstractable/version"
|
2
|
+
require "abstractable/not_implemented_info_finder"
|
3
|
+
|
4
|
+
# abstract method support module.
|
5
|
+
module Abstractable
|
6
|
+
class WrongOperationError < StandardError; end
|
7
|
+
|
8
|
+
def included(base)
|
9
|
+
base.extend(Abstractable)
|
10
|
+
end
|
11
|
+
|
12
|
+
# new after unimplemented abstract methods validation.
|
13
|
+
def new(*args, &block)
|
14
|
+
validate_on_create(:new)
|
15
|
+
super(*args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# allocate after unimplemented abstract methods validation.
|
19
|
+
def allocate(*args, &block)
|
20
|
+
validate_on_create(:allocate)
|
21
|
+
super(*args, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Define abstract methods.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
# abstract :execute # one symbol
|
28
|
+
# abstract :method1, :method2 # multiple symbol
|
29
|
+
# # in block
|
30
|
+
# abstract do
|
31
|
+
# def method3; end
|
32
|
+
# def method4; end
|
33
|
+
# end
|
34
|
+
def abstract(*names, &block)
|
35
|
+
add_abstract_methods(*names.compact)
|
36
|
+
add_abstract_methods_by_block(&block) if block_given?
|
37
|
+
end
|
38
|
+
|
39
|
+
# abstract_methods(all=true) -> array
|
40
|
+
#
|
41
|
+
# Returns an array containing the names of abstract methods in the receiver.
|
42
|
+
# if set true to args include ancestors abstract methods.
|
43
|
+
# (default true)
|
44
|
+
#
|
45
|
+
def abstract_methods(all = true)
|
46
|
+
return individual_abstract_methods unless all
|
47
|
+
collect = lambda { |klass, array| array.push(*klass.abstract_methods(false)) if klass.is_a? Abstractable }
|
48
|
+
individual_abstract_methods + (ancestors - [self]).each_with_object([], &collect)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Unimplemented abstract methods validation.
|
52
|
+
#
|
53
|
+
# if found unimplemented methods then throw NotImplementedError.
|
54
|
+
def validate_not_implemented_abstract_methods
|
55
|
+
not_impl_info = Abstractable.find_not_implemented_info(self)
|
56
|
+
fail NotImplementedError, build_error_message(not_impl_info) unless not_impl_info.empty?
|
57
|
+
@implemented_abstract_methods = abstract_methods
|
58
|
+
end
|
59
|
+
|
60
|
+
# required_validate? -> true or false
|
61
|
+
#
|
62
|
+
# Returns <code>true</code> if required unimplemented abstract methods validation.
|
63
|
+
#
|
64
|
+
# if validated or if defined environment variable <code>ABSTRACTABLE_IGNORE_VALIDATE</code>
|
65
|
+
# then always return true.
|
66
|
+
def required_validate?
|
67
|
+
!ENV["ABSTRACTABLE_IGNORE_VALIDATE"] && @implemented_abstract_methods != abstract_methods
|
68
|
+
end
|
69
|
+
|
70
|
+
# called when the method is undef.
|
71
|
+
def method_undefined(method)
|
72
|
+
individual_abstract_methods.delete(method)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Shortcut to NotImplementedInfoFinder.new.find(klass)
|
76
|
+
def self.find_not_implemented_info(klass)
|
77
|
+
NotImplementedInfoFinder.new.find(klass)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Shortcut to NotImplementedInfoFinder.new.find_from_singleton(klass)
|
81
|
+
def self.find_not_implemented_info_from_singleton(klass)
|
82
|
+
NotImplementedInfoFinder.new.find_from_singleton(klass)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def individual_abstract_methods
|
88
|
+
@individual_abstract_methods ||= []
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_on_create(create_method_name)
|
92
|
+
validate_not_implemented_abstract_methods if required_validate?
|
93
|
+
if is_a?(Abstractable) && 0 < abstract_methods(false).size
|
94
|
+
fail WrongOperationError, "#{self} has abstract methods. and therefore can't call #{create_method_name}."
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_error_message(not_implemented_infos)
|
99
|
+
messages = ["following abstract methods are not implemented."]
|
100
|
+
formatter = Proc.new { |klass, methods| "#{methods} defined in #{klass}" }
|
101
|
+
messages.push(*not_implemented_infos.map(&formatter)).join("\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
def define_abstract_skeleton(method)
|
105
|
+
this = self
|
106
|
+
class_eval do
|
107
|
+
define_method method do |*_|
|
108
|
+
fail NotImplementedError, "#{method} is abstract method defined in #{this}, and must implement."
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_abstract_methods_by_block(&block)
|
114
|
+
old_instance_methods = instance_methods(false)
|
115
|
+
block.call
|
116
|
+
add_abstract_methods(*(instance_methods(false) - old_instance_methods))
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_abstract_methods(*methods)
|
120
|
+
methods.each { |method| add_abstract_method(method) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_abstract_method(method)
|
124
|
+
fail ArgumentError, "wrong type argument #{method} (should be Symbol) " unless method.is_a? Symbol
|
125
|
+
individual_abstract_methods << method
|
126
|
+
define_abstract_skeleton(method)
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Abstractable, "Simple" do
|
4
|
+
|
5
|
+
before(:context) do
|
6
|
+
|
7
|
+
class AbstractList
|
8
|
+
extend Abstractable
|
9
|
+
abstract :size, :empty?, :add
|
10
|
+
end
|
11
|
+
|
12
|
+
class NotImplList < AbstractList; end
|
13
|
+
|
14
|
+
class OneImplList < AbstractList
|
15
|
+
def size; end
|
16
|
+
end
|
17
|
+
|
18
|
+
class AllImplList < OneImplList
|
19
|
+
def empty?; end
|
20
|
+
|
21
|
+
def add; end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
it "all implemented abstract methods" do
|
27
|
+
expect { AllImplList.new }.not_to raise_error
|
28
|
+
expect { AllImplList.allocate }.not_to raise_error
|
29
|
+
end
|
30
|
+
|
31
|
+
it "not implemented abstract methods" do
|
32
|
+
expect do
|
33
|
+
NotImplList.new
|
34
|
+
end.to raise_error(NotImplementedError, <<-EOF.gsub(/^\s+|\n$/, ""))
|
35
|
+
following abstract methods are not implemented.
|
36
|
+
[:size, :empty?, :add] defined in AbstractList
|
37
|
+
EOF
|
38
|
+
end
|
39
|
+
|
40
|
+
it "1 implemented abstract methods" do
|
41
|
+
expect do
|
42
|
+
OneImplList.new
|
43
|
+
end.to raise_error(NotImplementedError, <<-EOF.gsub(/^\s+|\n$/, ""))
|
44
|
+
following abstract methods are not implemented.
|
45
|
+
[:empty?, :add] defined in AbstractList
|
46
|
+
EOF
|
47
|
+
end
|
48
|
+
|
49
|
+
it "new abstract" do
|
50
|
+
error_message = "#{AbstractList} has abstract methods. and therefore can't call new."
|
51
|
+
expect do
|
52
|
+
AbstractList.new
|
53
|
+
end.to raise_error(Abstractable::WrongOperationError, error_message)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "allocate abstract" do
|
57
|
+
error_message = "#{AbstractList} has abstract methods. and therefore can't call allocate."
|
58
|
+
expect do
|
59
|
+
AbstractList.allocate
|
60
|
+
end.to raise_error(Abstractable::WrongOperationError, error_message)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
describe Abstractable, "Complex class hierarchy" do
|
66
|
+
|
67
|
+
before(:context) do
|
68
|
+
|
69
|
+
module AbstractAddressHolder
|
70
|
+
extend Abstractable
|
71
|
+
abstract :city
|
72
|
+
abstract :state, :zip
|
73
|
+
end
|
74
|
+
|
75
|
+
module AbstractNameHolder
|
76
|
+
extend Abstractable
|
77
|
+
abstract do
|
78
|
+
def last_name; end
|
79
|
+
|
80
|
+
def first_name; end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class OneImplAddressHolder
|
85
|
+
include AbstractAddressHolder
|
86
|
+
def state; end
|
87
|
+
end
|
88
|
+
|
89
|
+
class TwoImplAdressAndNameHolder < OneImplAddressHolder
|
90
|
+
include AbstractNameHolder
|
91
|
+
def first_name; end
|
92
|
+
end
|
93
|
+
|
94
|
+
class AllImplAdressAndNameHolder
|
95
|
+
include AbstractAddressHolder
|
96
|
+
include AbstractNameHolder
|
97
|
+
|
98
|
+
def last_name; end
|
99
|
+
|
100
|
+
def first_name; end
|
101
|
+
|
102
|
+
def city; end
|
103
|
+
|
104
|
+
def state; end
|
105
|
+
|
106
|
+
def zip; end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "all implemented abstract methods" do
|
111
|
+
expect { AllImplAdressAndNameHolder.new }.not_to raise_error
|
112
|
+
end
|
113
|
+
|
114
|
+
it "1 implemented abstract methods" do
|
115
|
+
expect do
|
116
|
+
OneImplAddressHolder.new
|
117
|
+
end.to raise_error(NotImplementedError, <<-EOF.gsub(/^\s+|\n$/, ""))
|
118
|
+
following abstract methods are not implemented.
|
119
|
+
[:city, :zip] defined in AbstractAddressHolder
|
120
|
+
EOF
|
121
|
+
end
|
122
|
+
|
123
|
+
it "2 implemented abstract methods" do
|
124
|
+
expect do
|
125
|
+
TwoImplAdressAndNameHolder.new
|
126
|
+
end.to raise_error(NotImplementedError, <<-EOF.gsub(/^\s+|\n$/, ""))
|
127
|
+
following abstract methods are not implemented.
|
128
|
+
[:city, :zip] defined in AbstractAddressHolder
|
129
|
+
[:last_name] defined in AbstractNameHolder
|
130
|
+
EOF
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
describe Abstractable, "Independency" do
|
136
|
+
|
137
|
+
before(:context) do
|
138
|
+
class AbstractFormatter
|
139
|
+
extend Abstractable
|
140
|
+
abstract :format
|
141
|
+
end
|
142
|
+
|
143
|
+
class AFormatter < AbstractFormatter
|
144
|
+
def format; end
|
145
|
+
end
|
146
|
+
|
147
|
+
class BFormatter < AbstractFormatter
|
148
|
+
def format; end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
it "AFormatter & BFormatter validation is independent" do
|
154
|
+
expect(AFormatter.required_validate?).to be_truthy
|
155
|
+
expect(BFormatter.required_validate?).to be_truthy
|
156
|
+
|
157
|
+
AFormatter.new
|
158
|
+
expect(AFormatter.required_validate?).to be_falsey
|
159
|
+
expect(BFormatter.required_validate?).to be_truthy
|
160
|
+
|
161
|
+
BFormatter.new
|
162
|
+
expect(AFormatter.required_validate?).to be_falsey
|
163
|
+
expect(BFormatter.required_validate?).to be_falsey
|
164
|
+
end
|
165
|
+
|
166
|
+
it "Add later abstract method" do
|
167
|
+
AbstractFormatter.abstract :close
|
168
|
+
|
169
|
+
expect { AFormatter.new }.to raise_error(NotImplementedError)
|
170
|
+
expect { BFormatter.new }.to raise_error(NotImplementedError)
|
171
|
+
|
172
|
+
class AFormatter
|
173
|
+
def close; end
|
174
|
+
end
|
175
|
+
expect { AFormatter.new }.not_to raise_error
|
176
|
+
expect { BFormatter.new }.to raise_error(NotImplementedError)
|
177
|
+
|
178
|
+
class BFormatter
|
179
|
+
def close; end
|
180
|
+
end
|
181
|
+
expect { AFormatter.new }.not_to raise_error
|
182
|
+
expect { BFormatter.new }.not_to raise_error
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
describe Abstractable, "delete abstract" do
|
188
|
+
|
189
|
+
it "delete by undef_method & undef_abstract" do
|
190
|
+
class AbstractWriter
|
191
|
+
extend Abstractable
|
192
|
+
abstract :write, :open, :close
|
193
|
+
end
|
194
|
+
|
195
|
+
class AbstractDocumentWriter < AbstractWriter
|
196
|
+
abstract :write_document
|
197
|
+
end
|
198
|
+
|
199
|
+
class AbstractXMLDocumentWriter < AbstractDocumentWriter
|
200
|
+
abstract :to_xml
|
201
|
+
end
|
202
|
+
|
203
|
+
class XMLDocumentWriter < AbstractXMLDocumentWriter; end
|
204
|
+
|
205
|
+
expect { XMLDocumentWriter.new }.to raise_error(NotImplementedError)
|
206
|
+
|
207
|
+
class AbstractXMLDocumentWriter
|
208
|
+
undef_method :to_xml
|
209
|
+
end
|
210
|
+
|
211
|
+
expect(XMLDocumentWriter.abstract_methods).to eq([:write_document, :write, :open, :close])
|
212
|
+
expect(AbstractXMLDocumentWriter.abstract_methods).to eq([:write_document, :write, :open, :close])
|
213
|
+
expect(AbstractWriter.abstract_methods).to eq([:write, :open, :close])
|
214
|
+
|
215
|
+
class AbstractDocumentWriter
|
216
|
+
undef_method :write_document, :open, :close
|
217
|
+
end
|
218
|
+
|
219
|
+
expect(XMLDocumentWriter.abstract_methods).to eq([:write, :open, :close])
|
220
|
+
expect(AbstractXMLDocumentWriter.abstract_methods).to eq([:write, :open, :close])
|
221
|
+
expect(AbstractWriter.abstract_methods).to eq([:write, :open, :close])
|
222
|
+
|
223
|
+
class AbstractWriter
|
224
|
+
undef_method :write, :open, :close
|
225
|
+
end
|
226
|
+
|
227
|
+
expect(XMLDocumentWriter.abstract_methods).to eq([])
|
228
|
+
expect(AbstractXMLDocumentWriter.abstract_methods).to eq([])
|
229
|
+
expect(AbstractWriter.abstract_methods).to eq([])
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
describe Abstractable, "Class method" do
|
235
|
+
|
236
|
+
before(:context) do
|
237
|
+
class AbstractApplication
|
238
|
+
class << self
|
239
|
+
extend Abstractable
|
240
|
+
abstract :name
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class NotImplApplication < AbstractApplication; end
|
245
|
+
|
246
|
+
class Application < AbstractApplication
|
247
|
+
def self.name; end
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
it "not implemented class method" do
|
253
|
+
message = "name is abstract method defined in #{AbstractApplication.singleton_class}, and must implement."
|
254
|
+
expect do
|
255
|
+
NotImplApplication.name
|
256
|
+
end.to raise_error(NotImplementedError, message)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "implemented class method" do
|
260
|
+
expect { Application.new }.not_to raise_error
|
261
|
+
end
|
262
|
+
|
263
|
+
it "find not implemented info from singleton class" do
|
264
|
+
h = {AbstractApplication.singleton_class => [:name]}
|
265
|
+
expect(Abstractable.find_not_implemented_info_from_singleton(NotImplApplication)).to eq(h)
|
266
|
+
expect(Abstractable.find_not_implemented_info_from_singleton(Application)).to eq({})
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
describe Abstractable, "Use environment variable: ABSTRACTABLE_IGNORE_VALIDATE" do
|
272
|
+
|
273
|
+
before(:context) do
|
274
|
+
class AbstractPerson
|
275
|
+
extend Abstractable
|
276
|
+
abstract :say
|
277
|
+
end
|
278
|
+
|
279
|
+
class NotImplPerson < AbstractPerson; end
|
280
|
+
end
|
281
|
+
|
282
|
+
it "if defined ABSTRACTABLE_IGNORE_VALIDATE then ignore validation." do
|
283
|
+
expect(ENV).to receive(:[]).with("ABSTRACTABLE_IGNORE_VALIDATE").and_return(true)
|
284
|
+
expect { NotImplPerson.new }.not_to raise_error
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
describe Abstractable::NotImplementedInfoFinder do
|
290
|
+
|
291
|
+
it "find" do
|
292
|
+
class AbstractCollection
|
293
|
+
extend Abstractable
|
294
|
+
abstract :add, :remove
|
295
|
+
end
|
296
|
+
|
297
|
+
class AbstractQueue < AbstractCollection
|
298
|
+
abstract :clear
|
299
|
+
end
|
300
|
+
|
301
|
+
class PriorityQueue < AbstractQueue; end
|
302
|
+
|
303
|
+
abst_info1 = {AbstractCollection => [:add, :remove]}
|
304
|
+
abst_info2 = {AbstractQueue => [:clear]}
|
305
|
+
expect(Abstractable::NotImplementedInfoFinder.new.find(PriorityQueue)).to eq(abst_info1.merge(abst_info2))
|
306
|
+
expect(Abstractable::NotImplementedInfoFinder.new.find(AbstractQueue)).to eq(abst_info1)
|
307
|
+
expect(Abstractable::NotImplementedInfoFinder.new.find(AbstractCollection)).to eq({})
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "simplecov"
|
2
|
+
require "coveralls"
|
3
|
+
|
4
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
5
|
+
SimpleCov::Formatter::HTMLFormatter,
|
6
|
+
Coveralls::SimpleCov::Formatter
|
7
|
+
]
|
8
|
+
SimpleCov.start do
|
9
|
+
add_filter ".bundle/"
|
10
|
+
end
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
13
|
+
require "abstractable"
|
data/tasks/flay.rake
ADDED
data/tasks/flog.rake
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "flog"
|
2
|
+
|
3
|
+
desc "Analyze for code complexity"
|
4
|
+
task :flog do
|
5
|
+
flog = Flog.new(continue: true)
|
6
|
+
flog.flog(*FileList["lib/**/*.rb"])
|
7
|
+
threshold = 28
|
8
|
+
|
9
|
+
bad_methods = flog.totals.select do |name, score|
|
10
|
+
!(/##{flog.no_method}$/ =~ name) && score > threshold
|
11
|
+
end
|
12
|
+
bad_methods.sort { |a, b| a[1] <=> b[1] }.reverse.each do |name, score|
|
13
|
+
printf "%8.1f: %s\n", score, name
|
14
|
+
end
|
15
|
+
unless bad_methods.empty?
|
16
|
+
$stderr.puts "#{bad_methods.size} methods have a complexity > #{threshold}"
|
17
|
+
end
|
18
|
+
end
|
data/tasks/rspec.rake
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: abstractable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kenji Suzuki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Library for define abstract method. Can know unimplemented abstract methods
|
42
|
+
by fail fast as possible. This mechanism is very useful for prevent the implementation
|
43
|
+
leakage.
|
44
|
+
email:
|
45
|
+
- pujoheadsoft@gmail.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".coveralls.yml"
|
51
|
+
- ".gitignore"
|
52
|
+
- ".rspec"
|
53
|
+
- ".rubocop.yml"
|
54
|
+
- ".travis.yml"
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- abstractable.gemspec
|
60
|
+
- lib/abstractable.rb
|
61
|
+
- lib/abstractable/not_implemented_info_finder.rb
|
62
|
+
- lib/abstractable/pedigree_stream.rb
|
63
|
+
- lib/abstractable/version.rb
|
64
|
+
- spec/abstractable_spec.rb
|
65
|
+
- spec/spec_helper.rb
|
66
|
+
- tasks/flay.rake
|
67
|
+
- tasks/flog.rake
|
68
|
+
- tasks/rspec.rake
|
69
|
+
homepage: ''
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata: {}
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 2.4.1
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: Library for define abstract method.
|
93
|
+
test_files:
|
94
|
+
- spec/abstractable_spec.rb
|
95
|
+
- spec/spec_helper.rb
|