rubycube 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|