interfaces 0.0.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +178 -0
- data/Rakefile +7 -0
- data/interfaces.gemspec +24 -0
- data/lib/interfaces/castable.rb +76 -0
- data/lib/interfaces/interface.rb +80 -0
- data/lib/interfaces/typed_accessors.rb +31 -0
- data/lib/interfaces/version.rb +3 -0
- data/lib/interfaces.rb +20 -0
- data/spec/castable_spec.rb +66 -0
- data/spec/fixtures/fixtures.rb +42 -0
- data/spec/interface_spec.rb +58 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/typed_accessor_spec.rb +21 -0
- metadata +119 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p429
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Justin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# Interfaces
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'interfaces'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install interfaces
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Ruby is a fun, super-flexible language that lets us as developers do almost anything we can dream of. Sometimes we do great with that kind of freedom, but other times we need stricter rules to help guide us. Interfaces is a library designed to help put some enforcement around the way that duck-typing is used in ruby. It allows us to ask the question 'does this object have the methods I need?' and then to say 'thanks, I promise to only call these methods.' It also helps to make code more readable, by stating 'I'm expecting you to give me an object that has these methods.'
|
22
|
+
|
23
|
+
Let's role play a development scenario. Let's say you are writing an application that will send mail through a user's mail server. You're going to store the user's configuration on the User model. To keep this example simple, we won't use ActiveRecord for our model, just a plain old class:
|
24
|
+
|
25
|
+
class User
|
26
|
+
# some properties of the user
|
27
|
+
attr_accessor :username
|
28
|
+
|
29
|
+
# some options specifically for sending mail
|
30
|
+
attr_accessor :email_server, :use_ssl?, :port, :use_html?
|
31
|
+
|
32
|
+
def email_sent_callback(mailer)
|
33
|
+
# do something after mail is sent
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
And we create a MailerService to send mail:
|
38
|
+
|
39
|
+
class MailerService
|
40
|
+
attr_accessor :user, :to, :subject, :message
|
41
|
+
|
42
|
+
def initialize(user, to, subject, message)
|
43
|
+
self.user = user
|
44
|
+
self.to = to
|
45
|
+
self.subject = subject
|
46
|
+
self.message = message
|
47
|
+
end
|
48
|
+
|
49
|
+
def deliver
|
50
|
+
# ... implement mail sending here
|
51
|
+
user.email_sent_callback(self)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
This works, but we've needlessly coupled our MailerService to our User model. The Mailer service does not really need a User, what it needs is configuration. To be slightly more explicit then we could simply rename the first parameter to 'configuration':
|
56
|
+
|
57
|
+
class MailerService
|
58
|
+
attr_accessor :configuration, :to, :subject, :message
|
59
|
+
|
60
|
+
def initialize(configuration, to, subject, message)
|
61
|
+
self.configuration = configuration
|
62
|
+
self.to = to
|
63
|
+
self.subject = subject
|
64
|
+
self.message = message
|
65
|
+
end
|
66
|
+
|
67
|
+
def deliver
|
68
|
+
# ... implement mail sending here
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
This also works fine. We can call the Mailer service and pass it a user:
|
73
|
+
|
74
|
+
MailerService.new(user, 'bob@example.com', 'test message', 'hello world!').deliver
|
75
|
+
|
76
|
+
The problem isn't that it doesn't work, it does. The problem is that the readability is low. Looking at the line of code above, one might wonder why a user is being passed into the mailer service. One might also wonder what properties of 'user' the mailer service is actually using. Is the mailer only reading properties of my user or is it *changing* the user? What if another developer comes along, noticing that the mailer is receiving a User model, and inadvertantly tightly couples MailerService to the User model? That may not cause immediate problems, but down the road services and models can become more and more tightly coupled. When you find yourself needing to use your service somewhere else you might find the de-coupling refactor to be a daunting task...
|
77
|
+
|
78
|
+
So let's re-write this code using Interfaces. First, let's define an interface:
|
79
|
+
|
80
|
+
class MailerConfiguration < Interface
|
81
|
+
abstract :email_server, :use_ssl?, :port, :use_html?, :email_sent_callback
|
82
|
+
end
|
83
|
+
|
84
|
+
Then, when we call our mailer service we cast the 'user' object to be a MailerConfiguration object:
|
85
|
+
|
86
|
+
MailerService.new(user.as(MailerConfiguration), 'bob@example.com', 'test message', 'hello world!').deliver
|
87
|
+
|
88
|
+
|
89
|
+
Now it's clear that the user is being passed in because it contains configuration information. Further, there is enforcement taking place-- if User did not implement one of the four required methods, a clear exception would be fired at runtime. And if MailerService tries to call another method of User that is not defined in the MailerConfiguration interface, an exception will be thrown. Lastly, there's one place to look to determine what methods are needed by MailerConfiguration-- the code is self documenting.
|
90
|
+
|
91
|
+
Behind the scenes what is really happening here is that a new MailerConfiguration instance is being created, and then the abstract methods are being redefined on that instance to proxy to the equivalent methods on the 'user' object.
|
92
|
+
|
93
|
+
Interfaces have a few other capabilities. First, they can be instantiated using a hash if you don't want to use duck typing, making them much more like a Struct that can also contain arbitrary methods.
|
94
|
+
|
95
|
+
config = MailerConfiguration.new(:email_server => 'myemailserver.com',
|
96
|
+
:port => 443,
|
97
|
+
:use_ssl? => true,
|
98
|
+
:use_html? => true,
|
99
|
+
:email_sent_callback => lambda { |s| puts "Mail sent!"})
|
100
|
+
MailerService.new(config, 'bob@example.com', 'test message', 'hello world!').deliver
|
101
|
+
|
102
|
+
Interfaces can also be derived from other interfaces, both adding or removing abstract methods:
|
103
|
+
|
104
|
+
class SecureMailerConfiguration < MailerConfiguration
|
105
|
+
abstract :vpn
|
106
|
+
|
107
|
+
# always use ssl
|
108
|
+
def use_ssl?
|
109
|
+
true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
This breaks away from the traditional notion of 'interfaces' in that we're now implementing methods directly on an interface. This practice is much more similar to the notion of abstract classes in other languages. Let's view the abstract methods of both of these interfaces:
|
114
|
+
|
115
|
+
MailerConfiguration.abstract_methods
|
116
|
+
=> [:email_server, :use_ssl?, :port, :use_html?, :email_sent_callback]
|
117
|
+
|
118
|
+
SecureMailerConfiguration.abstract_methods
|
119
|
+
=> [:vpn, :email_server, :port, :use_html?, :email_sent_callback]
|
120
|
+
|
121
|
+
Note that the use_ssl? method is no longer abstract in the SecureMailerConfiguration interface because it has been implemented.
|
122
|
+
|
123
|
+
## Optional methods
|
124
|
+
|
125
|
+
An interface may contain optional methods. If they are defined by a class then they will be delegated to, but if they are not defined they will simply return nil. This alleviates the developer from having to add respond_to? checks before calling methods that may or may not be defined.
|
126
|
+
|
127
|
+
class TestInterface < Interface
|
128
|
+
abstract :field1
|
129
|
+
optional :field2
|
130
|
+
end
|
131
|
+
|
132
|
+
TestInterface.new.field2
|
133
|
+
=> nil
|
134
|
+
|
135
|
+
## Typed accessors
|
136
|
+
|
137
|
+
The typed_attr_accessor and typed_attr_writer helpers make it easy to create attributes that always conform to an interface:
|
138
|
+
|
139
|
+
class MailerService
|
140
|
+
typed_attr_accessor :config => MailerConfiguration
|
141
|
+
# ...
|
142
|
+
end
|
143
|
+
|
144
|
+
Now when the 'config' attribute is assigned it will be automatically converted to an instance of MailerConfiguration or it will raise an exception if it cannot be converted.
|
145
|
+
|
146
|
+
## Built-in and custom conversions for non-interfaces
|
147
|
+
|
148
|
+
For the basic ruby types (String, Symbol, Integer, Float, Array, Hash (on ruby 2.0)) there are built-in conversions that simply call the corresponding standard ruby conversion method (to_s, to_sym, to_i, to_f, to_a, to_h):
|
149
|
+
|
150
|
+
"test".as(Symbol) == :test
|
151
|
+
|
152
|
+
This allows the typed_attr_accessor to be used with these standard types.
|
153
|
+
|
154
|
+
Additional custom conversions can be defined by overriding the 'as' method in a class.
|
155
|
+
|
156
|
+
## Interface caching and state
|
157
|
+
|
158
|
+
Interfaces are full-fledged ruby classes, and as such they can have methods and instance variables (state). To ensure that this state is maintained each time the object is cast, an interface cache is maintained on any object that has been casted at least once. This means that the following is always true:
|
159
|
+
|
160
|
+
user.as(MailerConfiguration) === user.as(MailerConfiguration)
|
161
|
+
|
162
|
+
## Usage with the 'contracts' gem
|
163
|
+
|
164
|
+
The 'interfaces' gem does not enforce that a parameter passed to a method conforms to an interface, but this can be achieved by using the [contracts gem](https://github.com/egonSchiele/contracts.ruby):
|
165
|
+
|
166
|
+
class MailerService
|
167
|
+
attr_accessor :configuration, :to, :subject, :message
|
168
|
+
|
169
|
+
Contract MailerConfiguration, String, String, String => MailerService
|
170
|
+
def initialize(configuration, to, subject, message)
|
171
|
+
|
172
|
+
## Contributing
|
173
|
+
|
174
|
+
1. Fork it
|
175
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
176
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
177
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
178
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/interfaces.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'interfaces/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "interfaces"
|
8
|
+
spec.version = Interfaces::VERSION
|
9
|
+
spec.authors = ["Justin Schumacher"]
|
10
|
+
spec.email = ["justin@thethinkingtree.com"]
|
11
|
+
spec.description = %q{This library provides a concept of Interfaces and abstract classes to the ruby language}
|
12
|
+
spec.summary = %q{Interfaces for ruby}
|
13
|
+
spec.homepage = "https://github.com/thinkingtree/ruby-interfaces"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
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 "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Interfaces
|
2
|
+
# a mixin that defines the 'as' method to allow an object to be cast as an interface
|
3
|
+
module Castable
|
4
|
+
BUILT_IN_CONVERSIONS = {
|
5
|
+
Array => :to_a,
|
6
|
+
String => :to_s,
|
7
|
+
Symbol => :to_sym,
|
8
|
+
Integer => :to_i,
|
9
|
+
Float => :to_f,
|
10
|
+
Hash => :to_h # only on ruby 2.0
|
11
|
+
}
|
12
|
+
|
13
|
+
# attempts to convert non-interface types
|
14
|
+
def self.convert_type(instance, type)
|
15
|
+
method = BUILT_IN_CONVERSIONS[type]
|
16
|
+
if method && instance.respond_to?(method)
|
17
|
+
instance.send(method)
|
18
|
+
else
|
19
|
+
raise NonConvertableObjectError, "Don't know how to convert #{instance} to #{type}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def as(interface)
|
24
|
+
# interface must be a class
|
25
|
+
raise InterfaceError, "#{interface} is not a class" unless interface.is_a?(Class)
|
26
|
+
|
27
|
+
# check if object already is an instance of interface
|
28
|
+
return self if self.kind_of?(interface)
|
29
|
+
|
30
|
+
# check if interface is really an interface
|
31
|
+
if interface < Interface
|
32
|
+
# cache the resulting interface so that we can load it faster next
|
33
|
+
# time and so that it can save state
|
34
|
+
cache = self.instance_variable_get(:@interface_cache)
|
35
|
+
unless cache
|
36
|
+
cache = {}
|
37
|
+
self.instance_variable_set(:@interface_cache, cache)
|
38
|
+
end
|
39
|
+
|
40
|
+
cache[interface] ||= begin
|
41
|
+
i = interface.new
|
42
|
+
delegate = self
|
43
|
+
non_implemented_methods = []
|
44
|
+
|
45
|
+
# define singleton methods that delegate back to this object for each abstract method
|
46
|
+
interface.abstract_methods.each do |method|
|
47
|
+
non_implemented_methods << method unless self.respond_to?(method)
|
48
|
+
i.define_singleton_method(method) do |*args|
|
49
|
+
delegate.send(method, *args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# raise an exception if all abstract methods are not overridden
|
54
|
+
unless non_implemented_methods.empty?
|
55
|
+
raise NonConformingObjectError, "#{self} does not conform to interface #{interface}. Expected methods not implemented: #{non_implemented_methods.join(", ")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# define singleton methods that delegate back to this object for each optional method
|
59
|
+
interface.optional_methods.each do |method|
|
60
|
+
if self.respond_to?(method)
|
61
|
+
i.define_singleton_method(method) do |*args|
|
62
|
+
delegate.send(method, *args)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
i
|
68
|
+
end
|
69
|
+
else
|
70
|
+
# interface is not really an interface, it's just a Class
|
71
|
+
# use some built-in conversions
|
72
|
+
Castable.convert_type(self, interface)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Interfaces
|
4
|
+
# Interface is meant to be used as a base class for the definition of Interfaces
|
5
|
+
class Interface
|
6
|
+
# Define methods as asbtract
|
7
|
+
def self.abstract(*methods)
|
8
|
+
@abstract_methods ||= Set.new
|
9
|
+
methods.each do |method|
|
10
|
+
# the default implmentation of an abstract method is to
|
11
|
+
# raise the AbstractMethodInvokedError exception
|
12
|
+
define_method(method) do
|
13
|
+
raise AbstractMethodInvokedError, "Abstract method #{method} called"
|
14
|
+
end
|
15
|
+
@abstract_methods << method.to_sym
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Lists all abstract methods of a class
|
20
|
+
def self.abstract_methods
|
21
|
+
if self == Interface
|
22
|
+
[]
|
23
|
+
else
|
24
|
+
# determine which methods of superclass are still undefined
|
25
|
+
nonoverrided_superclass_abtract_methods = superclass.abstract_methods.reject do |m|
|
26
|
+
self.instance_methods.include?(m) && self.instance_method(m).owner == self
|
27
|
+
end
|
28
|
+
( (@abstract_methods || Set.new) + nonoverrided_superclass_abtract_methods ).to_a
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# True if a class has abstract methods
|
33
|
+
def self.abstract?
|
34
|
+
!abstract_methods.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
# List all optional methods of a class
|
38
|
+
def self.optional_methods
|
39
|
+
if self == Interface
|
40
|
+
[]
|
41
|
+
else
|
42
|
+
( (@optional_methods || Set.new) + superclass.optional_methods ).to_a
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Define methods as optional
|
47
|
+
def self.optional(*methods)
|
48
|
+
@optional_methods ||= Set.new
|
49
|
+
methods.each do |method|
|
50
|
+
# the default implmentation of an optional method is to return nil
|
51
|
+
define_method(method) do
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
@optional_methods << method.to_sym
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# allow interfaces to be instantiated directly by passing
|
59
|
+
# in values for each of the abstract methods
|
60
|
+
def initialize(opts = {})
|
61
|
+
opts.each_pair do |key, value|
|
62
|
+
|
63
|
+
# only allow initializers for abstract methods
|
64
|
+
unless self.class.abstract_methods.include?(key.to_sym) || self.class.optional_methods.include?(key.to_sym)
|
65
|
+
raise InterfaceError, "Attempted to assign value to method '#{key}' which is not an abstract or optional method of #{self.class}"
|
66
|
+
end
|
67
|
+
|
68
|
+
if value.is_a?(Proc)
|
69
|
+
# if the value is a proc then the abstract method
|
70
|
+
# override should call that proc
|
71
|
+
self.define_singleton_method(key, &value)
|
72
|
+
else
|
73
|
+
# if the value is not a proc, then the abstract
|
74
|
+
# method override should just return the value
|
75
|
+
self.define_singleton_method(key) { value }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Interfaces
|
2
|
+
module TypedAccessors
|
3
|
+
# use like:
|
4
|
+
# typed_attr_accessor :field_name => InterfaceName
|
5
|
+
def typed_attr_accessor(attrs)
|
6
|
+
# attrs.each_pair |attr_name,interface|
|
7
|
+
# inst_variable_name = "@#{attr_name}"
|
8
|
+
# define_method method_name do
|
9
|
+
# instance_variable_get inst_variable_name
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
|
13
|
+
# use the standard reader
|
14
|
+
attrs.keys.each do |attr|
|
15
|
+
attr_reader attr
|
16
|
+
end
|
17
|
+
|
18
|
+
# also define writers
|
19
|
+
typed_attr_writer attrs
|
20
|
+
end
|
21
|
+
|
22
|
+
def typed_attr_writer(attrs)
|
23
|
+
attrs.each_pair do |attr_name,interface|
|
24
|
+
inst_variable_name = "@#{attr_name}"
|
25
|
+
define_method "#{attr_name}=" do |new_value|
|
26
|
+
instance_variable_set inst_variable_name, new_value ? new_value.as(interface) : nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/interfaces.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "interfaces/version"
|
2
|
+
require "interfaces/interface"
|
3
|
+
require "interfaces/castable"
|
4
|
+
require "interfaces/typed_accessors"
|
5
|
+
|
6
|
+
module Interfaces
|
7
|
+
class InterfaceError < StandardError; end
|
8
|
+
class AbstractMethodInvokedError < InterfaceError; end
|
9
|
+
class NonConformingObjectError < InterfaceError; end
|
10
|
+
class NonConvertableObjectError < InterfaceError; end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Object
|
14
|
+
include Interfaces::Castable
|
15
|
+
Interface = Interfaces::Interface
|
16
|
+
end
|
17
|
+
|
18
|
+
class Class
|
19
|
+
include Interfaces::TypedAccessors
|
20
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Interfaces::Castable do
|
4
|
+
it 'raises an exception when attempting to cast to a non-Class' do
|
5
|
+
expect { Object.new.as("something") }.to raise_error(Interfaces::InterfaceError)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'conversions' do
|
9
|
+
it 'can convert to String' do
|
10
|
+
:test.as(String).should == "test"
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can convert to Symbol' do
|
14
|
+
"test".as(Symbol).should == :test
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can convert to an Array' do
|
18
|
+
{:test => 1}.as(Array).should == [[:test, 1]]
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'can convert to an Integer' do
|
22
|
+
"1".as(Integer).should == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can convert to a Float' do
|
26
|
+
"1.1".as(Float).should == 1.1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ClassConformingToTestInterface do
|
31
|
+
let(:instance) { ClassConformingToTestInterface.new }
|
32
|
+
let(:casted_instance) { instance.as(TestInterface) }
|
33
|
+
|
34
|
+
it 'casted_instance should be an instance of TestInterface' do
|
35
|
+
casted_instance.should be_a(TestInterface)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'casted_instance should delegate to the overridden methods' do
|
39
|
+
casted_instance.method1.should == 1
|
40
|
+
casted_instance.method2.should == 2
|
41
|
+
casted_instance.method3(2).should == 8
|
42
|
+
casted_instance.opt_method.should == 5
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should return the same casted instance if cast is called multiple times' do
|
46
|
+
instance.as(TestInterface).should === casted_instance
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe FullyImplimentedClass do
|
51
|
+
let(:instance) { FullyImplimentedClass.new }
|
52
|
+
|
53
|
+
it 'should just return self if attempting to cast an object that is already the right kind of object' do
|
54
|
+
instance.as(TestInterface).should === instance
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe ClassNotConformingToTestInterface do
|
59
|
+
let(:instance) { ClassNotConformingToTestInterface.new }
|
60
|
+
|
61
|
+
it 'should raise an error when casted to TestInterface' do
|
62
|
+
expected_message = "#{instance.to_s} does not conform to interface TestInterface. Expected methods not implemented: method3"
|
63
|
+
expect { instance.as(TestInterface) }.to raise_error(Interfaces::NonConformingObjectError, expected_message)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class TestInterface < Interface
|
2
|
+
abstract :method1, :method2
|
3
|
+
abstract :method3
|
4
|
+
|
5
|
+
optional :opt_method
|
6
|
+
end
|
7
|
+
|
8
|
+
class TestSubInterface < TestInterface
|
9
|
+
abstract :method4
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestSubInterfaceWithOverride < TestInterface
|
13
|
+
abstract :method4
|
14
|
+
|
15
|
+
# sub interface overrides method3
|
16
|
+
def method3(x)
|
17
|
+
x * 2
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FullyImplimentedClass < TestInterface
|
22
|
+
def method1; end
|
23
|
+
def method2; end
|
24
|
+
def method3; end
|
25
|
+
end
|
26
|
+
|
27
|
+
class ClassConformingToTestInterface
|
28
|
+
def method1; 1; end
|
29
|
+
def method2; 2; end
|
30
|
+
def method3(x); x * 4; end
|
31
|
+
def opt_method; 5; end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ClassNotConformingToTestInterface
|
35
|
+
def method1; 1; end
|
36
|
+
def method2; 2; end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ClassWithTypedAttributes
|
40
|
+
typed_attr_accessor :field1 => TestInterface
|
41
|
+
typed_attr_writer :field2 => TestInterface
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Interfaces::Interface do
|
4
|
+
describe TestInterface do
|
5
|
+
subject { TestInterface }
|
6
|
+
|
7
|
+
it { should be_abstract}
|
8
|
+
|
9
|
+
it 'can define abstract methods' do
|
10
|
+
subject.abstract_methods.should =~ [:method1, :method2, :method3]
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can define optional methods' do
|
14
|
+
subject.optional_methods.should =~ [:opt_method]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can be instantiated with a hash to override abstract methods with constants or Procs' do
|
18
|
+
i = subject.new(:method1 => 1, :method2 => 2, :method3 => lambda { |x| x + 1 }, :opt_method => 4)
|
19
|
+
i.method1.should == 1
|
20
|
+
i.method2.should == 2
|
21
|
+
i.method3(2).should == 3
|
22
|
+
i.opt_method.should == 4
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'will raise an exception if an abstract method is called' do
|
26
|
+
expect { subject.new(:method1 => 1, :method2 => 2).method3 }.to raise_error(Interfaces::AbstractMethodInvokedError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'will return nil if a non-overridden optional method is called' do
|
30
|
+
subject.new(:method1 => 1, :method2 => 2).opt_method.should be_nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe TestSubInterface do
|
35
|
+
subject { TestSubInterface }
|
36
|
+
|
37
|
+
it { should be_abstract }
|
38
|
+
|
39
|
+
it 'should inherit abstract methods of base interface' do
|
40
|
+
subject.abstract_methods.should =~ [:method1, :method2, :method3, :method4]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe TestSubInterfaceWithOverride do
|
45
|
+
subject { TestSubInterfaceWithOverride }
|
46
|
+
|
47
|
+
it 'should be able to override an abstract method defined in its base interface' do
|
48
|
+
subject.abstract_methods.should =~ [:method1, :method2, :method4]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe FullyImplimentedClass do
|
53
|
+
subject { FullyImplimentedClass }
|
54
|
+
|
55
|
+
it { should_not be_abstract }
|
56
|
+
its(:abstract_methods) { should be_empty }
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Interfaces::TypedAccessors do
|
4
|
+
describe ClassWithTypedAttributes do
|
5
|
+
let(:instance) { ClassWithTypedAttributes.new }
|
6
|
+
|
7
|
+
it 'should convert an object to the correct type when it is assigned' do
|
8
|
+
instance.field1 = FullyImplimentedClass.new
|
9
|
+
instance.field1.should be_a_kind_of(TestInterface)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should throw an exception if an object that does not conform to the interface is passed' do
|
13
|
+
expect { instance.field1 = Object.new }.to raise_error(Interfaces::NonConformingObjectError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should allow nil to be assigned' do
|
17
|
+
instance.field1 = nil
|
18
|
+
instance.field1.should be_nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interfaces
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2.pre
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Justin Schumacher
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-05 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: This library provides a concept of Interfaces and abstract classes to
|
63
|
+
the ruby language
|
64
|
+
email:
|
65
|
+
- justin@thethinkingtree.com
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- .gitignore
|
71
|
+
- .rspec
|
72
|
+
- .ruby-version
|
73
|
+
- Gemfile
|
74
|
+
- LICENSE
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- interfaces.gemspec
|
78
|
+
- lib/interfaces.rb
|
79
|
+
- lib/interfaces/castable.rb
|
80
|
+
- lib/interfaces/interface.rb
|
81
|
+
- lib/interfaces/typed_accessors.rb
|
82
|
+
- lib/interfaces/version.rb
|
83
|
+
- spec/castable_spec.rb
|
84
|
+
- spec/fixtures/fixtures.rb
|
85
|
+
- spec/interface_spec.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
- spec/typed_accessor_spec.rb
|
88
|
+
homepage: https://github.com/thinkingtree/ruby-interfaces
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>'
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.3.1
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.8.23
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Interfaces for ruby
|
113
|
+
test_files:
|
114
|
+
- spec/castable_spec.rb
|
115
|
+
- spec/fixtures/fixtures.rb
|
116
|
+
- spec/interface_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
- spec/typed_accessor_spec.rb
|
119
|
+
has_rdoc:
|