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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 103592cc3433918459db3c3aaa11f5da53bfa839
4
- data.tar.gz: 9d40125b33e7f53ac0754b969ac715825d5e0b5e
3
+ metadata.gz: c136c0dc0b938dbd1ffe392203ae39781e7ebaf1
4
+ data.tar.gz: 355531cf507cee9bd3c68a69e7ebe59255ef6773
5
5
  SHA512:
6
- metadata.gz: b63f85ed3b18c48159ed5b926f7396a0900c3f704b0e1e8293d993ea5b95f26b8cecbce20b3df2160673701c5d198a77daa6d60cd4c3e6cd85b2bb41f4104813
7
- data.tar.gz: a2452695f47eb3c3c1b231fe5f9f9c0526a1983518b0fa87ad90637274ac164127a800a2226a6fa1b2f64faed066b61cbabfb3dc082676f64c9ca089771ca3ac
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
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'rubycube'
5
- spec.version = '0.2.1'
5
+ spec.version = '0.3.0'
6
6
  spec.author = 'Aditya Godbole'
7
7
  spec.license = 'Artistic 2.0'
8
8
  spec.email = 'code.aa@gdbl.me'
@@ -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 SimpleCalc
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
- c = SimpleCalc.as_interface(Calculator).new
35
- # If SimpleCalc does not have `implements Calculator`, but its methods match the interface
36
- # you can "cast" it to Calculator - `SimpleCalc.as_interface(Calculator).new`
37
- # This is useful for casting classes that you did not write
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
- module AdvancedCalcT
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
- def sum; end # A class method always takes precedence, no conflict here
55
- def foo; end # this conflicts with DummyCalcT. Needs to be aliased (see below)
56
- def bar; end # this conflicts with DummyCalcT. Needs to be aliased (see below)
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
- module DummyCalcT
63
- extend Cube::Trait
64
- def sum; end # this method conflicts with AdvancedCalcT, but SimpleCalc#sum takes precedence
65
- def foo; end
66
- def bar; end
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(AdvancedCalcT,
80
- aliases: { foo: :adfoo, bar: :adbar })
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
 
@@ -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
@@ -1,2 +1,3 @@
1
1
  require_relative 'cube/interfaces'
2
2
  require_relative 'cube/traits'
3
+ require_relative 'cube/toplevel'
@@ -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
- # Raised if a class or instance does not meet the interface requirements.
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
- inherited_specs = map_spec(inherited_ids.flatten)
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
- orig_method = mod.instance_method(id)
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
- "#{mod}: #{self}##{id} arity mismatch: #{orig_arity} instead of #{check_arity}"
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
- "#{mod}: #{iface}##{id} (arg: #{i}): #{e.message}"
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
- # def verify_arity(mod, meth)
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 private_visible(*ids)
180
- unless ids.all? { |id| id.is_a?(Symbol) || id.is_a?(String) }
181
- raise ArgumentError, "Arguments should be strings or symbols"
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
- spec = map_spec(ids)
184
- @private_ids.merge!(spec)
238
+ cl
185
239
  end
186
- # Accepts an array of method names that are removed as a requirement for
187
- # implementation. Presumably you would use this in a sub-interface where
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
+
@@ -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 @__interface__trait_required_interface
33
+ if @__interface_trait_required_interface && mod.is_a?(Class)
30
34
  intf = @__interface_trait_required_interface
31
- mod.include?(intf) || mod.assert_implements(intf)
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
- super(mod)
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, aliases: {}, suppresses: [])
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 aliases.is_a?(Hash)
47
- raise ArgumentError, "supresses must be a Array" unless suppresses.is_a?(Array)
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, aliases, suppresses)
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
- undef_method(sup)
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
- undef_method(before)
106
+ remove_method(before)
71
107
  end
72
108
  end
73
109
  cl
Binary file
@@ -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.as_interface(@@alpha_interface, runtime_checks: true).new.beta
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.2.1
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-07-12 00:00:00.000000000 Z
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: