oinky 0.1.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.
- data/LICENSE +22 -0
- data/README.md +141 -0
- data/ext/extconf.rb +79 -0
- data/ext/include/oinky.h +424 -0
- data/ext/include/oinky.hpp +63 -0
- data/ext/include/oinky/nky_base.hpp +1116 -0
- data/ext/include/oinky/nky_core.hpp +1603 -0
- data/ext/include/oinky/nky_cursor.hpp +665 -0
- data/ext/include/oinky/nky_dialect.hpp +107 -0
- data/ext/include/oinky/nky_error.hpp +164 -0
- data/ext/include/oinky/nky_fixed_table.hpp +710 -0
- data/ext/include/oinky/nky_handle.hpp +334 -0
- data/ext/include/oinky/nky_index.hpp +1038 -0
- data/ext/include/oinky/nky_log.hpp +15 -0
- data/ext/include/oinky/nky_merge_itr.hpp +403 -0
- data/ext/include/oinky/nky_model.hpp +110 -0
- data/ext/include/oinky/nky_pool.hpp +760 -0
- data/ext/include/oinky/nky_public.hpp +808 -0
- data/ext/include/oinky/nky_serializer.hpp +1625 -0
- data/ext/include/oinky/nky_strtable.hpp +504 -0
- data/ext/include/oinky/nky_table.hpp +1996 -0
- data/ext/nky_lib.cpp +390 -0
- data/ext/nky_lib_core.hpp +212 -0
- data/ext/nky_lib_index.cpp +158 -0
- data/ext/nky_lib_table.cpp +224 -0
- data/lib/oinky.rb +1284 -0
- data/lib/oinky/compiler.rb +106 -0
- data/lib/oinky/cpp_emitter.rb +311 -0
- data/lib/oinky/dsl.rb +167 -0
- data/lib/oinky/error.rb +19 -0
- data/lib/oinky/modelbase.rb +12 -0
- data/lib/oinky/nbuffer.rb +152 -0
- data/lib/oinky/normalize.rb +132 -0
- data/lib/oinky/oc_builder.rb +44 -0
- data/lib/oinky/query.rb +193 -0
- data/lib/oinky/rb_emitter.rb +147 -0
- data/lib/oinky/shard.rb +40 -0
- data/lib/oinky/testsup.rb +104 -0
- data/lib/oinky/version.rb +9 -0
- data/oinky.gemspec +36 -0
- metadata +120 -0
data/lib/oinky/error.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# This source is distributed under the terms of the MIT License. Refer
|
2
|
+
# to the 'LICENSE' file for details.
|
3
|
+
#
|
4
|
+
# Copyright (c) Jacob Lacouture, 2012
|
5
|
+
|
6
|
+
module Oinky
|
7
|
+
class OinkyException < StandardError
|
8
|
+
attr_reader :code
|
9
|
+
def initialize(*a)
|
10
|
+
if a.size > 1
|
11
|
+
super(*a[1..-1])
|
12
|
+
@code = a[0]
|
13
|
+
else
|
14
|
+
super(*a)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# This source is distributed under the terms of the MIT License. Refer
|
2
|
+
# to the 'LICENSE' file for details.
|
3
|
+
#
|
4
|
+
# Copyright (c) Jacob Lacouture, 2012
|
5
|
+
|
6
|
+
require 'ffi'
|
7
|
+
|
8
|
+
class NativeMallocBuffer
|
9
|
+
end
|
10
|
+
|
11
|
+
# This is intended to be used by multiple libraries to share large string
|
12
|
+
# objects, without having to copy them on/off the ruby heap, via
|
13
|
+
# ruby String.
|
14
|
+
#
|
15
|
+
# Some ruby implementations (JRuby) move GC memory around, rather than just
|
16
|
+
# refcounting it, as MRI does. To keep the string from moving, we need to
|
17
|
+
# allocate it on the native heap. However, we still want to manage it, as
|
18
|
+
# we want to be abel to share it between components, none of which may be
|
19
|
+
# capable of managing it independently.
|
20
|
+
#
|
21
|
+
# So, we use the ruby GC to manage an object, which explicitly manages the
|
22
|
+
# lifetime of the native object. The native object can be passed between
|
23
|
+
# components by reference, rather than always by value.
|
24
|
+
module NativeBuffer
|
25
|
+
include Comparable
|
26
|
+
|
27
|
+
module LibC
|
28
|
+
extend FFI::Library
|
29
|
+
ffi_lib FFI::Library::LIBC
|
30
|
+
|
31
|
+
attach_function :malloc, [:size_t], :pointer
|
32
|
+
attach_function :free, [:pointer], :void
|
33
|
+
attach_function :memcpy, [:pointer, :pointer, :size_t], :void
|
34
|
+
attach_function :memcmp, [:pointer, :pointer, :size_t], :int
|
35
|
+
end
|
36
|
+
|
37
|
+
# supports the following methods:
|
38
|
+
|
39
|
+
# String length in bytes (not chars)
|
40
|
+
# length/size
|
41
|
+
|
42
|
+
# Returns an FFI pointer
|
43
|
+
# def ptr
|
44
|
+
|
45
|
+
# converts to a ruby string.
|
46
|
+
# def to_s
|
47
|
+
|
48
|
+
# Implementations of this module should supply these two definitions.
|
49
|
+
|
50
|
+
# def ptr
|
51
|
+
# @ptr
|
52
|
+
# end
|
53
|
+
# def length
|
54
|
+
# @size
|
55
|
+
# end
|
56
|
+
|
57
|
+
def clone
|
58
|
+
NativeMallocBuffer.new(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convert native string to a ruby string
|
62
|
+
def rb_str
|
63
|
+
l = self.length
|
64
|
+
return '' unless l > 0
|
65
|
+
self.ptr.read_string(l)
|
66
|
+
end
|
67
|
+
|
68
|
+
alias :to_s :rb_str
|
69
|
+
|
70
|
+
def inspect
|
71
|
+
"#<#{self.class}:0x#{self.__id__.to_s(16)} length=#{self.length} ptr=#{self.ptr.inspect}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def each_byte
|
75
|
+
e = Enumerator.new { |blk|
|
76
|
+
(0..self.length-1).each { |i|
|
77
|
+
blk.yield (self.ptr + i).read_uchar
|
78
|
+
}
|
79
|
+
}
|
80
|
+
if block_given?
|
81
|
+
e.each {|v| yield v}
|
82
|
+
return self
|
83
|
+
else
|
84
|
+
return e
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def each_char
|
89
|
+
if block_given?
|
90
|
+
each_byte { |b| yield(b.chr) }
|
91
|
+
return self
|
92
|
+
else
|
93
|
+
return Enumerator.new { |blk|
|
94
|
+
each_byte { |b| blk.yield(b.chr) }
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def <=>(other)
|
100
|
+
if other.is_a? NativeBuffer
|
101
|
+
ll = self.length
|
102
|
+
ol = other.length
|
103
|
+
k = ll - ol
|
104
|
+
cl = k < 0 ? ll : ol
|
105
|
+
r = LibC.memcmp(self.ptr, other.ptr, cl)
|
106
|
+
return r if r != 0
|
107
|
+
return k
|
108
|
+
else
|
109
|
+
return self.to_s <=> other
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# This is a specific implementation of NativeBuffer. It uses malloc/free.
|
115
|
+
class NativeMallocBuffer
|
116
|
+
include NativeBuffer
|
117
|
+
|
118
|
+
attr_reader :ptr, :length
|
119
|
+
|
120
|
+
def initialize(v = 0)
|
121
|
+
if (v.is_a? Fixnum)
|
122
|
+
@ptr = FFI::MemoryPointer.new :pointer
|
123
|
+
size = v
|
124
|
+
if size > 0
|
125
|
+
@ptr = LibC.malloc(size)
|
126
|
+
@length = size
|
127
|
+
else
|
128
|
+
@ptr = nil
|
129
|
+
@length = 0
|
130
|
+
end
|
131
|
+
elsif v.is_a? String
|
132
|
+
size = v.length
|
133
|
+
@ptr = LibC.malloc(size)
|
134
|
+
LibC.memcpy(@ptr, v, size)
|
135
|
+
@length = size
|
136
|
+
elsif v.is_a? NativeBuffer
|
137
|
+
size = v.length
|
138
|
+
@ptr = LibC.malloc(size)
|
139
|
+
LibC.memcpy(@ptr, v.ptr, size)
|
140
|
+
@length = size
|
141
|
+
else
|
142
|
+
raise OinkyException.new("Invalid initialize parameter to NativeMallocBuffer.new")
|
143
|
+
end
|
144
|
+
|
145
|
+
ObjectSpace.define_finalizer( self, self.class.finalize(@ptr) )
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.finalize(ptr)
|
149
|
+
proc { LibC.free(ptr) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Oinky
|
4
|
+
module Model
|
5
|
+
|
6
|
+
DefaultsByType = {
|
7
|
+
:string=>'',
|
8
|
+
:bit=>false,
|
9
|
+
:int8=>0,
|
10
|
+
:int16=>0,
|
11
|
+
:int32=>0,
|
12
|
+
:int64=>0,
|
13
|
+
:uint8=>0,
|
14
|
+
:uint16=>0,
|
15
|
+
:uint32=>0,
|
16
|
+
:uint64=>0,
|
17
|
+
:float32=>0.0,
|
18
|
+
:float64=>0.0,
|
19
|
+
:variant=>0,
|
20
|
+
:datetime=>DateTime.parse("0001-01-01T00:00:00 UTC")
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.validate_keys(h, keys)
|
24
|
+
h.keys.each{|k,v|
|
25
|
+
unless keys.find_index(k)
|
26
|
+
raise ArgumentError.new("Unrecognized key in Oinky schema " +
|
27
|
+
"definition: #{k}")
|
28
|
+
end
|
29
|
+
}
|
30
|
+
end
|
31
|
+
def self.validate_type(t)
|
32
|
+
unless DefaultsByType.has_key?(t)
|
33
|
+
raise ArgumentError.new("Unrecognized type in Oinky schema definition: #{k}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.normalize_column_def(nm, v)
|
38
|
+
if v.is_a? Symbol
|
39
|
+
validate_type(v)
|
40
|
+
return {:type=>v, :default=>DefaultsByType[v], :accessor=>nm}
|
41
|
+
end
|
42
|
+
# Otherwise v is a hash
|
43
|
+
validate_keys(v,[:type,:default,:accessor])
|
44
|
+
t = v[:type]
|
45
|
+
raise ArgumentError.new("No column type specified for column [#{nm}]") unless t
|
46
|
+
v[:default] = DefaultsByType[t] unless v.has_key?(:default)
|
47
|
+
# the accessor is only used for some target languages
|
48
|
+
v[:accessor] = nm unless v.has_key?(:accessor)
|
49
|
+
return v
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.normalize_index_column_def(cd)
|
53
|
+
if cd.is_a?(Symbol)
|
54
|
+
cd = cd.to_s
|
55
|
+
end
|
56
|
+
if cd.is_a?(String)
|
57
|
+
# ascending is the default
|
58
|
+
return {:name=>cd, :ascending=>true}
|
59
|
+
end
|
60
|
+
cd[:ascending] = true unless cd.has_key?(:ascending)
|
61
|
+
validate_keys(cd,[:name,:ascending])
|
62
|
+
return cd
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.normalize_index_def(k,v)
|
66
|
+
validate_keys(v,[:unique,:columns,:accessor, :name])
|
67
|
+
if v.has_key?(:name) and (k.to_s != v[:name].to_s)
|
68
|
+
raise ArgumentError.new("Inconsistent names for index: [#{k}] and [#{v[:name]}].")
|
69
|
+
end
|
70
|
+
d = {}
|
71
|
+
d[:unique] = (v[:unique] or false)
|
72
|
+
d[:accessor] = (v[:accessor] or k.to_s)
|
73
|
+
d[:columns] = v[:columns].map{|cd|
|
74
|
+
normalize_index_column_def(cd)
|
75
|
+
}
|
76
|
+
return d
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.normalize_table_schema(nm, h)
|
80
|
+
cols = {}
|
81
|
+
# columns is a hash of column_name=>defn
|
82
|
+
h[:columns].each{|k,v|
|
83
|
+
cn = k.to_s
|
84
|
+
cols[cn] = normalize_column_def(cn, v)
|
85
|
+
}
|
86
|
+
|
87
|
+
ixs = {}
|
88
|
+
h[:indices].each{|k,v|
|
89
|
+
ixs[k.to_s] = normalize_index_def(k,v)
|
90
|
+
}
|
91
|
+
|
92
|
+
if h.has_key?(:name) and (nm.to_s != h[:name].to_s)
|
93
|
+
raise ArgumentError.new("Inconsistent names for table: [#{nm}] and [#{h[:name]}].")
|
94
|
+
end
|
95
|
+
|
96
|
+
return {
|
97
|
+
:name=>nm.to_s,
|
98
|
+
:accessor=>(h[:accessor] or nm).to_s,
|
99
|
+
:columns=>cols,
|
100
|
+
:indices=>ixs
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.normalize_schema(s)
|
105
|
+
validate_keys(s,[:name, :tables, :version, :classname])
|
106
|
+
raise ArgumentError.new("Invalid schema definition") unless
|
107
|
+
s[:name] and s[:name].size and s[:tables] and s[:version]
|
108
|
+
|
109
|
+
tbls = {}
|
110
|
+
if s[:tables].is_a? Hash
|
111
|
+
s[:tables].each{|k,v|
|
112
|
+
tbls[k.to_s] = normalize_table_schema(k.to_s, v)
|
113
|
+
}
|
114
|
+
else
|
115
|
+
s[:tables].each{|v|
|
116
|
+
nm = v[:name].to_s
|
117
|
+
tbls[nm] = normalize_table_schema(nm, v)
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
extras = {}
|
122
|
+
extras[:classname] = s[:classname] if s.has_key?(:classname)
|
123
|
+
|
124
|
+
return {
|
125
|
+
:name=>s[:name].to_s,
|
126
|
+
:tables=>tbls,
|
127
|
+
:version=>s[:version]
|
128
|
+
}.merge(extras)
|
129
|
+
end
|
130
|
+
|
131
|
+
end #module Model
|
132
|
+
end #module Oinky
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# This source is distributed under the terms of the MIT License. Refer
|
2
|
+
# to the 'LICENSE' file for details.
|
3
|
+
#
|
4
|
+
# Copyright (c) Jacob Lacouture, 2012
|
5
|
+
|
6
|
+
|
7
|
+
module Oinky
|
8
|
+
module Detail
|
9
|
+
class Builder
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
def initialize(lwidth = 4, lcount = 0)
|
13
|
+
@lwidth = lwidth
|
14
|
+
@lcount = lcount
|
15
|
+
@str = []
|
16
|
+
end
|
17
|
+
def next(s1, s2)
|
18
|
+
self << s1
|
19
|
+
@lcount += 1
|
20
|
+
yield
|
21
|
+
@lcount -= 1
|
22
|
+
self << s2
|
23
|
+
self
|
24
|
+
end
|
25
|
+
def <<(s)
|
26
|
+
write(s, 0)
|
27
|
+
end
|
28
|
+
def write(s, ldelta)
|
29
|
+
l = @lcount + ldelta
|
30
|
+
@str << ((' ' * l * @lwidth) + s)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def format
|
35
|
+
return (@str * "\n") + "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def each
|
39
|
+
return @str.each
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
data/lib/oinky/query.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# This source is distributed under the terms of the MIT License. Refer
|
2
|
+
# to the 'LICENSE' file for details.
|
3
|
+
#
|
4
|
+
# Copyright (c) Jacob Lacouture, 2012
|
5
|
+
|
6
|
+
require 'oinky/error'
|
7
|
+
|
8
|
+
# This is the crude skeleton of basic query operations. Currently
|
9
|
+
# This works by returning enhanced enumerator objects.
|
10
|
+
|
11
|
+
module Oinky
|
12
|
+
# This adds simple operations (max/min/average) to enumerable value sets.
|
13
|
+
class ValuesEnumerator < Enumerator
|
14
|
+
def self.from_opt(opt)
|
15
|
+
# These are column references.
|
16
|
+
if opt.is_a? Symbol
|
17
|
+
opt = opt.to_s
|
18
|
+
end
|
19
|
+
if opt.is_a? String
|
20
|
+
# Turn this into a proc that extracts the selected value.
|
21
|
+
cn = opt
|
22
|
+
opt = lambda {|row| row[cn]}
|
23
|
+
end
|
24
|
+
|
25
|
+
raise OinkyException.new("ArgumentError - Invalid proc") unless opt.is_a? Proc
|
26
|
+
return opt
|
27
|
+
end
|
28
|
+
private
|
29
|
+
def values_loop(opt)
|
30
|
+
# This is a function object which will map the row to a value that
|
31
|
+
# we will compute the average of
|
32
|
+
opt = self.class.from_opt(opt)
|
33
|
+
begin
|
34
|
+
while true
|
35
|
+
v = self.next
|
36
|
+
val = opt.call(v)
|
37
|
+
raise OinkyException.new("nil result from functor") unless val
|
38
|
+
yield val
|
39
|
+
end
|
40
|
+
rescue StopIteration => e
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
public
|
45
|
+
def average(opt)
|
46
|
+
avg = nil
|
47
|
+
count = 0
|
48
|
+
values_loop(opt) { |val|
|
49
|
+
if avg
|
50
|
+
avg += val
|
51
|
+
else
|
52
|
+
avg = val
|
53
|
+
end
|
54
|
+
count += 1
|
55
|
+
}
|
56
|
+
# We do not test for zero elements. We just invoke the value's divide
|
57
|
+
# method. The caller can use any type, and define divide however
|
58
|
+
# they choose.
|
59
|
+
return avg / count
|
60
|
+
end
|
61
|
+
def max(opt)
|
62
|
+
mx = nil
|
63
|
+
values_loop(opt) { |val|
|
64
|
+
if not mx
|
65
|
+
mx = val
|
66
|
+
elsif mx < val
|
67
|
+
mx = val
|
68
|
+
end
|
69
|
+
}
|
70
|
+
return mx
|
71
|
+
end
|
72
|
+
def min(opt)
|
73
|
+
mn = nil
|
74
|
+
values_loop(opt) { |val|
|
75
|
+
if not mn
|
76
|
+
mn = val
|
77
|
+
elsif mn > val
|
78
|
+
mn = val
|
79
|
+
end
|
80
|
+
}
|
81
|
+
return mn
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Add dataset filtering to the rowset (applies to both tables and indices)
|
86
|
+
# The indexes are not used autmatically. An index is only used if the
|
87
|
+
# filter is applied to an index using positional specification.
|
88
|
+
|
89
|
+
module RowSet
|
90
|
+
def __each_filtered(c,terminate,filter)
|
91
|
+
Enumerator.new {|blk|
|
92
|
+
if c.is_valid?
|
93
|
+
v = c.select_all
|
94
|
+
while true
|
95
|
+
blk.yield v if filter.call(v)
|
96
|
+
c.seek_next
|
97
|
+
break unless c.is_valid?
|
98
|
+
v = c.select_all
|
99
|
+
break if terminate and terminate.call(v)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
}
|
103
|
+
end
|
104
|
+
def __make_filter_proc(f)
|
105
|
+
if [String,Integer,Fixnum,Float,DateTime].find_index(f.class)
|
106
|
+
return lambda{|v| v == f}
|
107
|
+
end
|
108
|
+
if f.is_a? Proc
|
109
|
+
return f
|
110
|
+
end
|
111
|
+
raise ArgumentError.new("Hash value must be proc or value.")
|
112
|
+
end
|
113
|
+
def __rs_filter(f,c,t)
|
114
|
+
if f.is_a? Hash
|
115
|
+
p = {}
|
116
|
+
cols = @table.columns
|
117
|
+
f.each{|k,kf|
|
118
|
+
unless cols[k]
|
119
|
+
raise ArgumentError.new("Unknown column name in hash filter.")
|
120
|
+
end
|
121
|
+
p[k] = __make_filter_proc(kf)
|
122
|
+
}
|
123
|
+
f = lambda {|v|
|
124
|
+
r = true
|
125
|
+
p.each{|k,kf|
|
126
|
+
r &&= kf.call(v[k])
|
127
|
+
}
|
128
|
+
}
|
129
|
+
end
|
130
|
+
unless f.is_a? Proc
|
131
|
+
raise ArgumentError.new("Invalid argument to filter. Need Proc or Hash.")
|
132
|
+
end
|
133
|
+
|
134
|
+
seq = __each_filtered(c,t,f)
|
135
|
+
if block_given?
|
136
|
+
seq.each {|v|
|
137
|
+
yield v
|
138
|
+
}
|
139
|
+
return self
|
140
|
+
else
|
141
|
+
return seq
|
142
|
+
end
|
143
|
+
end
|
144
|
+
def filter(f)
|
145
|
+
__rs_filter(f,new_cursor(),nil)
|
146
|
+
end
|
147
|
+
end #module RowSet
|
148
|
+
|
149
|
+
class Index
|
150
|
+
# Specialize the rowset filter to add specification by array(position)
|
151
|
+
def filter(f)
|
152
|
+
if f.is_a? Array
|
153
|
+
c = new_cursor().seek(f)
|
154
|
+
rg = f
|
155
|
+
cols = self.columns
|
156
|
+
if f.size > cols.size
|
157
|
+
raise ArgumentError.new("Position specification exceeds index width.")
|
158
|
+
end
|
159
|
+
# truncate our test width to what we were given
|
160
|
+
cols = cols[0..f.size-1]
|
161
|
+
# Use an array for indirection in the first parameter, so
|
162
|
+
# the two lambdas can share state.
|
163
|
+
result = [true]
|
164
|
+
f = lambda {|v|
|
165
|
+
r = true
|
166
|
+
cols.each_with_index{|cn,i|
|
167
|
+
if rg[i].is_a? Proc
|
168
|
+
r &&= rg[i].call(v[cn])
|
169
|
+
else
|
170
|
+
r &&= (v[cn] == rg[i])
|
171
|
+
end
|
172
|
+
}
|
173
|
+
result[0] = r
|
174
|
+
r
|
175
|
+
}
|
176
|
+
result[0] = c.is_valid? && f.call(c.select_all)
|
177
|
+
# We stop on the first rejection
|
178
|
+
done = lambda { |c| !result[0]}
|
179
|
+
else
|
180
|
+
c = new_cursor().seek_first()
|
181
|
+
done = nil
|
182
|
+
end
|
183
|
+
e = __rs_filter(f,c,done)
|
184
|
+
if block_given?
|
185
|
+
e.each {|v| yield v}
|
186
|
+
return self
|
187
|
+
else
|
188
|
+
return e
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end #module Oinky
|
193
|
+
|