ainterface 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +146 -0
- data/lib/ainterface.rb +137 -0
- data/lib/ainterface/version.rb +9 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1a4ab42daca0488b11de9d97ac864b3f23dbec33
|
4
|
+
data.tar.gz: d2beab95da462a4c55b0ded37bb77d0c843557ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c7051c8b8b7aac4f6842cf3ae58bcb94f1bcaefea487356375141ab362218040e03707241f6ad1601012cca584c7cb053b4c4d9f16f24d5f8b6264c341b424ea
|
7
|
+
data.tar.gz: 7067e03c873901d428715c792c45dad5434f7710c63bac3de2c1102d25742fed3ec841f290133c5762688b105f2aa164f9a7282d157392ed92935c98a4a7cf7c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Fred Appelman
|
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,146 @@
|
|
1
|
+
|
2
|
+
<style> body {counter-reset: chapter; } H2:before {content: counter(chapter) ". "; counter-increment: chapter; } H2 {counter-reset: SectionH2; } H3:before {content: counter(chapter) "." counter(SectionH2) " "; counter-increment: SectionH2; } H3 {counter-reset: SectionH3; } H4:before {content: counter(chapter) "." counter(SectionH2) "." counter(SectionH3) " "; counter-increment: SectionH3; } H4 {counter-reset: SectionH4; } H5:before {content: counter(chapter) "." counter(SectionH2) "." counter(SectionH3) "." counter(SectionH4) " "; counter-increment: SectionH4; } H5 {counter-reset: SectionH5; } H6:before {content: counter(chapter) "." counter(SectionH2) "." counter(SectionH3) "." counter(SectionH4) "." counter(SectionH5) " "; counter-increment: SectionH5; } H6 {counter-reset: SectionH6; } </style>
|
3
|
+
# Interface
|
4
|
+
|
5
|
+
This Gem implements the concept of an abstract base class to Ruby.
|
6
|
+
|
7
|
+
## References
|
8
|
+
This code has been heavily influenced by the code from Mark Bates (<http://metabates.com/2011/02/07/building-interfaces-and-abstract-classes-in-ruby/>) and James Lopez (<https://github.com/bluegod/rint>).
|
9
|
+
|
10
|
+
The classes provided by Mark Beates are not really "Gem ready". The `AbstractInterface` module referenced `Bicycle`. The work from James Lopez was much closer to what I wanted. The main problem I saw with his approach is that the `must_implement` were have to be provided in the `initialize` method which feels wrong and error prone. IMHO the statements should be on the module level. Hence this Gem to address this and work on the class and module level only.
|
11
|
+
|
12
|
+
## Under the hood
|
13
|
+
This Gem will rewrite the new method of the class that implements an Interface and that new method will just do the normal init followed by a check if all required methods are implemented.
|
14
|
+
|
15
|
+
The new method now looks like this:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
def self.new(*args, &block)
|
19
|
+
obj = self.allocate
|
20
|
+
obj.send :initialize, *args, &block
|
21
|
+
obj.send :__check_interface_methods
|
22
|
+
obj
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
where `__check_interface_methods()` does the actuall checking to see if the Abstract Interface has been fully implemented.
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add this line to your application's Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem 'ainterface'
|
34
|
+
```
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
$ bundle
|
39
|
+
|
40
|
+
Or install it yourself as:
|
41
|
+
|
42
|
+
$ gem install ainterface
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
This Gem implements the concept of an abstract interface in `Ruby`.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
#!/usr/bin/env ruby
|
50
|
+
require 'ainterface'
|
51
|
+
|
52
|
+
# Define the abtract interface named Wheels
|
53
|
+
# Any class that implements Wheels must implement
|
54
|
+
# the methods 'number_of_wheels' and 'diameter'
|
55
|
+
module Wheels
|
56
|
+
must_implement :number_of_wheels
|
57
|
+
must_implement :diameter
|
58
|
+
end
|
59
|
+
|
60
|
+
# This class implments wheels.
|
61
|
+
class Car
|
62
|
+
implements Wheels
|
63
|
+
def number_of_wheels
|
64
|
+
4
|
65
|
+
end
|
66
|
+
def diameter
|
67
|
+
13
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
car = Car.new
|
72
|
+
```
|
73
|
+
|
74
|
+
In the above example Car fullfills the Wheels contract and will not raise any error.
|
75
|
+
|
76
|
+
if for example the following code would have been written:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class Bicycle
|
80
|
+
implements Wheels
|
81
|
+
def number_of_wheels
|
82
|
+
2
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
bicycle = Bicycle.new
|
87
|
+
```
|
88
|
+
|
89
|
+
The following error message would have been thrown:
|
90
|
+
|
91
|
+
(eval):4:in `block in __check_interface_methods': Expected Bicycle to implement diameter for interface Wheels (AInterface::Error::NotImplementedError)
|
92
|
+
from (eval):2:in `each'
|
93
|
+
from (eval):2:in `__check_interface_methods'
|
94
|
+
from (eval):4:in `new'
|
95
|
+
|
96
|
+
In the above example `Wheels` was a module. If desired an Interface can also be implemented as a class.
|
97
|
+
|
98
|
+
### Similar to include
|
99
|
+
Any method that was defined by the Interface module is also added
|
100
|
+
to the class that implements the interface
|
101
|
+
|
102
|
+
For example:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
module Geometry
|
106
|
+
must_implement :width
|
107
|
+
must_implement :height
|
108
|
+
|
109
|
+
def outline
|
110
|
+
2 * width + 2 * height
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Rectangle
|
115
|
+
implements Geometry
|
116
|
+
def width
|
117
|
+
4
|
118
|
+
end
|
119
|
+
def height
|
120
|
+
3
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
rect = Rectangle.new
|
125
|
+
p (rect.methods - Object.methods).sort
|
126
|
+
puts "Outline = #{rect.outline}"
|
127
|
+
```
|
128
|
+
|
129
|
+
will product the following output:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
[:height, :outline, :width]
|
133
|
+
Outline = 14
|
134
|
+
```
|
135
|
+
|
136
|
+
As such `implements` acts as an `include` statement.
|
137
|
+
|
138
|
+
## Options
|
139
|
+
The environment variable
|
140
|
+
|
141
|
+
```shell
|
142
|
+
DISABLE_RUBY_INTERFACE=1
|
143
|
+
```
|
144
|
+
|
145
|
+
can be set in order to globally disable the abstract interfaces - no Error will get thrown. This might be particularly useful in production for performance reasons if we are confident enough through tests that the interfaces are all implemented.
|
146
|
+
|
data/lib/ainterface.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
#
|
2
|
+
# Monkey patch the standard Module class
|
3
|
+
class Module
|
4
|
+
# Holds all methods that are in an interface
|
5
|
+
# for a given interface
|
6
|
+
# Use a long name to prevent accidental name conflicts
|
7
|
+
@@__interface_methods_per_interface = {}
|
8
|
+
|
9
|
+
# Called by a class to indicate it implements the specified
|
10
|
+
# interface(s)
|
11
|
+
#
|
12
|
+
# @param [AInterface] args One or more interface names that
|
13
|
+
# are being implemented
|
14
|
+
#
|
15
|
+
# @return [void]
|
16
|
+
#
|
17
|
+
def implements(*args)
|
18
|
+
raise AInterface::Error::MissingArgument if args == []
|
19
|
+
# Gather the methods from all specified interfaces
|
20
|
+
m = []
|
21
|
+
args.each do |module_name|
|
22
|
+
raise AInterface::Error::NosuchInterface, module_name.to_s unless
|
23
|
+
@@__interface_methods_per_interface[module_name.to_s]
|
24
|
+
@@__interface_methods_per_interface[module_name.to_s].each do |method_name|
|
25
|
+
m << [module_name.name, method_name]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a method that checks for the existence
|
30
|
+
# of all methods
|
31
|
+
t = <<-EOF
|
32
|
+
def __check_interface_methods
|
33
|
+
#{m}.each do |module_name, method_name|
|
34
|
+
next if respond_to? method_name
|
35
|
+
raise AInterface::Error::NotImplementedError.new(
|
36
|
+
self.class.name,
|
37
|
+
method_name,
|
38
|
+
module_name
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
private :__check_interface_methods
|
43
|
+
EOF
|
44
|
+
class_eval t unless disabled_interface?
|
45
|
+
|
46
|
+
# Redefine the new method
|
47
|
+
t = <<-EOF
|
48
|
+
def self.new(*args, &block)
|
49
|
+
obj = self.allocate
|
50
|
+
obj.send :initialize, *args, &block
|
51
|
+
obj.send :__check_interface_methods
|
52
|
+
obj
|
53
|
+
end
|
54
|
+
EOF
|
55
|
+
class_eval t unless disabled_interface?
|
56
|
+
|
57
|
+
args.each do |module_name|
|
58
|
+
prepend module_name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Errors raised here identify the class_name that is not implementing
|
63
|
+
# the method required by the Interface.
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# Specifies that an method for an interface must be
|
67
|
+
# implemented
|
68
|
+
#
|
69
|
+
# @param [Symbol] args One ore more methods that need implementation
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
#
|
73
|
+
def must_implement(*args)
|
74
|
+
raise AInterface::Error::MissingArgument if args == []
|
75
|
+
@@__interface_methods_per_interface[self.name] ||= []
|
76
|
+
args.each do |method_name|
|
77
|
+
@@__interface_methods_per_interface[self.name] << method_name
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Checks if the Interface error message must be surpressed
|
83
|
+
#
|
84
|
+
#
|
85
|
+
# @return [Boolean] Return true if error messages should be
|
86
|
+
# surpressed.
|
87
|
+
#
|
88
|
+
def disabled_interface?
|
89
|
+
ENV['DISABLE_RUBY_INTERFACE'] == '1'
|
90
|
+
end
|
91
|
+
private :disabled_interface?
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Module AInterface provides some helper classes for the
|
96
|
+
# abstract interface implementation.
|
97
|
+
#
|
98
|
+
# @author Fred Appelman <fred@appelman.net>
|
99
|
+
#
|
100
|
+
module AInterface
|
101
|
+
#
|
102
|
+
# Module Error provides error message handlers for the
|
103
|
+
# abstract interface implementation.
|
104
|
+
#
|
105
|
+
# @author Fred Appelman <fred@appelman.net>
|
106
|
+
#
|
107
|
+
module Error
|
108
|
+
#
|
109
|
+
# Class MissingArgument is raised when must_implement or
|
110
|
+
# implements is issued without an argument.
|
111
|
+
#
|
112
|
+
# @author Fred Appelman <fred@appelman.net>
|
113
|
+
#
|
114
|
+
class MissingArgument < StandardError; end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Class NosuchInterface is raised when implements
|
118
|
+
# references a Interface that wasn't defined.
|
119
|
+
#
|
120
|
+
# @author Fred Appelman <fred@appelman.net>
|
121
|
+
#
|
122
|
+
class NosuchInterface < StandardError; end
|
123
|
+
|
124
|
+
# Raised when a method has not been implemented by a class that
|
125
|
+
# has used the implements <InterfaceName> method.
|
126
|
+
class NotImplementedError < NoMethodError
|
127
|
+
def initialize(class_name, method_name, interface_name)
|
128
|
+
super(error_message(class_name, method_name, interface_name), method_name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def error_message(class_name, method_name, interface_name)
|
132
|
+
"Expected #{class_name} to implement #{method_name} for interface #{interface_name}"
|
133
|
+
end
|
134
|
+
private :error_message
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ainterface
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fred Appelman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Provides an abstract interface to Ruby
|
14
|
+
email: rubygems@appelman.net
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE.txt
|
20
|
+
- README.md
|
21
|
+
- lib/ainterface.rb
|
22
|
+
- lib/ainterface/version.rb
|
23
|
+
homepage: https://bitbucket.org/fappelman/ainterface
|
24
|
+
licenses:
|
25
|
+
- MIT
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.4.6
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Provides the concept of an Abstract Interface for the Ruby language.
|
47
|
+
test_files: []
|
48
|
+
has_rdoc: false
|