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 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: