interfaces 0.0.2.pre

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p429
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interfaces.gemspec
4
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new("spec")
5
+
6
+ # If you want to make this the default task
7
+ task :default => :spec
@@ -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
@@ -0,0 +1,3 @@
1
+ module Interfaces
2
+ VERSION = "0.0.2.pre"
3
+ 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
@@ -0,0 +1,6 @@
1
+ require 'interfaces'
2
+ require 'fixtures/fixtures.rb'
3
+
4
+ RSpec.configure do |config|
5
+ # some (optional) config here
6
+ end
@@ -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: