adherence 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rspec", "~> 2.11.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,24 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ adherence (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rspec (2.11.0)
11
+ rspec-core (~> 2.11.0)
12
+ rspec-expectations (~> 2.11.0)
13
+ rspec-mocks (~> 2.11.0)
14
+ rspec-core (2.11.1)
15
+ rspec-expectations (2.11.2)
16
+ diff-lcs (~> 1.1.3)
17
+ rspec-mocks (2.11.2)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ adherence!
24
+ rspec (~> 2.11.0)
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Adherence
2
+
3
+ Adherence adds simple interface-like behavior to Ruby.
4
+
5
+ ## Description
6
+
7
+ A common practice in object oriented programming languages is to declare abstract behavior in a certain type whose actual behavior must be defined by types implementing the abstract type.
8
+
9
+ In Java, for example, this is abstract behavior may be defined in an abstract class or an interface. In many dynamically types languages, like Ruby, duck typing makes these abstract types unneccessary. However, it is common to see in Ruby programs the following abstract-like method pattern:
10
+
11
+ ```ruby
12
+ class Validator
13
+ def validate(record)
14
+ raise NotImplementedError, "Subclasses must implement a validate(record) method."
15
+ end
16
+ end
17
+ ```
18
+
19
+ Adherence provides a simple means of declaring abstract behavior in Ruby modules and enforcing their complete implementation in any class adhereing to this module.
20
+
21
+ ## Usage
22
+
23
+ A class may adhere to any number of modules by use of the adheres_to method.
24
+
25
+ ```ruby
26
+ module ElectronicDevice
27
+ def on() end
28
+
29
+ def off() end
30
+
31
+ def self.compatible?(device1, device2) end
32
+ end
33
+
34
+ module Calculator
35
+ def add(x1, x2) end
36
+
37
+ def subtract(x1, x2) end
38
+
39
+ def multiply(x1, x2) end
40
+
41
+ def divide(x1, x2) end
42
+ end
43
+
44
+ class TexasInstruments
45
+ def on
46
+ @power = true
47
+ end
48
+
49
+ def off
50
+ @power = false
51
+ end
52
+
53
+ def add(x1, x2)
54
+ x1 + x2
55
+ end
56
+
57
+ def subtract(x1, x2)
58
+ x1 - x2
59
+ end
60
+
61
+ def multiply(x1, x2)
62
+ x1 * x2
63
+ end
64
+
65
+ def divide(x1, x2)
66
+ x1 / x2
67
+ end
68
+
69
+ def self.compatible?(device1, device2)
70
+ device1.respond_to?(:add) && device2.respond_to?(:add)
71
+ end
72
+
73
+ adheres_to ElectronicDevice, Calculator
74
+ end
75
+ ```
76
+
77
+ A class must define all instance and class methods of all visibilities defined in each module passed to adheres_to or an error is raised. Not only must all methods be defined, but their method signitures must match those of the specified module(s). Failure to implement all methods will result in a NotImplementedError when the class is loaded. Failure to incorrectly declare a message signature will result in a Adherence::NotAdheredError when the class is loaded. Because the methods defined in the class must be inspected, adheres_to must be called after the methods are defined in the class.
78
+
79
+ Similarly, a module may adhere to other modules:
80
+
81
+ ```ruby
82
+ module ScientificCalculator
83
+ adhere_to Calculator
84
+
85
+ def factorial(n) end
86
+
87
+ def power(base, exponent) end
88
+ end
89
+ ```
90
+
91
+ In this case, the module is not expected to redefine the methods defined in the modules passed to adheres_to, rather these methods will be redefined within the calling module to raise an error when invoked directly. Thus the module ScientificCalculator in the example above will be redefined to look like this:
92
+
93
+ ```ruby
94
+ module ScientificCalculator
95
+ def on
96
+ raise NotImplementedError, "This method must be defined in the calling class."
97
+ end
98
+
99
+ def off
100
+ raise NotImplementedError, "This method must be defined in the calling class."
101
+ end
102
+
103
+ def add(x1, x2)
104
+ raise NotImplementedError, "This method must be defined in the calling class."
105
+ end
106
+
107
+ def subtract(x1, x2)
108
+ raise NotImplementedError, "This method must be defined in the calling class."
109
+ end
110
+
111
+ def multiply(x1, x2)
112
+ raise NotImplementedError, "This method must be defined in the calling class."
113
+ end
114
+
115
+ def divide(x1, x2)
116
+ raise NotImplementedError, "This method must be defined in the calling class."
117
+ end
118
+
119
+ def self.compatible?(device1, device2)
120
+ raise NotImplementedError, "This method must be defined in the calling class."
121
+ end
122
+
123
+ def factorial(n) end
124
+
125
+ def power(base, exponent) end
126
+ end
127
+ ```
128
+
129
+ So if any class that includes or extends ScientificCalculator and does not override these methods, a NotImplementedError would be raised if any of them were called.
130
+
131
+ ## Requirements
132
+
133
+ Due to some of the methods used to inspect method signatures, Adherence is only compatible with Ruby version >= 1.9.2-p180.
134
+
135
+ ## Installation
136
+
137
+ gem install adherence
138
+
139
+ ## License
140
+
141
+ Adherence is released under the [MIT license](http://www.opensource.org/licenses/MIT). Copyright (c) 2012 Mike Bradford.
data/adherence.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "adherence"
3
+ spec.version = "0.1.0"
4
+ spec.date = "2012-08-30"
5
+ spec.summary = "Adds simple interface-like behavior to Ruby"
6
+ spec.authors = ["Mike Bradford"]
7
+ spec.email = "mbradford@47primes.com"
8
+ spec.files = Dir["#{File.dirname(__FILE__)}/**/*"]
9
+ spec.test_files = Dir.glob("spec/*_spec.rb")
10
+ spec.homepage = "http://github.com/47primes/adherence"
11
+
12
+ spec.add_development_dependency "rspec", "~> 2.11.0"
13
+ end
data/lib/adherence.rb ADDED
@@ -0,0 +1,12 @@
1
+ Dir.glob(File.dirname(__FILE__) + "/adherence/*.rb").each {|f| require f}
2
+
3
+ module Adherence
4
+ end
5
+
6
+ class Module
7
+ include Adherence::Module
8
+ end
9
+
10
+ class Class
11
+ include Adherence::Class
12
+ end
@@ -0,0 +1,38 @@
1
+ module Adherence
2
+ class NotAdheredError < NotImplementedError; end
3
+ module Class
4
+
5
+ def adheres_to(*args)
6
+ args.each {|mod| adheres_to_module mod}
7
+ end
8
+
9
+ private
10
+
11
+ def adheres_to_module(mod)
12
+ raise ArgumentError, "Module expected" unless mod.class.to_s == "Module"
13
+
14
+ (mod.instance_methods(false) + mod.private_instance_methods(false)).each do |method|
15
+ if !(instance_methods + private_instance_methods).include?(method)
16
+ raise NotImplementedError, "#{self.class} must define #{method}"
17
+ end
18
+ if !equal? instance_method(method), mod.instance_method(method)
19
+ raise NotAdheredError, "Signature does not match #{mod}##{method}"
20
+ end
21
+ end
22
+
23
+ module_class_methods = mod.singleton_class.instance_methods(false) + mod.singleton_class.private_instance_methods(false)
24
+ module_class_methods.each do |method|
25
+ if !(singleton_class.instance_methods + singleton_class.private_instance_methods).include?(method)
26
+ raise NotImplementedError, "#{self.class} must define self.#{method}"
27
+ end
28
+ if !equal? singleton_class.instance_method(method), mod.singleton_class.instance_method(method)
29
+ raise NotAdheredError, "Signature does not match #{mod}.#{method}"
30
+ end
31
+ end
32
+ end
33
+
34
+ def equal?(method1, method2)
35
+ method1.name == method2.name && method1.parameters == method2.parameters ? true : false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,63 @@
1
+ module Adherence
2
+ class ModuleTerminatedError < StandardError; end
3
+ module Module
4
+
5
+ def adheres_to(*args)
6
+ args.each { |mod| adheres_to_module(mod) }
7
+ end
8
+
9
+ private
10
+
11
+ def adheres_to_module(mod)
12
+ raise ArgumentError, "Module expected" unless mod.class.to_s == "Module"
13
+
14
+ mod.public_instance_methods(false).each do |method|
15
+ add_method mod.instance_method(method)
16
+ end
17
+
18
+ mod.protected_instance_methods(false).each do |method|
19
+ add_method mod.instance_method(method)
20
+ end
21
+ protected *mod.protected_instance_methods(false)
22
+
23
+ mod.private_instance_methods(false).each do |method|
24
+ add_method mod.instance_method(method)
25
+ end
26
+ private *mod.private_instance_methods(false)
27
+
28
+ mod.singleton_class.public_instance_methods(false).each do |method|
29
+ add_method mod.singleton_class.instance_method(method), true
30
+ end
31
+
32
+ mod.singleton_class.protected_instance_methods(false).each do |method|
33
+ add_method mod.singleton_class.instance_method(method), true
34
+ end
35
+ singleton_class.send :protected, *mod.singleton_class.protected_instance_methods(false)
36
+
37
+ mod.singleton_class.private_instance_methods(false).each do |method|
38
+ add_method mod.singleton_class.instance_method(method), true
39
+ end
40
+ private_class_method *mod.singleton_class.private_instance_methods(false)
41
+ end
42
+
43
+ def add_method(method, singleton_method=false)
44
+ name = method.name
45
+ params = method.parameters.reduce([]) do |memo, word|
46
+ arity, param = word
47
+ case arity
48
+ when :req then memo << "#{param}"
49
+ when :opt then memo << "#{param}=nil"
50
+ when :block then memo << "&#{param}"
51
+ when :rest then memo << "*#{param}"
52
+ end
53
+ end.join(", ")
54
+
55
+ module_eval <<-DEF
56
+ def #{"self." if singleton_method}#{name}(#{params})
57
+ raise NotImplementedError, "This method must be defined in the calling class."
58
+ end
59
+ DEF
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,104 @@
1
+ require "adherence"
2
+
3
+ module Animal
4
+ def speak() end
5
+
6
+ def move(feet) end
7
+
8
+ private
9
+
10
+ def life_expectancy() end
11
+
12
+ class <<self
13
+ def build(name, &block) end
14
+ end
15
+ end
16
+
17
+ describe Object, "adheres_to" do
18
+
19
+ before do
20
+ class Duck
21
+ attr_reader :name
22
+
23
+ def initialize(name)
24
+ @name = name
25
+ end
26
+
27
+ def speak
28
+ "quack"
29
+ end
30
+
31
+ def move(feet)
32
+ "waddle #{feet} feet"
33
+ end
34
+
35
+ private
36
+
37
+ def life_expectancy
38
+ 10
39
+ end
40
+
41
+ def self.build(name, &block)
42
+ Duck.new(name)
43
+ end
44
+ end
45
+ end
46
+
47
+ it "should succeed if the object has defined all behavior from the module" do
48
+ expect do
49
+ class Duck
50
+ adheres_to Animal
51
+ end
52
+ end.to_not raise_error
53
+ end
54
+
55
+ it "should raise an error if argument is not a module" do
56
+ expect do
57
+ class Duck
58
+ adheres_to Class.new
59
+ end
60
+ end.to raise_error(ArgumentError)
61
+ end
62
+
63
+ it "should raise an error if all methods are not implemented" do
64
+ Duck.send(:remove_method, :speak)
65
+
66
+ expect do
67
+ class Duck
68
+ adheres_to Animal
69
+ end
70
+ end.to raise_error(NotImplementedError)
71
+ end
72
+
73
+ it "should raise an error if all private methods are not implemented" do
74
+ Duck.send(:remove_method, :life_expectancy)
75
+
76
+ expect do
77
+ class Duck
78
+ adheres_to Animal
79
+ end
80
+ end.to raise_error(NotImplementedError)
81
+ end
82
+
83
+ it "should raise an error if all class methods are not implemented" do
84
+ Duck.singleton_class.send(:remove_method, :build)
85
+
86
+ expect { Duck.adheres_to(Animal) }.to raise_error(NotImplementedError)
87
+ end
88
+
89
+ it "should raise an error if methods are implemented with an invalid signature" do
90
+ Duck.send(:remove_method, :move)
91
+ Duck.class_eval do
92
+ def move(distance)
93
+ "waddle #{distance} feet"
94
+ end
95
+ end
96
+
97
+ expect do
98
+ class Duck
99
+ adheres_to Animal
100
+ end
101
+ end.to raise_error(Adherence::NotAdheredError)
102
+ end
103
+
104
+ end
@@ -0,0 +1,133 @@
1
+ require "adherence"
2
+
3
+ module Vehicle
4
+ def initialize(make, model) end
5
+
6
+ def start() end
7
+
8
+ def stop() end
9
+
10
+ def accellerate(speed) end
11
+
12
+ def turn(orientation, speed=5, *args) end
13
+
14
+ protected
15
+
16
+ def repair(area, &block) end
17
+
18
+ module ClassMethods
19
+ class <<self
20
+ def build(*args, &block) end
21
+
22
+ protected
23
+
24
+ def run() end
25
+
26
+ private
27
+
28
+ def helper(object) end
29
+ end
30
+ end
31
+ end
32
+
33
+ module Watercraft
34
+ adheres_to Vehicle, Vehicle::ClassMethods
35
+
36
+ def dock() end
37
+ end
38
+
39
+ describe Module, "adheres_to" do
40
+ it "should raise an error if argument is not a module" do
41
+ expect { Module.new { adheres_to(Class.new())} }.to raise_error(ArgumentError)
42
+ end
43
+
44
+ it "should define public instance methods based on module" do
45
+ Watercraft.instance_method(:start).parameters.should == []
46
+ Watercraft.instance_method(:stop).parameters.should == []
47
+ Watercraft.instance_method(:accellerate).parameters.should == [[:req, :speed]]
48
+ Watercraft.instance_method(:turn).parameters.should == [[:req, :orientation], [:opt, :speed], [:rest, :args]]
49
+ end
50
+
51
+ it "should define protected instance methods based on module" do
52
+ Watercraft.protected_method_defined?(:repair).should be_true
53
+ Watercraft.instance_method(:repair).parameters.should == [[:req, :area], [:block, :block]]
54
+ end
55
+
56
+ it "should define private instance methods based on module" do
57
+ Watercraft.private_method_defined?(:initialize).should be_true
58
+ Watercraft.instance_method(:initialize).parameters.should == [[:req, :make], [:req, :model]]
59
+ end
60
+
61
+ it "should define public class methods based on module" do
62
+ Watercraft.singleton_class.public_instance_method(:build).parameters.should == [[:rest, :args], [:block, :block]]
63
+ end
64
+
65
+ it "should define protected class methods based on module" do
66
+ Watercraft.singleton_class.protected_instance_methods.include?(:run).should be_true
67
+ Watercraft.singleton_class.instance_method(:run).parameters.should == []
68
+ end
69
+
70
+ it "should define private class methods based on module" do
71
+ Watercraft.singleton_class.private_instance_methods.include?(:helper).should be_true
72
+ Watercraft.singleton_class.instance_method(:helper).parameters.should == [[:req, :object]]
73
+ end
74
+
75
+ it "should define all public instance methods to raise an error when called directly" do
76
+ JetSki = Class.new do
77
+ include Watercraft
78
+
79
+ def initialize(make, model)
80
+ end
81
+ end
82
+
83
+ jet_ski = JetSki.new("Kawasaki","Ultra 300LX")
84
+
85
+ expect { jet_ski.start }.to raise_error(NotImplementedError)
86
+ expect { jet_ski.stop }.to raise_error(NotImplementedError)
87
+ expect { jet_ski.accellerate(10) }.to raise_error(NotImplementedError)
88
+ expect { jet_ski.turn(90, -5) }.to raise_error(NotImplementedError)
89
+ end
90
+
91
+ it "should define all protected instance methods to raise an error when called directly" do
92
+ OceanLiner = Class.new do
93
+ include Watercraft
94
+
95
+ def initialize(make, model)
96
+ end
97
+ end
98
+
99
+ expect { OceanLiner.new("RMS","Titanic").send(:repair, "stern") }.to raise_error(NotImplementedError)
100
+ end
101
+
102
+ it "should define all private instance methods to raise an error when called directly" do
103
+ RowBoat = Class.new do
104
+ include Watercraft
105
+ end
106
+
107
+ expect { RowBoat.new("Freebooter","Row Boat") }.to raise_error(NotImplementedError)
108
+ end
109
+
110
+ it "should define all public class methods to raise an error when called directly" do
111
+ Automobile = Module.new do
112
+ adheres_to Vehicle::ClassMethods
113
+ end
114
+
115
+ expect { Automobile.build("Cadillac") {|c| c.year = 2012} }.to raise_error(NotImplementedError)
116
+ end
117
+
118
+ it "should define all protected class methods to raise an error when called directly" do
119
+ Locomotive = Module.new do
120
+ adheres_to Vehicle::ClassMethods
121
+ end
122
+
123
+ expect { Locomotive.send(:run) }.to raise_error(NotImplementedError)
124
+ end
125
+
126
+ it "should define all private class methods to raise an error when called directly" do
127
+ Bicycle = Module.new do
128
+ adheres_to Vehicle::ClassMethods
129
+ end
130
+
131
+ expect { Bicycle.send(:helper, "object") }.to raise_error(NotImplementedError)
132
+ end
133
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adherence
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mike Bradford
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
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: 2.11.0
30
+ description:
31
+ email: mbradford@47primes.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ./adherence.gemspec
37
+ - ./Gemfile
38
+ - ./Gemfile.lock
39
+ - ./lib/adherence/class.rb
40
+ - ./lib/adherence/module.rb
41
+ - ./lib/adherence.rb
42
+ - ./README.md
43
+ - ./spec/adherence_module_spec.rb
44
+ - ./spec/adherence_class_spec.rb
45
+ - spec/adherence_module_spec.rb
46
+ - spec/adherence_class_spec.rb
47
+ homepage: http://github.com/47primes/adherence
48
+ licenses: []
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 1.8.23
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Adds simple interface-like behavior to Ruby
71
+ test_files:
72
+ - spec/adherence_module_spec.rb
73
+ - spec/adherence_class_spec.rb