rubycube 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/cube.gemspec +1 -1
- data/examples/demo.rb +23 -31
- data/examples/traits.rb +69 -0
- data/lib/cube.rb +1 -0
- data/lib/cube/interfaces.rb +109 -120
- data/lib/cube/toplevel.rb +104 -0
- data/lib/cube/traits.rb +45 -9
- data/rubycube-0.2.1.gem +0 -0
- data/test/test_interface.rb +22 -16
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c136c0dc0b938dbd1ffe392203ae39781e7ebaf1
|
4
|
+
data.tar.gz: 355531cf507cee9bd3c68a69e7ebe59255ef6773
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5064b3d52cbe81ee4429a6054d33046b279f0b6fdcb52d1667503d82e17fb6667157c1bc7ace38d7dde154388340c38dac20d2f779c02fd8241070040f56969a
|
7
|
+
data.tar.gz: 2a6b3e2fa5c0282e5d2529a81c22722e57a427fa9059112fccb6edfb8a4405767ad4b92cf81acb853272cfa283b3552297386aa92ce211ca9f6bf1d76bf6dd8f
|
data/README.md
CHANGED
@@ -6,13 +6,15 @@ It provides interfaces, traits and runtime interface checks in Ruby
|
|
6
6
|
`gem install rubycube` OR
|
7
7
|
`gem rubycube` in your Gemfile
|
8
8
|
|
9
|
+
### Theory
|
10
|
+
|
11
|
+
http://web.archive.org/web/20061209142330/http://www.iam.unibe.ch/~scg/Archive/Papers/Scha02bTraits.pdf
|
12
|
+
|
9
13
|
### Synopsis
|
10
14
|
See [this annotated example](examples/demo.rb)
|
11
15
|
|
12
16
|
### General Notes
|
13
17
|
|
14
|
-
Subinterfaces work as well.
|
15
|
-
|
16
18
|
Since runtime checks are meant to be invoked for every invocation of a method,
|
17
19
|
there is a runtime overhead associated which may not be desirable in
|
18
20
|
production. Hence these checks are guarded by an environment variable
|
data/cube.gemspec
CHANGED
data/examples/demo.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# run as `RUBY_CUBE_TYPECHECK= 1 ruby examples/demo.rb`
|
2
2
|
require_relative '../lib/cube'
|
3
3
|
|
4
|
-
Adder = interface {
|
4
|
+
Adder = Cube.interface {
|
5
5
|
# sum is a method that takes an array of Integer and returns an Integer
|
6
6
|
proto(:sum, [Integer]) { Integer }
|
7
7
|
}
|
8
8
|
|
9
|
-
Calculator = interface {
|
9
|
+
Calculator = Cube.interface {
|
10
10
|
# interfaces can be composed
|
11
11
|
extends Adder
|
12
12
|
# method fact takes an Integer and returns an Integer
|
@@ -15,7 +15,7 @@ Calculator = interface {
|
|
15
15
|
proto(:pos, [Integer], Integer) { [Integer, NilClass].to_set }
|
16
16
|
}
|
17
17
|
|
18
|
-
class
|
18
|
+
class SimpleCalcImpl
|
19
19
|
def fact(n)
|
20
20
|
(2..n).reduce(1) { |m, e| m * e }
|
21
21
|
end
|
@@ -27,63 +27,55 @@ class SimpleCalc
|
|
27
27
|
def pos(arr, i)
|
28
28
|
arr.index(i)
|
29
29
|
end
|
30
|
-
|
31
|
-
# implements Calculator #, runtime_checks: false # default is true
|
32
30
|
end
|
33
31
|
|
34
|
-
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
32
|
+
SimpleCalc = Cube[SimpleCalcImpl].as_interface(Calculator)
|
33
|
+
# OR
|
34
|
+
# SimpleCalc = Cube.from(SimpleCalcImpl).as_interface(Calculator)
|
35
|
+
c = SimpleCalc.new
|
38
36
|
p c.sum([1, 2])
|
39
37
|
p c.pos([1, 2, 3], 4)
|
40
38
|
|
41
|
-
AdvancedCalculator = interface {
|
39
|
+
AdvancedCalculator = Cube.interface {
|
42
40
|
extend Calculator
|
43
41
|
proto(:product, Integer, Integer) { Integer }
|
42
|
+
proto(:avg, [Integer]) { Float }
|
44
43
|
}
|
45
44
|
|
46
|
-
|
47
|
-
extend Cube::Trait
|
45
|
+
ProductCalcT = Cube.trait do
|
48
46
|
def product(a, b)
|
49
47
|
ret = 0
|
50
48
|
a.times { ret = sum([ret, b]) }
|
51
49
|
ret
|
52
50
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
51
|
+
# This specifies the interface that the including Class must satisfy in order for
|
52
|
+
# this trait to work properly.
|
53
|
+
# Eg, the product method above uses `sum`, which it expects to get from the including
|
54
|
+
# class
|
58
55
|
requires_interface Adder # Note that this will give an error if SimpleCalc#sum is removed
|
59
56
|
# even if this trait itself has a `sum` method
|
60
57
|
end
|
61
58
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
def
|
66
|
-
|
67
|
-
class << self
|
68
|
-
def included(_)
|
69
|
-
$stderr.puts "Works like a regular module as well"
|
70
|
-
end
|
59
|
+
StatsCalcT = Cube.trait do
|
60
|
+
def product; end
|
61
|
+
|
62
|
+
def avg(arr)
|
63
|
+
arr.reduce(0, &:+) / arr.size
|
71
64
|
end
|
72
65
|
end
|
73
|
-
|
66
|
+
#
|
74
67
|
# This is how we compose behaviours
|
75
68
|
# AdvancedCalc is a class which mixes traits AdvancedCalcT and DummyCalcT
|
76
69
|
# into SimpleCalc and implements the interface AdvancedCalculator
|
77
70
|
# To avoid conflicts, alias methods in AdvancedCalcT (otherwise error will be raised)
|
78
71
|
# One can also suppress methods in DummyCalcT
|
79
|
-
AdvancedCalc = SimpleCalc.with_trait(
|
80
|
-
|
81
|
-
.with_trait(DummyCalcT, suppresses: [:foo, :bar])
|
72
|
+
AdvancedCalc = SimpleCalc.with_trait(ProductCalcT)
|
73
|
+
.with_trait(StatsCalcT, suppress: [:product])
|
82
74
|
.as_interface(AdvancedCalculator)
|
83
|
-
|
84
75
|
sc = AdvancedCalc.new
|
85
76
|
p sc.product(3, 2)
|
86
77
|
|
78
|
+
__END__
|
87
79
|
|
88
80
|
# Benchmarks. Run with RUBY_INTERFACE_TYPECHECK=0 and 1 to compare
|
89
81
|
|
data/examples/traits.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
ENV['RUBY_CUBE_TYPECHECK'] = "1"
|
2
|
+
require_relative '../lib/cube'
|
3
|
+
EmailNotifierT = Cube.trait do
|
4
|
+
def notify
|
5
|
+
puts "email sent to #{email}"
|
6
|
+
end
|
7
|
+
requires_interface Cube.interface {
|
8
|
+
proto(:email) { String }
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
AndroidNotifierT = Cube.trait do
|
13
|
+
def notify
|
14
|
+
puts "push notification sent to android device #{mobile_number}"
|
15
|
+
end
|
16
|
+
|
17
|
+
requires_interface Cube.interface {
|
18
|
+
proto(:mobile_number) { String }
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
IOSNotifierT = Cube.trait do
|
23
|
+
def notify
|
24
|
+
puts "push notification sent to ios device #{mobile_number}"
|
25
|
+
end
|
26
|
+
|
27
|
+
requires_interface Cube.interface {
|
28
|
+
proto(:mobile_number) { String }
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
CombinedNotifierT = Cube.trait do
|
33
|
+
def notify
|
34
|
+
mobile_notify
|
35
|
+
email_notify
|
36
|
+
end
|
37
|
+
requires_interface Cube.interface {
|
38
|
+
proto(:mobile_notify)
|
39
|
+
proto(:email_notify)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
AndroidCombinedNotifier = Cube.trait.with_trait(AndroidNotifierT, rename: { notify: :mobile_notify })
|
44
|
+
.with_trait(EmailNotifierT, rename: { notify: :email_notify })
|
45
|
+
.with_trait(CombinedNotifierT)
|
46
|
+
|
47
|
+
IOSCombinedNotifier = Cube.trait.with_trait(IOSNotifierT, rename: { notify: :mobile_notify })
|
48
|
+
.with_trait(EmailNotifierT, rename: { notify: :email_notify })
|
49
|
+
.with_trait(CombinedNotifierT)
|
50
|
+
|
51
|
+
MobileEmailUser = Cube.interface {
|
52
|
+
proto(:email) { String }
|
53
|
+
proto(:mobile_number) { String }
|
54
|
+
}
|
55
|
+
|
56
|
+
Services = {
|
57
|
+
android: AndroidCombinedNotifier.wrap(MobileEmailUser),
|
58
|
+
ios: IOSCombinedNotifier.wrap(MobileEmailUser)
|
59
|
+
}
|
60
|
+
User = Struct.new(:email, :mobile_number, :type)
|
61
|
+
Cube.mark_interface!(User, MobileEmailUser)
|
62
|
+
CubeUser = Cube[User].as_interface(MobileEmailUser)
|
63
|
+
|
64
|
+
u1 = User.new('u1@foo.com', '1234567890', :android)
|
65
|
+
u2 = User.new('u2@foo.com', '1234567899', :ios)
|
66
|
+
|
67
|
+
[u1, u2].each do |u|
|
68
|
+
Services[u.type].new(u).notify
|
69
|
+
end
|
data/lib/cube.rb
CHANGED
data/lib/cube/interfaces.rb
CHANGED
@@ -5,22 +5,45 @@ require 'set'
|
|
5
5
|
#
|
6
6
|
# http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
|
7
7
|
#
|
8
|
+
|
9
|
+
# Top level module for RubyCube
|
8
10
|
module Cube
|
9
11
|
module Interface
|
10
|
-
# The version of the interface library.
|
11
|
-
Interface::VERSION = '0.2.0'
|
12
12
|
|
13
|
-
|
13
|
+
def self.match_specs(i1specs, i2specs)
|
14
|
+
i1specs.each do |meth, i1spec|
|
15
|
+
i2spec = i2specs[meth]
|
16
|
+
raise InterfaceMatchError, "Method `#{meth}` not found" unless i2spec
|
17
|
+
i2_in = i2spec[:in]
|
18
|
+
i1_in = i1spec[:in]
|
19
|
+
if i1_in && (!i2_in || i1_in.size != i2_in.size)
|
20
|
+
raise InterfaceMatchError, "Method `#{meth}` prototype does not match"
|
21
|
+
end
|
22
|
+
(i1_in || []).each_index do |i|
|
23
|
+
Cube.check_type_spec(i1_in[i], i2_in[i]) { |t1, t2| t2 == t1 }
|
24
|
+
end
|
25
|
+
i1_out = i1spec[:out]
|
26
|
+
if i1_out
|
27
|
+
i2_out = i2spec[:out]
|
28
|
+
raise InterfaceMatchError, "Method `#{meth}` prototype does not match" unless i2_out
|
29
|
+
Cube.check_type_spec(i1_out, i2_out) { |t1, t2| t2 == t1 }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Exceptions thrown while checking interfaces
|
14
35
|
class MethodMissing < RuntimeError; end
|
15
36
|
class PrivateVisibleMethodMissing < MethodMissing; end
|
16
37
|
class PublicVisibleMethodMissing < MethodMissing; end
|
17
38
|
class MethodArityError < RuntimeError; end
|
18
39
|
class TypeMismatchError < RuntimeError; end
|
40
|
+
class InterfaceMatchError < RuntimeError; end
|
19
41
|
|
20
42
|
alias :extends :extend
|
21
43
|
|
22
44
|
private
|
23
45
|
|
46
|
+
# convert a proc to lambda
|
24
47
|
def convert_to_lambda &block
|
25
48
|
obj = Object.new
|
26
49
|
obj.define_singleton_method(:_, &block)
|
@@ -33,20 +56,20 @@ module Cube
|
|
33
56
|
included(obj)
|
34
57
|
end
|
35
58
|
|
59
|
+
# This is called before `included`
|
36
60
|
def append_features(mod)
|
37
61
|
return super if Interface === mod
|
38
|
-
|
62
|
+
|
39
63
|
# Is this a sub-interface?
|
64
|
+
# Get specs from super interfaces
|
40
65
|
inherited = (self.ancestors-[self]).select{ |x| Interface === x }
|
41
66
|
inherited_ids = inherited.map{ |x| x.instance_variable_get('@ids') }
|
42
|
-
|
67
|
+
|
43
68
|
# Store required method ids
|
44
|
-
|
45
|
-
specs = @ids.merge(inherited_specs)
|
69
|
+
specs = to_spec
|
46
70
|
ids = @ids.keys + map_spec(inherited_ids.flatten).keys
|
47
71
|
@unreq ||= []
|
48
|
-
|
49
|
-
|
72
|
+
|
50
73
|
# Iterate over the methods, minus the unrequired methods, and raise
|
51
74
|
# an error if the method has not been defined.
|
52
75
|
mod_public_instance_methods = mod.public_instance_methods(true)
|
@@ -57,79 +80,87 @@ module Cube
|
|
57
80
|
end
|
58
81
|
spec = specs[id]
|
59
82
|
if spec.is_a?(Hash) && spec.key?(:in) && spec[:in].is_a?(Array)
|
83
|
+
# Check arity and replace method with type checking method
|
60
84
|
replace_check_method(mod, id, spec[:in], spec[:out])
|
61
85
|
end
|
62
86
|
end
|
63
|
-
|
64
|
-
inherited_private_ids = inherited.map{ |x| x.instance_variable_get('@private_ids') }
|
65
|
-
# Store required method ids
|
66
|
-
private_ids = @private_ids.keys + map_spec(inherited_private_ids.flatten).keys
|
67
|
-
|
68
|
-
# Iterate over the methods, minus the unrequired methods, and raise
|
69
|
-
# an error if the method has not been defined.
|
70
|
-
mod_all_methods = mod.instance_methods(true) + mod.private_instance_methods(true)
|
71
|
-
|
72
|
-
(private_ids - @unreq).uniq.each do |id|
|
73
|
-
id = id.to_s if RUBY_VERSION.to_f < 1.9
|
74
|
-
unless mod_all_methods.include?(id)
|
75
|
-
raise Interface::PrivateVisibleMethodMissing, "#{mod}: #{self}##{id}"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
87
|
+
|
79
88
|
super mod
|
80
89
|
end
|
81
|
-
|
90
|
+
|
91
|
+
# Stash a method in the module for future checks
|
92
|
+
def stash_method(mod, id)
|
93
|
+
unless mod.instance_variable_defined?('@__interface_stashed_methods')
|
94
|
+
mod.instance_variable_set('@__interface_stashed_methods', {})
|
95
|
+
end
|
96
|
+
mod.instance_variable_get('@__interface_stashed_methods')[id] = mod.instance_method(id)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get a stashed method for the module
|
100
|
+
def stashed_method(mod, id)
|
101
|
+
return nil unless mod.instance_variable_defined?('@__interface_stashed_methods')
|
102
|
+
mod.instance_variable_get('@__interface_stashed_methods')[id]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Check arity
|
106
|
+
# Replace with type_checking method if demanded
|
82
107
|
def replace_check_method(mod, id, inchecks, outcheck)
|
83
|
-
|
84
|
-
|
108
|
+
# Get the previously stashed method if it exists
|
109
|
+
stashed_meth = stashed_method(mod, id)
|
110
|
+
orig_method = stashed_meth || mod.instance_method(id)
|
85
111
|
unless mod.instance_variable_defined?("@__interface_arity_skip") \
|
86
112
|
&& mod.instance_variable_get("@__interface_arity_skip")
|
87
113
|
orig_arity = orig_method.parameters.size
|
88
114
|
check_arity = inchecks.size
|
89
115
|
if orig_arity != check_arity
|
90
116
|
raise Interface::MethodArityError,
|
91
|
-
|
117
|
+
"#{mod}: #{self}##{id} arity mismatch: #{orig_arity} instead of #{check_arity}"
|
92
118
|
end
|
93
119
|
end
|
94
|
-
|
120
|
+
|
121
|
+
# return if we are not doing runtime checks for this class
|
95
122
|
unless ENV['RUBY_CUBE_TYPECHECK'].to_i > 0 \
|
96
123
|
&& mod.instance_variable_defined?("@__interface_runtime_check") \
|
97
124
|
&& mod.instance_variable_get("@__interface_runtime_check")
|
98
125
|
return
|
99
126
|
end
|
127
|
+
# if the stashed method exists, it already exists. return
|
128
|
+
return if stashed_meth
|
100
129
|
iface = self
|
130
|
+
stash_method(mod, id)
|
131
|
+
# replace method with a type checking wrapper
|
101
132
|
mod.class_exec do
|
133
|
+
# random alias name to avoid conflicts
|
102
134
|
ns_meth_name = "#{id}_#{SecureRandom.hex(3)}".to_sym
|
103
135
|
alias_method ns_meth_name, id
|
136
|
+
# The type checking wrapper
|
104
137
|
define_method(id) do |*args|
|
105
138
|
args.each_index do |i|
|
139
|
+
# the value and expected type of the arg
|
106
140
|
v, t = args[i], inchecks[i]
|
107
141
|
begin
|
108
|
-
check_type(t, v)
|
142
|
+
Cube.check_type(t, v)
|
109
143
|
rescue Interface::TypeMismatchError => e
|
110
144
|
raise Interface::TypeMismatchError,
|
111
|
-
|
145
|
+
"#{mod}: #{iface}##{id} (arg: #{i}): #{e.message}"
|
112
146
|
end
|
113
147
|
end
|
148
|
+
# types look good, call the original method
|
114
149
|
ret = send(ns_meth_name, *args)
|
150
|
+
# check return type if it exists
|
115
151
|
begin
|
116
|
-
check_type(outcheck, ret) if outcheck
|
152
|
+
Cube.check_type(outcheck, ret) if outcheck
|
117
153
|
rescue Interface::TypeMismatchError => e
|
118
154
|
raise Interface::TypeMismatchError,
|
119
155
|
"#{mod}: #{iface}##{id} (return): #{e.message}"
|
120
156
|
end
|
157
|
+
# looks good, return
|
121
158
|
ret
|
122
159
|
end
|
123
160
|
end
|
124
161
|
end
|
125
|
-
|
126
|
-
#
|
127
|
-
# arity = mod.instance_method(meth).arity
|
128
|
-
# unless arity == @ids[meth]
|
129
|
-
# raise Interface::MethodArityError, "#{mod}: #{self}##{meth}=#{arity}. Should be #{@ids[meth]}"
|
130
|
-
# end
|
131
|
-
# end
|
132
|
-
|
162
|
+
|
163
|
+
# massage array spec and hash spec into hash spec
|
133
164
|
def map_spec(ids)
|
134
165
|
ids.reduce({}) do |res, m|
|
135
166
|
if m.is_a?(Hash)
|
@@ -139,7 +170,8 @@ module Cube
|
|
139
170
|
end
|
140
171
|
end
|
141
172
|
end
|
142
|
-
|
173
|
+
|
174
|
+
# validate the interface spec is valid
|
143
175
|
def validate_spec(spec)
|
144
176
|
[*spec].each do |t|
|
145
177
|
if t.is_a?(Array)
|
@@ -151,8 +183,9 @@ module Cube
|
|
151
183
|
end
|
152
184
|
end
|
153
185
|
end
|
154
|
-
|
186
|
+
|
155
187
|
public
|
188
|
+
|
156
189
|
# Accepts an array of method names that define the interface. When this
|
157
190
|
# module is included/implemented, those method names must have already been
|
158
191
|
# defined.
|
@@ -160,14 +193,30 @@ module Cube
|
|
160
193
|
def required_public_methods
|
161
194
|
@ids.keys
|
162
195
|
end
|
163
|
-
|
196
|
+
|
197
|
+
def to_spec
|
198
|
+
inherited = (self.ancestors-[self]).select{ |x| Interface === x }
|
199
|
+
inherited_ids = inherited.map{ |x| x.instance_variable_get('@ids') }
|
200
|
+
|
201
|
+
# Store required method ids
|
202
|
+
inherited_specs = map_spec(inherited_ids.flatten)
|
203
|
+
@ids.merge(inherited_specs)
|
204
|
+
end
|
205
|
+
|
206
|
+
def assert_match(intf)
|
207
|
+
raise ArgumentError, "#{intf} is not a Cube::Interface" unless intf.is_a?(Interface)
|
208
|
+
intf_specs = intf.to_spec
|
209
|
+
self_specs = to_spec
|
210
|
+
Interface.match_specs(self_specs, intf_specs)
|
211
|
+
end
|
212
|
+
|
164
213
|
def proto(meth, *args)
|
165
214
|
out_spec = yield if block_given?
|
166
215
|
validate_spec(args)
|
167
216
|
validate_spec(out_spec) if out_spec
|
168
|
-
@ids.merge!({ meth.to_sym => { in: args, out: out_spec }})
|
217
|
+
@ids.merge!({ meth.to_sym => { in: args, out: out_spec } })
|
169
218
|
end
|
170
|
-
|
219
|
+
|
171
220
|
def public_visible(*ids)
|
172
221
|
unless ids.all? { |id| id.is_a?(Symbol) || id.is_a?(String) }
|
173
222
|
raise ArgumentError, "Arguments should be strings or symbols"
|
@@ -175,23 +224,21 @@ module Cube
|
|
175
224
|
spec = map_spec(ids)
|
176
225
|
@ids.merge!(spec)
|
177
226
|
end
|
178
|
-
|
179
|
-
def
|
180
|
-
|
181
|
-
|
227
|
+
|
228
|
+
def impotent
|
229
|
+
cl = Cube.interface {}.extend(self)
|
230
|
+
cl.module_exec do
|
231
|
+
def extend_object(mod)
|
232
|
+
super
|
233
|
+
end
|
234
|
+
def append_features(mod)
|
235
|
+
super
|
236
|
+
end
|
182
237
|
end
|
183
|
-
|
184
|
-
@private_ids.merge!(spec)
|
238
|
+
cl
|
185
239
|
end
|
186
|
-
|
187
|
-
#
|
188
|
-
# you only wanted a partial implementation of an existing interface.
|
189
|
-
#
|
190
|
-
def unrequired_methods(*ids)
|
191
|
-
@unreq ||= []
|
192
|
-
@unreq += ids
|
193
|
-
end
|
194
|
-
|
240
|
+
|
241
|
+
# creates a shell object for testing
|
195
242
|
def shell
|
196
243
|
ids = @ids
|
197
244
|
unreq = @unreq
|
@@ -200,67 +247,9 @@ module Cube
|
|
200
247
|
define_method(m) { |*args| }
|
201
248
|
end
|
202
249
|
end
|
203
|
-
cls.send(:shell_implements, self)
|
250
|
+
Cube[cls].send(:shell_implements, self)
|
204
251
|
end
|
205
252
|
end
|
206
253
|
end
|
207
254
|
|
208
|
-
class Object
|
209
|
-
def interface(&block)
|
210
|
-
mod = Module.new
|
211
|
-
mod.extend(Cube::Interface)
|
212
|
-
mod.instance_variable_set('@ids', {})
|
213
|
-
mod.instance_variable_set('@private_ids', {})
|
214
|
-
mod.instance_eval(&block)
|
215
|
-
mod
|
216
|
-
end
|
217
|
-
|
218
|
-
if ENV['RUBY_CUBE_TYPECHECK'].to_i > 0
|
219
|
-
def check_type(t, v)
|
220
|
-
if t.is_a?(Set)
|
221
|
-
unless t.any? { |tp| check_type(tp, v) rescue false }
|
222
|
-
raise Cube::Interface::TypeMismatchError,
|
223
|
-
"#{v.inspect} is not any of #{tp.to_a}" unless v.is_a?(tp)
|
224
|
-
end
|
225
|
-
return
|
226
|
-
end
|
227
|
-
if t.is_a? Array
|
228
|
-
raise Cube::Interface::TypeMismatchError,
|
229
|
-
"#{v} is not an Array" unless v.is_a? Array
|
230
|
-
check_type(t.first, v.first)
|
231
|
-
check_type(t.first, v.last)
|
232
|
-
return
|
233
|
-
end
|
234
|
-
raise Cube::Interface::TypeMismatchError, "#{v.inspect} is not type #{t}" unless v.is_a? t
|
235
|
-
true
|
236
|
-
end
|
237
|
-
else
|
238
|
-
def check_type(*_); end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
class Module
|
243
|
-
def implements(mod, runtime_checks: true)
|
244
|
-
unless is_a? Class
|
245
|
-
raise "Non-Class modules should not implement interfaces"
|
246
|
-
end
|
247
|
-
instance_variable_set(:@__interface_runtime_check, true) if runtime_checks
|
248
|
-
include(mod)
|
249
|
-
end
|
250
|
-
|
251
|
-
def as_interface(iface, runtime_checks: true)
|
252
|
-
clone.implements(iface, runtime_checks: runtime_checks)
|
253
|
-
end
|
254
|
-
|
255
|
-
def assert_implements(iface)
|
256
|
-
clone.implements(iface, false)
|
257
|
-
end
|
258
|
-
|
259
|
-
def shell_implements(mod)
|
260
|
-
instance_variable_set(:@__interface_runtime_check, false)
|
261
|
-
instance_variable_set(:@__interface_arity_skip, true)
|
262
|
-
include(mod)
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
255
|
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Cube
|
2
|
+
def self.mark_interface!(cls, iface)
|
3
|
+
Cube[cls].as_interface(iface, runtime_checks: false)
|
4
|
+
cl_iface = iface.impotent
|
5
|
+
cls.include(cl_iface)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.[](mod)
|
9
|
+
return mod if mod.is_a?(CubeMethods)
|
10
|
+
unless mod.is_a?(Class)
|
11
|
+
raise ArgumentError, "Only classes can be be converted to Cube classes"
|
12
|
+
end
|
13
|
+
Class.new(mod).extend(CubeMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
alias_method :from, :[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.interface(&block)
|
21
|
+
mod = Module.new
|
22
|
+
mod.extend(Cube::Interface)
|
23
|
+
mod.instance_variable_set('@ids', {})
|
24
|
+
mod.instance_eval(&block)
|
25
|
+
mod
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.trait(&blk)
|
29
|
+
m = Module.new
|
30
|
+
m.extend(Cube::Trait)
|
31
|
+
m.module_exec(&blk) if block_given?
|
32
|
+
m
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.check_type_spec(t, v, &blk)
|
36
|
+
if t.is_a?(Set)
|
37
|
+
if v.is_a?(Set)
|
38
|
+
if v != t
|
39
|
+
raise Cube::Interface::TypeMismatchError, "#{t.to_a} is not eql to #{v.to_a}"
|
40
|
+
end
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
unless t.any? { |tp| check_type(tp, v, &blk) rescue false }
|
44
|
+
raise Cube::Interface::TypeMismatchError,
|
45
|
+
"#{v.inspect} is not any of #{t.to_a}"
|
46
|
+
end
|
47
|
+
return
|
48
|
+
end
|
49
|
+
if t.is_a? Array
|
50
|
+
raise Cube::Interface::TypeMismatchError,
|
51
|
+
"#{v} is not an Array" unless v.is_a? Array
|
52
|
+
check_type(t.first, v.first, &blk)
|
53
|
+
check_type(t.first, v.last, &blk)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
raise Cube::Interface::TypeMismatchError, "#{v.inspect} is not type #{t}" unless blk.call(t, v)
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
if ENV['RUBY_CUBE_TYPECHECK'].to_i > 0
|
61
|
+
def self.check_type(t, v)
|
62
|
+
if t.is_a?(Set)
|
63
|
+
unless t.any? { |tp| check_type(tp, v) rescue false }
|
64
|
+
raise Cube::Interface::TypeMismatchError,
|
65
|
+
"#{v.inspect} is not any of #{t.to_a}"
|
66
|
+
end
|
67
|
+
return
|
68
|
+
end
|
69
|
+
if t.is_a? Array
|
70
|
+
raise Cube::Interface::TypeMismatchError,
|
71
|
+
"#{v} is not an Array" unless v.is_a? Array
|
72
|
+
check_type(t.first, v.first)
|
73
|
+
check_type(t.first, v.last)
|
74
|
+
return
|
75
|
+
end
|
76
|
+
raise Cube::Interface::TypeMismatchError, "#{v.inspect} is not type #{t}" unless v.is_a? t
|
77
|
+
true
|
78
|
+
end
|
79
|
+
else
|
80
|
+
def self.check_type(*_); end
|
81
|
+
end
|
82
|
+
|
83
|
+
module CubeMethods
|
84
|
+
def as_interface(iface, runtime_checks: true)
|
85
|
+
raise ArgumentError, "#{iface} is not a Cube::Interface" unless iface.is_a?(Cube::Interface)
|
86
|
+
implements = lambda { |this|
|
87
|
+
unless this.is_a? Class
|
88
|
+
raise "Non-Class modules cannot implement interfaces"
|
89
|
+
end
|
90
|
+
this.instance_variable_set(:@__interface_runtime_check, true) if runtime_checks
|
91
|
+
this.include(iface)
|
92
|
+
}
|
93
|
+
implements.call(clone)
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def shell_implements(mod)
|
98
|
+
instance_variable_set(:@__interface_runtime_check, false)
|
99
|
+
instance_variable_set(:@__interface_arity_skip, true)
|
100
|
+
include(mod)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
data/lib/cube/traits.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'interfaces'
|
2
|
+
require 'delegate'
|
2
3
|
|
3
4
|
module Cube
|
4
5
|
module Trait
|
@@ -13,6 +14,9 @@ module Cube
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def append_features(mod)
|
17
|
+
if mod.is_a?(Class) && !mod.is_a?(CubeMethods)
|
18
|
+
raise IncludeError, "Traits can only be mixed into cube classes"
|
19
|
+
end
|
16
20
|
unless mod.instance_variable_defined?(:@__trait_allow_include) &&
|
17
21
|
mod.instance_variable_get(:@__trait_allow_include)
|
18
22
|
raise IncludeError, "Traits can only be mixed in using method `with_trait`"
|
@@ -26,27 +30,59 @@ module Cube
|
|
26
30
|
message = "\n" + errors.map { |e| e[:meth].to_s }.join("\n")
|
27
31
|
raise MethodConflict, message
|
28
32
|
end
|
29
|
-
if @
|
33
|
+
if @__interface_trait_required_interface && mod.is_a?(Class)
|
30
34
|
intf = @__interface_trait_required_interface
|
31
|
-
mod.include?(intf) || mod.
|
35
|
+
mod.include?(intf) || mod.as_interface(intf, runtime_checks: false)
|
36
|
+
end
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def wrap(intf)
|
41
|
+
assert_match(intf)
|
42
|
+
cls = Class.new(SimpleDelegator) do
|
43
|
+
define_method(:initialize) do |obj|
|
44
|
+
$stderr.puts "Checking with #{intf}"
|
45
|
+
Cube.check_type(intf, obj)
|
46
|
+
super(obj)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
inc_trait = clone
|
50
|
+
inc_trait.instance_variable_set(:@__interface_trait_required_interface, nil)
|
51
|
+
inc_trait.instance_variable_set(:@__trait_cloned_from, self)
|
52
|
+
Cube[cls].with_trait(inc_trait)
|
53
|
+
end
|
54
|
+
|
55
|
+
def assert_match(intf)
|
56
|
+
self_methods = instance_methods
|
57
|
+
inherited = self.ancestors.select{ |x| Trait === x }
|
58
|
+
required_interface_spec = inherited.inject({}) { |acc, x|
|
59
|
+
req = x.instance_variable_get('@__interface_trait_required_interface')
|
60
|
+
if req
|
61
|
+
acc.merge(req.to_spec)
|
62
|
+
else
|
63
|
+
acc
|
64
|
+
end
|
65
|
+
}
|
66
|
+
self_methods.each do |sm|
|
67
|
+
required_interface_spec.delete(sm)
|
32
68
|
end
|
33
|
-
|
69
|
+
Interface.match_specs(required_interface_spec, intf.to_spec)
|
34
70
|
end
|
35
71
|
end
|
36
72
|
end
|
37
73
|
|
38
74
|
class Module
|
39
|
-
def with_trait(trait,
|
75
|
+
def with_trait(trait, rename: {}, suppress: [])
|
40
76
|
unless trait.is_a? Cube::Trait
|
41
77
|
raise ArgumentError, "#{trait} is not an Cube::Trait"
|
42
78
|
end
|
43
79
|
cls = clone
|
44
80
|
cls.instance_variable_set(:@__trait_allow_include, true)
|
45
81
|
cls.instance_variable_set(:@__trait_cloned_from, self)
|
46
|
-
raise ArgumentError, "aliases must be a Hash" unless
|
47
|
-
raise ArgumentError, "supresses must be a Array" unless
|
82
|
+
raise ArgumentError, "aliases must be a Hash" unless rename.is_a?(Hash)
|
83
|
+
raise ArgumentError, "supresses must be a Array" unless suppress.is_a?(Array)
|
48
84
|
|
49
|
-
al_trait = trait_with_resolutions(trait,
|
85
|
+
al_trait = trait_with_resolutions(trait, rename, suppress)
|
50
86
|
al_trait.instance_variable_set(:@__interface_runtime_check, false)
|
51
87
|
cls.include(al_trait)
|
52
88
|
cls
|
@@ -58,7 +94,7 @@ class Module
|
|
58
94
|
cl = trait.clone
|
59
95
|
cl.module_exec do
|
60
96
|
suppress.each do |sup|
|
61
|
-
|
97
|
+
remove_method(sup)
|
62
98
|
end
|
63
99
|
aliases.each do |before, after|
|
64
100
|
begin
|
@@ -67,7 +103,7 @@ class Module
|
|
67
103
|
$stderr.puts "with_trait(#{trait}): #{e.message}"
|
68
104
|
raise ArgumentError, "with_trait(#{trait}): #{e.message}"
|
69
105
|
end
|
70
|
-
|
106
|
+
remove_method(before)
|
71
107
|
end
|
72
108
|
end
|
73
109
|
cl
|
data/rubycube-0.2.1.gem
ADDED
Binary file
|
data/test/test_interface.rb
CHANGED
@@ -10,13 +10,13 @@ require 'cube'
|
|
10
10
|
|
11
11
|
class TC_Interface < Test::Unit::TestCase
|
12
12
|
def self.startup
|
13
|
-
alpha_interface = interface{
|
13
|
+
alpha_interface = Cube.interface{
|
14
14
|
public_visible(:alpha, :beta)
|
15
15
|
proto(:beta) { [Integer, NilClass].to_set }
|
16
16
|
proto(:delta, Integer, String, Integer) { Integer }
|
17
17
|
}
|
18
18
|
|
19
|
-
gamma_interface = interface{
|
19
|
+
gamma_interface = Cube.interface{
|
20
20
|
extends alpha_interface
|
21
21
|
public_visible :gamma
|
22
22
|
}
|
@@ -47,11 +47,7 @@ class TC_Interface < Test::Unit::TestCase
|
|
47
47
|
|
48
48
|
|
49
49
|
def checker_method(arg)
|
50
|
-
check_type(@@gamma_interface, arg)
|
51
|
-
end
|
52
|
-
|
53
|
-
def test_version
|
54
|
-
assert_equal('0.2.0', Cube::Interface::VERSION)
|
50
|
+
Cube.check_type(@@gamma_interface, arg)
|
55
51
|
end
|
56
52
|
|
57
53
|
def test_interface_requirements_not_met
|
@@ -70,34 +66,44 @@ class TC_Interface < Test::Unit::TestCase
|
|
70
66
|
|
71
67
|
def test_gamma_interface_requirements_met
|
72
68
|
assert_raise(Cube::Interface::MethodArityError) { C.new.extend(@@gamma_interface) }
|
73
|
-
assert_raise(Cube::Interface::MethodArityError) { C.as_interface(@@gamma_interface) }
|
69
|
+
assert_raise(Cube::Interface::MethodArityError) { Cube[C].as_interface(@@gamma_interface) }
|
74
70
|
end
|
75
71
|
|
76
72
|
def test_method_check
|
77
|
-
assert_raise(Cube::Interface::TypeMismatchError) { checker_method(B.as_interface(@@alpha_interface).new) }
|
73
|
+
assert_raise(Cube::Interface::TypeMismatchError) { checker_method(Cube[B].as_interface(@@alpha_interface).new) }
|
78
74
|
end
|
79
75
|
|
80
76
|
def test_runtime_error_check
|
81
77
|
assert_nothing_raised {
|
82
|
-
B
|
78
|
+
Cube[Cube[B]]
|
79
|
+
}
|
80
|
+
assert(Cube[B].is_a?(Cube::CubeMethods))
|
81
|
+
assert_raise(ArgumentError) {
|
82
|
+
Cube[@@gamma_interface]
|
83
|
+
}
|
84
|
+
assert_nothing_raised {
|
85
|
+
Cube[B].as_interface(@@alpha_interface).as_interface(@@alpha_interface)
|
86
|
+
}
|
87
|
+
assert_nothing_raised {
|
88
|
+
Cube[B].as_interface(@@alpha_interface, runtime_checks: true).new.beta
|
83
89
|
}
|
84
90
|
assert_raise(ArgumentError) {
|
85
|
-
B.as_interface(@@alpha_interface).new.delta
|
91
|
+
Cube[B].as_interface(@@alpha_interface).new.delta
|
86
92
|
}
|
87
93
|
assert_raise(ArgumentError) {
|
88
|
-
B.as_interface(@@alpha_interface).new.delta(1)
|
94
|
+
Cube[B].as_interface(@@alpha_interface).new.delta(1)
|
89
95
|
}
|
90
96
|
assert_raise(Cube::Interface::TypeMismatchError) {
|
91
|
-
B.as_interface(@@alpha_interface).new.delta(1, 2)
|
97
|
+
Cube[B].as_interface(@@alpha_interface).new.delta(1, 2)
|
92
98
|
}
|
93
99
|
assert_raise(Cube::Interface::TypeMismatchError) {
|
94
|
-
B.as_interface(@@alpha_interface).new.delta(1, "2", "3")
|
100
|
+
Cube[B].as_interface(@@alpha_interface).new.delta(1, "2", "3")
|
95
101
|
}
|
96
102
|
assert_nothing_raised {
|
97
|
-
B.as_interface(@@alpha_interface).new.delta(1, "2")
|
103
|
+
Cube[B].as_interface(@@alpha_interface).new.delta(1, "2")
|
98
104
|
}
|
99
105
|
assert_nothing_raised {
|
100
|
-
B.as_interface(@@alpha_interface).new.delta(1, "2", 3)
|
106
|
+
Cube[B].as_interface(@@alpha_interface).new.delta(1, "2", 3)
|
101
107
|
}
|
102
108
|
end
|
103
109
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubycube
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aditya Godbole
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain:
|
11
11
|
- certs/djberg96_pub.pem
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-08-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
@@ -120,9 +120,12 @@ files:
|
|
120
120
|
- doc/js/searcher.js.gz
|
121
121
|
- doc/table_of_contents.html
|
122
122
|
- examples/demo.rb
|
123
|
+
- examples/traits.rb
|
123
124
|
- lib/cube.rb
|
124
125
|
- lib/cube/interfaces.rb
|
126
|
+
- lib/cube/toplevel.rb
|
125
127
|
- lib/cube/traits.rb
|
128
|
+
- rubycube-0.2.1.gem
|
126
129
|
- test/test_interface.rb
|
127
130
|
homepage: http://github.com/adityagodbole/rubycube
|
128
131
|
licenses:
|