storable 0.8.5 → 0.9.pre.RC1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/{README.rdoc → README.md} +38 -37
- data/Rakefile +12 -65
- data/lib/core_ext.rb +273 -0
- data/lib/proc_source.rb +169 -65
- data/lib/storable.rb +60 -32
- data/storable.gemspec +18 -38
- metadata +36 -59
- data/CHANGES.txt +0 -152
- data/LICENSE.txt +0 -19
data/lib/proc_source.rb
CHANGED
@@ -1,113 +1,156 @@
|
|
1
|
-
|
1
|
+
|
2
|
+
#
|
2
3
|
# Based on:
|
3
|
-
#
|
4
|
-
|
4
|
+
# https://github.com/imedo/background
|
5
|
+
# https://github.com/imedo/background_lite
|
6
|
+
# With improvements by:
|
7
|
+
# https://github.com/notro/storable
|
8
|
+
#
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'irb/ruby-token' # ruby <2.7
|
12
|
+
rescue
|
13
|
+
require 'lib/core_ext' # ruby >=2.7
|
14
|
+
end
|
5
15
|
|
6
|
-
require 'stringio'
|
7
16
|
require 'irb/ruby-lex'
|
8
|
-
|
17
|
+
require 'stringio'
|
18
|
+
|
19
|
+
SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
|
20
|
+
|
9
21
|
|
10
22
|
class ProcString < String
|
11
|
-
|
23
|
+
# Filename where the proc is defined
|
24
|
+
attr_accessor :file
|
25
|
+
|
26
|
+
# Range of lines where the proc is defined
|
27
|
+
# ex. (12..16)
|
28
|
+
attr_accessor :lines
|
29
|
+
|
30
|
+
attr_accessor :arity, :kind # :nodoc: FIXME: Should be removed?
|
31
|
+
|
32
|
+
# Return a Proc object
|
33
|
+
# If #lines and #file is specified, these are tied to the proc.
|
12
34
|
def to_proc(kind="proc")
|
13
|
-
|
35
|
+
if @file && @lines
|
36
|
+
raise "#lines must be a range" unless @lines.kind_of? Range
|
37
|
+
result = eval("#{kind} #{self}", binding, @file, @lines.min)
|
38
|
+
else
|
39
|
+
result = eval("#{kind} #{self}")
|
40
|
+
end
|
14
41
|
result.source = self
|
15
42
|
result
|
16
43
|
end
|
44
|
+
|
45
|
+
# Return a lambda
|
17
46
|
def to_lambda
|
18
|
-
to_proc "
|
47
|
+
to_proc "lambda"
|
19
48
|
end
|
20
49
|
end
|
21
50
|
|
22
51
|
class RubyToken::Token
|
23
|
-
|
24
|
-
|
25
|
-
FAKIES = [
|
26
|
-
|
52
|
+
|
53
|
+
# These EXPR_BEG tokens don't have associated end tags
|
54
|
+
FAKIES = [
|
55
|
+
RubyToken::TkWHEN,
|
56
|
+
RubyToken::TkELSIF,
|
57
|
+
RubyToken::TkELSE,
|
58
|
+
RubyToken::TkTHEN,
|
59
|
+
]
|
60
|
+
|
61
|
+
def name
|
62
|
+
@name ||= nil
|
63
|
+
end
|
64
|
+
|
27
65
|
def open_tag?
|
28
|
-
return false if
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
66
|
+
return false if name.nil? || get_props.nil?
|
67
|
+
is_open = (
|
68
|
+
(get_props[1] == RubyToken::EXPR_BEG) &&
|
69
|
+
(self.class.to_s !~ /_MOD/) && # ignore onliner if, unless, etc...
|
70
|
+
(!FAKIES.member?(self.class))
|
71
|
+
)
|
72
|
+
is_open
|
33
73
|
end
|
34
|
-
|
74
|
+
|
35
75
|
def get_props
|
36
|
-
RubyToken::TkReading2Token[
|
76
|
+
RubyToken::TkReading2Token[name]
|
37
77
|
end
|
38
|
-
|
78
|
+
|
39
79
|
end
|
40
80
|
|
41
81
|
# Based heavily on code from http://github.com/imedo/background
|
42
82
|
# Big thanks to the imedo dev team!
|
43
83
|
#
|
44
84
|
module ProcSource
|
45
|
-
|
46
|
-
def self.find(filename, start_line=
|
85
|
+
|
86
|
+
def self.find(filename, start_line=1, block_only=true)
|
47
87
|
lines, lexer = nil, nil
|
48
88
|
retried = 0
|
49
89
|
loop do
|
50
90
|
lines = get_lines(filename, start_line)
|
91
|
+
# binding.pry
|
51
92
|
return nil if lines.nil?
|
52
|
-
#p [start_line, lines[0]]
|
53
93
|
if !line_has_open?(lines.join) && start_line >= 0
|
54
|
-
start_line -= 1 and retried +=1 and redo
|
94
|
+
start_line -= 1 and retried +=1 and redo
|
55
95
|
end
|
56
96
|
lexer = RubyLex.new
|
57
97
|
lexer.set_input(StringIO.new(lines.join))
|
58
98
|
break
|
59
99
|
end
|
100
|
+
|
60
101
|
stoken, etoken, nesting = nil, nil, 0
|
61
102
|
while token = lexer.token
|
62
|
-
n = token.instance_variable_get(:@name)
|
63
|
-
|
64
103
|
if RubyToken::TkIDENTIFIER === token
|
65
|
-
#nothing
|
104
|
+
# nothing
|
66
105
|
elsif token.open_tag? || RubyToken::TkfLBRACE === token
|
67
106
|
nesting += 1
|
68
107
|
stoken = token if nesting == 1
|
69
108
|
elsif RubyToken::TkEND === token || RubyToken::TkRBRACE === token
|
70
109
|
if nesting == 1
|
71
|
-
etoken = token
|
110
|
+
etoken = token
|
72
111
|
break
|
73
112
|
end
|
74
113
|
nesting -= 1
|
114
|
+
elsif RubyToken::TkLBRACE === token
|
115
|
+
nesting += 1
|
75
116
|
elsif RubyToken::TkBITOR === token && stoken
|
76
|
-
#nothing
|
117
|
+
# nothing
|
77
118
|
elsif RubyToken::TkNL === token && stoken && etoken
|
78
119
|
break if nesting <= 0
|
79
120
|
else
|
80
|
-
#
|
121
|
+
# nothing
|
81
122
|
end
|
82
123
|
end
|
83
|
-
|
124
|
+
|
125
|
+
# binding.pry
|
126
|
+
|
84
127
|
lines = lines[stoken.line_no-1 .. etoken.line_no-1]
|
85
|
-
|
86
|
-
# Remove the crud before the block definition.
|
128
|
+
|
129
|
+
# Remove the crud before the block definition.
|
87
130
|
if block_only
|
88
131
|
spaces = lines.last.match(/^\s+/)[0] rescue ''
|
89
132
|
lines[0] = spaces << lines[0][stoken.char_no .. -1]
|
90
133
|
end
|
91
134
|
ps = ProcString.new lines.join
|
92
|
-
ps.file
|
93
|
-
|
135
|
+
ps.file = filename
|
136
|
+
ps.lines = start_line .. start_line+etoken.line_no-1
|
94
137
|
ps
|
95
138
|
end
|
96
|
-
|
139
|
+
|
97
140
|
# A hack for Ruby 1.9, otherwise returns true.
|
98
141
|
#
|
99
142
|
# Ruby 1.9 returns an incorrect line number
|
100
143
|
# when a block is specified with do/end. It
|
101
|
-
# happens b/c the line number returned by
|
144
|
+
# happens b/c the line number returned by
|
102
145
|
# Ruby 1.9 is based on the first line in the
|
103
146
|
# block which contains a token (i.e. not a
|
104
|
-
# new line or comment etc...).
|
147
|
+
# new line or comment etc...).
|
105
148
|
#
|
106
|
-
# NOTE: This won't work in cases where the
|
107
|
-
# incorrect line also contains a "do".
|
149
|
+
# NOTE: This won't work in cases where the
|
150
|
+
# incorrect line also contains a "do".
|
108
151
|
#
|
109
152
|
def self.line_has_open?(str)
|
110
|
-
return true unless RUBY_VERSION >= '1.9'
|
153
|
+
return true unless RUBY_VERSION >= '1.9' && RUBY_VERSION < '2.0'
|
111
154
|
lexer = RubyLex.new
|
112
155
|
lexer.set_input(StringIO.new(str))
|
113
156
|
success = false
|
@@ -117,12 +160,14 @@ module ProcSource
|
|
117
160
|
break
|
118
161
|
when RubyToken::TkDO
|
119
162
|
success = true
|
163
|
+
when RubyToken::TkfLBRACE
|
164
|
+
success = true
|
120
165
|
when RubyToken::TkCONSTANT
|
121
|
-
if token.
|
166
|
+
if token.name == "Proc" &&
|
122
167
|
lexer.token.is_a?(RubyToken::TkDOT)
|
123
168
|
method = lexer.token
|
124
169
|
if method.is_a?(RubyToken::TkIDENTIFIER) &&
|
125
|
-
method.
|
170
|
+
method.name == "new"
|
126
171
|
success = true
|
127
172
|
end
|
128
173
|
end
|
@@ -130,20 +175,26 @@ module ProcSource
|
|
130
175
|
end
|
131
176
|
success
|
132
177
|
end
|
133
|
-
|
134
|
-
|
135
|
-
def self.get_lines(filename, start_line = 0)
|
178
|
+
|
179
|
+
def self.get_lines(filename, start_line = 1)
|
136
180
|
case filename
|
137
181
|
when nil
|
138
182
|
nil
|
139
|
-
|
183
|
+
|
184
|
+
# We're in irb
|
185
|
+
when "(irb)"
|
140
186
|
IRB.conf[:MAIN_CONTEXT].io.line(start_line .. -2)
|
141
|
-
|
187
|
+
|
188
|
+
# Or an eval
|
189
|
+
when /^\(eval.+\)$/
|
142
190
|
EVAL_LINES__[filename][start_line .. -2]
|
143
|
-
|
191
|
+
|
192
|
+
# Or most likely a .rb file
|
193
|
+
else
|
144
194
|
# Ruby already parsed this file? (see disclaimer above)
|
145
195
|
if defined?(SCRIPT_LINES__) && SCRIPT_LINES__[filename]
|
146
196
|
SCRIPT_LINES__[filename][(start_line - 1) .. -1]
|
197
|
+
|
147
198
|
# If the file exists we're going to try reading it in
|
148
199
|
elsif File.exist?(filename)
|
149
200
|
begin
|
@@ -156,24 +207,77 @@ module ProcSource
|
|
156
207
|
end
|
157
208
|
end
|
158
209
|
|
159
|
-
class Proc
|
160
|
-
attr_reader :file, :line
|
210
|
+
class Proc # :nodoc:
|
161
211
|
attr_writer :source
|
162
|
-
|
212
|
+
@@regexp = Regexp.new('^#<Proc:0x[0-9A-Fa-f]+@?\s*(.+):(\d+)(.+?)?>$')
|
213
|
+
|
163
214
|
def source_descriptor
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
215
|
+
return [@file, @line] if @file && @line
|
216
|
+
|
217
|
+
source_location = nil
|
218
|
+
if RUBY_VERSION >= '2.7'
|
219
|
+
source_location = *self.source_location
|
220
|
+
else
|
221
|
+
inspection = inspect
|
222
|
+
md = @@regexp.match(inspection)
|
223
|
+
exmsg = 'Unable to parse proc inspect (%s)' % inspection
|
224
|
+
raise Exception, exmsg if md.nil?
|
225
|
+
source_location = md.captures
|
168
226
|
end
|
169
|
-
|
227
|
+
|
228
|
+
file, line = *source_location
|
229
|
+
@file, @line = [file, line.to_i]
|
170
230
|
end
|
171
|
-
|
231
|
+
|
172
232
|
def source
|
173
233
|
@source ||= ProcSource.find(*self.source_descriptor)
|
174
234
|
end
|
175
|
-
|
176
|
-
|
235
|
+
|
236
|
+
def line
|
237
|
+
source_descriptor
|
238
|
+
@line
|
239
|
+
end
|
240
|
+
|
241
|
+
def file
|
242
|
+
source_descriptor
|
243
|
+
@file
|
244
|
+
end
|
245
|
+
|
246
|
+
# Dump to Marshal format.
|
247
|
+
# p = Proc.new { false }
|
248
|
+
# Marshal.dump p
|
249
|
+
def _dump(limit)
|
250
|
+
raise "can't dump proc, #source is nil" if source.nil?
|
251
|
+
str = Marshal.dump(source)
|
252
|
+
str
|
253
|
+
end
|
254
|
+
|
255
|
+
# Load from Marshal format.
|
256
|
+
# p = Proc.new { false }
|
257
|
+
# Marshal.load Marshal.dump p
|
258
|
+
def self._load(str)
|
259
|
+
@source = Marshal.load(str)
|
260
|
+
@source.to_proc
|
261
|
+
end
|
262
|
+
|
263
|
+
# Dump to JSON string
|
264
|
+
def to_json(*args)
|
265
|
+
raise "can't serialize proc, #source is nil" if source.nil?
|
266
|
+
{
|
267
|
+
'json_class' => self.class.name,
|
268
|
+
'data' => [source.to_s, source.file, source.lines.min, source.lines.max]
|
269
|
+
}.to_json#(*args)
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.json_create(o)
|
273
|
+
s, file, min, max = o['data']
|
274
|
+
ps = ProcString.new s
|
275
|
+
ps.file = file
|
276
|
+
ps.lines = (min..max)
|
277
|
+
ps.to_proc
|
278
|
+
end
|
279
|
+
|
280
|
+
# Create a Proc object from a string of Ruby code.
|
177
281
|
# It's assumed the string contains do; end or { }.
|
178
282
|
#
|
179
283
|
# Proc.from_string("do; 2+2; end")
|
@@ -181,7 +285,7 @@ class Proc #:nodoc:
|
|
181
285
|
def self.from_string(str)
|
182
286
|
eval "Proc.new #{str}"
|
183
287
|
end
|
184
|
-
|
288
|
+
|
185
289
|
end
|
186
290
|
|
187
291
|
if $0 == __FILE__
|
@@ -194,17 +298,17 @@ if $0 == __FILE__
|
|
194
298
|
end
|
195
299
|
|
196
300
|
a = Proc.new() { |a|
|
197
|
-
puts "Hello Rudy2"
|
301
|
+
puts "Hello Rudy2"
|
198
302
|
}
|
199
|
-
|
303
|
+
|
200
304
|
b = Proc.new() do |b|
|
201
305
|
puts { "Hello Rudy3" } if true
|
202
306
|
end
|
203
|
-
|
307
|
+
|
204
308
|
puts @blk.inspect, @blk.source
|
205
309
|
puts [a.inspect, a.source]
|
206
310
|
puts b.inspect, b.source
|
207
|
-
|
311
|
+
|
208
312
|
proc = @blk.source.to_proc
|
209
313
|
proc.call(1)
|
210
314
|
end
|
data/lib/storable.rb
CHANGED
@@ -35,14 +35,14 @@ class Storable
|
|
35
35
|
require 'proc_source'
|
36
36
|
require 'storable/orderedhash' if USE_ORDERED_HASH
|
37
37
|
unless defined?(SUPPORTED_FORMATS) # We can assume all are defined
|
38
|
-
VERSION = "0.8.
|
38
|
+
VERSION = "0.8.9"
|
39
39
|
NICE_TIME_FORMAT = "%Y-%m-%d@%H:%M:%S".freeze
|
40
40
|
SUPPORTED_FORMATS = [:tsv, :csv, :yaml, :json, :s, :string].freeze
|
41
41
|
end
|
42
42
|
|
43
43
|
@debug = false
|
44
44
|
class << self
|
45
|
-
attr_accessor :sensitive_fields, :field_names, :field_types, :debug
|
45
|
+
attr_accessor :sensitive_fields, :field_names, :field_types, :field_opts, :debug
|
46
46
|
end
|
47
47
|
|
48
48
|
# Passes along fields to inherited classes
|
@@ -66,37 +66,61 @@ class Storable
|
|
66
66
|
# data is available by the standard accessors, class.product and class.product= etc...
|
67
67
|
# The value of the field will be cast to the type (if provided) when read from a file.
|
68
68
|
# The value is not touched when the type is not provided.
|
69
|
-
def self.field(args
|
69
|
+
def self.field(*args, &processor)
|
70
70
|
# TODO: Examine casting from: http://codeforpeople.com/lib/ruby/fattr/fattr-1.0.3/
|
71
|
-
|
72
|
-
|
71
|
+
field_definitions = {}
|
72
|
+
if args.first.kind_of?(Hash)
|
73
|
+
args.first.each_pair do |fname,klass|
|
74
|
+
field_definitions[fname] = { :class => klass }
|
75
|
+
end
|
76
|
+
else
|
77
|
+
fname, opts = *args
|
78
|
+
if opts.nil?
|
79
|
+
field_definitions[fname] = {}
|
80
|
+
elsif Hash === opts
|
81
|
+
field_definitions[fname] = opts
|
82
|
+
else
|
83
|
+
raise ArgumentError, "Second argument must be a hash"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
73
87
|
self.field_names ||= []
|
74
88
|
self.field_types ||= {}
|
75
|
-
|
76
|
-
|
77
|
-
self.
|
89
|
+
self.field_opts ||= {}
|
90
|
+
field_definitions.each_pair do |fname,opts|
|
91
|
+
self.field_names << fname
|
92
|
+
self.field_opts[fname] = opts
|
93
|
+
self.field_types[fname] = opts[:class] unless opts[:class].nil?
|
78
94
|
|
79
95
|
# This processor automatically converts a Proc object
|
80
96
|
# to a String of its source.
|
81
|
-
processor = proc_processor if
|
97
|
+
processor = proc_processor if opts[:class] == Proc && processor.nil?
|
82
98
|
|
83
99
|
unless processor.nil?
|
84
|
-
define_method("_storable_processor_#{
|
100
|
+
define_method("_storable_processor_#{fname}", &processor)
|
85
101
|
end
|
86
102
|
|
87
|
-
if method_defined?(
|
88
|
-
STDERR.puts "method exists: #{self}##{
|
103
|
+
if method_defined?(fname) # don't redefine the getter method
|
104
|
+
STDERR.puts "method exists: #{self}##{fname}" if Storable.debug
|
89
105
|
else
|
90
|
-
define_method(
|
91
|
-
instance_variable_get("@#{
|
106
|
+
define_method(fname) do
|
107
|
+
ret = instance_variable_get("@#{fname}")
|
108
|
+
if ret.nil?
|
109
|
+
if opts[:default]
|
110
|
+
ret = opts[:default]
|
111
|
+
elsif opts[:meth]
|
112
|
+
ret = self.send(opts[:meth])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
ret
|
92
116
|
end
|
93
117
|
end
|
94
118
|
|
95
|
-
if method_defined?("#{
|
96
|
-
STDERR.puts "method exists: #{self}##{
|
119
|
+
if method_defined?("#{fname}=") # don't redefine the setter methods
|
120
|
+
STDERR.puts "method exists: #{self}##{fname}=" if Storable.debug
|
97
121
|
else
|
98
|
-
define_method("#{
|
99
|
-
instance_variable_set("@#{
|
122
|
+
define_method("#{fname}=") do |val|
|
123
|
+
instance_variable_set("@#{fname}",val)
|
100
124
|
end
|
101
125
|
end
|
102
126
|
end
|
@@ -145,15 +169,15 @@ class Storable
|
|
145
169
|
|
146
170
|
# Returns an array of field names defined by self.field
|
147
171
|
def field_names
|
148
|
-
self.class.field_names
|
172
|
+
self.class.field_names #|| self.class.ancestors.first.field_names
|
149
173
|
end
|
150
174
|
# Returns an array of field types defined by self.field. Fields that did
|
151
175
|
# not receive a type are set to nil.
|
152
176
|
def field_types
|
153
|
-
self.class.field_types
|
177
|
+
self.class.field_types #|| self.class.ancestors.first.field_types
|
154
178
|
end
|
155
179
|
def sensitive_fields
|
156
|
-
self.class.sensitive_fields
|
180
|
+
self.class.sensitive_fields #|| self.class.ancestors.first.sensitive_fields
|
157
181
|
end
|
158
182
|
|
159
183
|
# Dump the object data to the given format.
|
@@ -201,6 +225,10 @@ class Storable
|
|
201
225
|
from_array *args
|
202
226
|
end
|
203
227
|
|
228
|
+
def initialize *args
|
229
|
+
init *args
|
230
|
+
end
|
231
|
+
|
204
232
|
def from_array *from
|
205
233
|
(self.field_names || []).each_with_index do |n,index|
|
206
234
|
break if index >= from.size
|
@@ -294,14 +322,16 @@ class Storable
|
|
294
322
|
def to_hash
|
295
323
|
preprocess if respond_to? :preprocess
|
296
324
|
tmp = USE_ORDERED_HASH ? Storable::OrderedHash.new : {}
|
297
|
-
field_names
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
325
|
+
if field_names
|
326
|
+
field_names.each do |fname|
|
327
|
+
next if sensitive? && self.class.sensitive_field?(fname)
|
328
|
+
v = self.send(fname)
|
329
|
+
v = process(fname, v) if has_processor?(fname)
|
330
|
+
if Array === v
|
331
|
+
v = v.collect { |v2| v2.kind_of?(Storable) ? v2.to_hash : v2 }
|
332
|
+
end
|
333
|
+
tmp[fname] = v.kind_of?(Storable) ? v.to_hash : v
|
303
334
|
end
|
304
|
-
tmp[fname] = v.kind_of?(Storable) ? v.to_hash : v
|
305
335
|
end
|
306
336
|
tmp
|
307
337
|
end
|
@@ -325,9 +355,6 @@ class Storable
|
|
325
355
|
hash = to_hash
|
326
356
|
if YAJL_LOADED # set by Storable
|
327
357
|
ret = Yajl::Encoder.encode(hash)
|
328
|
-
#raise "DELANO"
|
329
|
-
#ret.force_encoding("ISO-8859-1")
|
330
|
-
#p [:to, ret.encoding.name] if ret.respond_to?(:encoding)
|
331
358
|
ret
|
332
359
|
elsif JSON_LOADED
|
333
360
|
JSON.generate(hash, *from, &blk)
|
@@ -504,7 +531,8 @@ class Storable
|
|
504
531
|
end
|
505
532
|
def proc_processor
|
506
533
|
Proc.new do |val|
|
507
|
-
(Proc === val) ? val.source : val
|
534
|
+
ret = (Proc === val) ? val.source : val
|
535
|
+
ret
|
508
536
|
end
|
509
537
|
end
|
510
538
|
# If the object already has a value for +@id+
|