store-digest 0.3.0 → 0.4.3

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.
@@ -37,7 +37,7 @@ module Store::Digest::Meta
37
37
 
38
38
  # Set/add an individual object's metadata to the database.
39
39
  #
40
- # @param obj [Store::Digest::Object] the object to store
40
+ # @param obj [Store::Digest::Entry] the object to store
41
41
  # @param preserve [false, true] flag to preserve modification time
42
42
  #
43
43
  # @return [void]
@@ -0,0 +1,174 @@
1
+ require 'store/digest/version'
2
+
3
+ require 'thread'
4
+ require 'stringio'
5
+
6
+ # This class is an attempt to normalize input so that it can be
7
+ # {#read} like an IO handle. Use the class method {.coerce} to
8
+ # determine if it's even necessary.
9
+ #
10
+ class Store::Digest::ReadWrapper
11
+
12
+ private
13
+
14
+ # Test if the object quacks like an IO.
15
+ #
16
+ # @param obj [Object] said object
17
+ #
18
+ # @return [false, true]
19
+ #
20
+ def self.quacks? obj
21
+ obj.is_a?(IO) or %i[gets read close].all? do |m|
22
+ obj.respond_to? m
23
+ end
24
+ end
25
+
26
+ # close the pipe and join the thread.
27
+ #
28
+ # @return [void]
29
+ #
30
+ def cleanup
31
+ @mutex.synchronize do
32
+ return if @done
33
+ @done = true
34
+ @read.close unless @read.closed?
35
+ end
36
+
37
+ @thread.join
38
+ end
39
+
40
+ public
41
+
42
+ # XXX maybe later lol
43
+ # def self.assert! obj, thunk: false
44
+ # return true if obj.is_a?(self) || quacks?(obj) || obj.respond_to?(:each)
45
+
46
+ # if obj.respond_to?(:call)
47
+ # elsif obj.respond.to_?
48
+ # end
49
+ # end
50
+
51
+ # Attempt to coerce a suitable object or no-op.
52
+ #
53
+ # @param obj [Object] an object to be coerced
54
+ #
55
+ # @param thunk [false, true] let a thunk (a zero-arity callable that
56
+ # in this case returns a read handle) pass through; if falsy, it
57
+ # will execute the thunk and expect it to return something that
58
+ # quacks like a read handle, and throw an error if it isn't.
59
+ #
60
+ # @raise [ArgumentError] if the input is not sufficiently coercible
61
+ #
62
+ # @return [ReadWrapper,Object] a new proxy object around whatever
63
+ # the input is, or the original input if file-handle-ey enough
64
+ #
65
+ def self.coerce obj, thunk: false
66
+ return obj if [self, Store::Digest::Entry].any? { |c| obj.is_a? c }
67
+
68
+ return obj.open('rb') if obj.is_a? Pathname
69
+
70
+ return obj if quacks? obj # no need for this if it can read
71
+
72
+ return StringIO.new(obj) if obj.is_a? String
73
+
74
+ # response bodies /don't do this but other stuff does
75
+ if obj.respond_to?(:arity) && obj.arity == 0 ||
76
+ obj.respond_to?(:call) && obj.method(:call).arity == 0
77
+ # let the thunk through
78
+ return obj if thunk
79
+
80
+ out = obj.call
81
+ raise ArgumentError,
82
+ 'a `call` with no arguments must return an IO-like object' unless
83
+ quacks? out
84
+
85
+ return out
86
+ end
87
+
88
+ new obj
89
+ end
90
+
91
+ class << self
92
+ alias_method :[], :coerce
93
+ end
94
+
95
+ # Initialize a wrapper.
96
+ #
97
+ # @param obj [#call, #each] a suitable object
98
+ #
99
+ # @raise [ArgumentError] said object is unsuitable
100
+ #
101
+ def initialize obj
102
+ test = obj.respond_to?(:arity) ? obj :
103
+ obj.respond_to?(:call) ? obj.method(:call) : nil
104
+
105
+ if test
106
+ raise ArgumentError,
107
+ 'Callable object is expected to take a write handle as an argument' if
108
+ test.arity == 0
109
+ elsif obj.respond_to?(:each)
110
+ nil
111
+ elsif obj.respond_to? :to_s
112
+ obj = [obj.to_s]
113
+ else
114
+ raise ArgumentError,
115
+ 'Argument must respond to #call(write_fh) or #each or #to_s'
116
+ end
117
+
118
+ @done = false
119
+ @mutex = Mutex.new
120
+
121
+ @read, @write = IO.pipe
122
+
123
+ @thread = Thread.new do
124
+ if obj.respond_to? :call
125
+ obj.call @write
126
+ else
127
+ obj.each { |x| @write << x.to_s.b }
128
+ end
129
+ rescue Errno::EPIPE # => e
130
+ nil # not sure if we do anything here
131
+ ensure
132
+ @write.close unless @write.closed?
133
+ end
134
+ end
135
+
136
+ # `gets` for parity with IO
137
+ #
138
+ # @return [String, nil]
139
+ #
140
+ def gets sep = $/, chomp = false
141
+ unless @read.closed?
142
+ out = @read.gets sep, chomp
143
+ cleanup if out.nil?
144
+
145
+ out
146
+ end
147
+ end
148
+
149
+ # `read` for parity with IO
150
+ #
151
+ # @param maxlen [Integer] the length to read
152
+ # @param string [String] an optional string
153
+ #
154
+ # @return [String, nil]
155
+ #
156
+ def read maxlen = nil, string = nil
157
+ unless @read.closed?
158
+ out = @read.read maxlen, string
159
+ cleanup if out.nil?
160
+
161
+ out
162
+ end
163
+ end
164
+
165
+ # `close` for parity with IO
166
+ #
167
+ def close
168
+ cleanup
169
+ nil # close returns nil
170
+ end
171
+ end
172
+
173
+ # for the symbol
174
+ class Store::Digest::Entry; end
@@ -1,5 +1,5 @@
1
1
  module Store
2
2
  class Digest
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.3".freeze
4
4
  end
5
5
  end