cache_box 0.0.1.pre.preview5 → 0.0.1.pre.preview10

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.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CacheBox
4
+ module Storage
5
+ # A storage backed by files.
6
+ class File
7
+ LOCK = Mutex.new
8
+
9
+ # Input:
10
+ #
11
+ # namespace = Symbol | String # Default: 'namespace'
12
+ # path = String # Path; Default: nil
13
+ #
14
+ # Output: N/A
15
+ def initialize(namespace: nil, path: nil)
16
+ validate_symbol_or_string!(namespace, 'namespace')
17
+ validate_string!(path, 'path')
18
+
19
+ namespace = namespace.to_s || 'namespace'
20
+
21
+ root = path || ::File.join(Dir.pwd, '.cache')
22
+ @path = ::File.join(root, namespace)
23
+ end
24
+
25
+ # Output: self
26
+ def reset!
27
+ LOCK.synchronize do
28
+ FileUtils.remove_entry_secure(@path, true)
29
+ end
30
+
31
+ self
32
+ end
33
+
34
+ # Reads the content.
35
+ #
36
+ # Input:
37
+ #
38
+ # name = String
39
+ #
40
+ # Output: Object # Anything
41
+ def read!(name)
42
+ validate_string!(name, 'name')
43
+
44
+ LOCK.synchronize do
45
+ file = ::File.join(@path, name)
46
+ return unless ::File.exist?(file)
47
+
48
+ Marshal.load(::File.read(file))
49
+ end
50
+ end
51
+
52
+ # Input:
53
+ #
54
+ # name = String
55
+ # data = Object # Anything
56
+ #
57
+ # Output: self
58
+ def write!(name, data)
59
+ validate_string!(name, 'name')
60
+
61
+ content = Marshal.dump(data)
62
+ file = ::File.join(@path, name)
63
+
64
+ LOCK.synchronize do
65
+ FileUtils.mkdir_p(@path)
66
+
67
+ ::File.write(file, content)
68
+ end
69
+
70
+ self
71
+ end
72
+
73
+ # Input:
74
+ #
75
+ # name = String
76
+ #
77
+ # Output: self
78
+ def delete!(name)
79
+ validate_string!(name, 'name')
80
+
81
+ LOCK.synchronize do
82
+ file = ::File.join(@path, name)
83
+ FileUtils.remove_entry_secure(file, true)
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ # Input:
90
+ #
91
+ # name = String
92
+ #
93
+ # Output: true | false
94
+ def has?(name)
95
+ validate_string!(name, 'name')
96
+
97
+ LOCK.synchronize do
98
+ file = ::File.join(@path, name)
99
+ ::File.file?(file)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def validate_symbol_or_string!(arg, name)
106
+ return if arg.nil? || arg.is_a?(Symbol) || arg.is_a?(String)
107
+
108
+ klass = arg.class
109
+ value = arg.inspect
110
+
111
+ raise(
112
+ ArgumentError,
113
+ "#{name} must be a Symbol or String, got #{klass}: #{value}"
114
+ )
115
+ end
116
+
117
+ def validate_string!(arg, name)
118
+ return if arg.nil? || arg.is_a?(String)
119
+
120
+ klass = arg.class
121
+ value = arg.inspect
122
+
123
+ raise(
124
+ ArgumentError,
125
+ "#{name} must be a String, got #{klass}: #{value}"
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CacheBox
4
+ module Storage
5
+ # A storage backed by an in memory Hash.
6
+ class Memory
7
+ LOCK = Mutex.new
8
+
9
+ # Accepts a Hash or any Hash-like object as argument. Will use a plain Hash
10
+ # if none provided.
11
+ #
12
+ # Input:
13
+ #
14
+ # state = Hash{...Object => Object} # Default: nil
15
+ #
16
+ # Output: N/A
17
+ def initialize(state = nil)
18
+ validate!(state)
19
+
20
+ LOCK.synchronize do
21
+ @state = state || {}
22
+ end
23
+ end
24
+
25
+ # Accepts a Hash or any Hash-like object as argument. Will use a plain Hash
26
+ # if none provided.
27
+ #
28
+ # Input:
29
+ #
30
+ # state = Hash{...Object => Object}
31
+ #
32
+ # Output: self
33
+ def reset!(state = nil)
34
+ initialize(state)
35
+
36
+ self
37
+ end
38
+
39
+ # Input:
40
+ #
41
+ # name = String
42
+ #
43
+ # Output: Object # Anything
44
+ def read!(name)
45
+ validate_string!(name, 'name')
46
+
47
+ LOCK.synchronize do
48
+ @state[name]
49
+ end
50
+ end
51
+
52
+ # Input:
53
+ #
54
+ # name = String
55
+ # data = Object # Anything
56
+ #
57
+ # Output: self
58
+ def write!(name, data)
59
+ validate_string!(name, 'name')
60
+
61
+ LOCK.synchronize do
62
+ @state[name] = data
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ # Input:
69
+ #
70
+ # name = String
71
+ #
72
+ # Output: self
73
+ def delete!(name)
74
+ validate_string!(name, 'name')
75
+
76
+ LOCK.synchronize do
77
+ @state.delete(name)
78
+ end
79
+
80
+ self
81
+ end
82
+
83
+ # Input:
84
+ #
85
+ # name = String
86
+ #
87
+ # Output: true | false
88
+ def has?(name)
89
+ validate_string!(name, 'name')
90
+
91
+ LOCK.synchronize do
92
+ @state.key?(name)
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ # Input:
99
+ #
100
+ # state = Object # Anything
101
+ #
102
+ # Output: N/A
103
+ def validate!(state)
104
+ return if state.nil? || state.is_a?(Hash)
105
+
106
+ validate_get!(state)
107
+ validate_set!(state)
108
+ validate_delete!(state)
109
+ validate_key!(state)
110
+ end
111
+
112
+ # Input:
113
+ #
114
+ # state = Object # Anything
115
+ #
116
+ # Output: N/A
117
+ def validate_get!(state)
118
+ unless state.respond_to?(:[])
119
+ raise(ArgumentError, 'Given state object does not respond to `:[]`')
120
+ end
121
+
122
+ arity = state.method(:[]).arity
123
+ unless arity == 1
124
+ raise(
125
+ ArgumentError,
126
+ "Given state object's `:[]` method arity must be 1, got: #{arity}"
127
+ )
128
+ end
129
+ end
130
+
131
+ # Input:
132
+ #
133
+ # state = Object # Anything
134
+ #
135
+ # Output: N/A
136
+ def validate_set!(state)
137
+ unless state.respond_to?(:[]=)
138
+ raise(ArgumentError, 'Given state object does not respond to `:[]=`')
139
+ end
140
+
141
+ arity = state.method(:[]=).arity
142
+ unless arity == 2
143
+ raise(
144
+ ArgumentError,
145
+ "Given state object's `:[]=` method arity must be 2, got: #{arity}"
146
+ )
147
+ end
148
+ end
149
+
150
+ # Input:
151
+ #
152
+ # state = Object # Anything
153
+ #
154
+ # Output: N/A
155
+ def validate_delete!(state)
156
+ unless state.respond_to?(:delete)
157
+ raise(ArgumentError, 'Given state object does not respond to `delete`')
158
+ end
159
+
160
+ arity = state.method(:delete).arity
161
+ unless arity == 1
162
+ raise(
163
+ ArgumentError,
164
+ "Given state object's `:delete` method arity must be 1, got: #{arity}"
165
+ )
166
+ end
167
+ end
168
+
169
+ # Input:
170
+ #
171
+ # state = Object # Anything
172
+ #
173
+ # Output: N/A
174
+ def validate_key!(state)
175
+ unless state.respond_to?(:key?)
176
+ raise(ArgumentError, 'Given state object does not respond to `:key?`')
177
+ end
178
+
179
+ arity = state.method(:key?).arity
180
+ unless arity == 1
181
+ raise(
182
+ ArgumentError,
183
+ "Given state object's `:key?` method arity must be 1, got: #{arity}"
184
+ )
185
+ end
186
+ end
187
+
188
+ # Input:
189
+ #
190
+ # arg = String
191
+ # name = String
192
+ #
193
+ # Output: N/A
194
+ def validate_string!(arg, name)
195
+ return if arg.nil? || arg.is_a?(String)
196
+
197
+ klass = arg.class
198
+ value = arg.inspect
199
+
200
+ raise(
201
+ ArgumentError,
202
+ "#{name} must be a String, got #{klass}: #{value}"
203
+ )
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CacheBox
4
+ class Unit
5
+ include CacheBox::Helper::Validate
6
+
7
+ LOCK = Mutex.new
8
+
9
+ # Input:
10
+ #
11
+ # namespace = String | Symbol # Default: :namespace
12
+ #
13
+ # Output: N/A
14
+ def initialize(namespace = nil, logger: nil, storage: nil)
15
+ namespace ||= :namespace
16
+ validate_string_or_symbol!(namespace, 'namespace')
17
+
18
+ @namespace_o = namespace
19
+ @namespace_s = namespace.to_s
20
+ @logger = logger || Logger.new(STDOUT, level: Logger::INFO)
21
+ @storage = storage || ::CacheBox::Storage::File.new(namespace: @namespace_o)
22
+
23
+ LOCK.synchronize do
24
+ @state = { result: ::CacheBox::Stash.new, stash: ::CacheBox::Stash.new }
25
+ end
26
+ end
27
+
28
+ # Input:
29
+ #
30
+ # name = String | Symbol # Default: 'name'
31
+ # args = Object
32
+ # input = Object
33
+ # &block = Proc(CacheBox::Box)
34
+ #
35
+ # Output: Object # Anything the &block returns.
36
+ def with(name = nil, args = nil, input = nil, &block)
37
+ name ||= 'name'
38
+ validate_string_or_symbol!(name, 'name')
39
+ validate_block_presence!(block, '#with')
40
+
41
+ name_o = name
42
+ name_s = name.to_s
43
+ box = nil
44
+
45
+ LOCK.synchronize do
46
+ return @state[:result][name_s] if @state[:result].key?(name_s)
47
+
48
+ data = @storage.read!(name_s)
49
+ if data&.key?(:result)
50
+ @state[:result][name_s] = data[:result]
51
+
52
+ return data[:result]
53
+ elsif data&.key?(:stash)
54
+ @state[:stash][name_s] = data[:stash]
55
+ end
56
+
57
+ box = ::CacheBox::Box.new(
58
+ namespace: @namespace_o.dup,
59
+ name: name_o.dup,
60
+ args: args,
61
+ input: input,
62
+ stash: @state[:stash][name] || ::CacheBox::Stash.new,
63
+ logger: @logger
64
+ )
65
+ end
66
+
67
+ begin
68
+ result = block.call(box)
69
+
70
+ LOCK.synchronize do
71
+ @state[:result][name_s] = result
72
+ end
73
+ ensure
74
+ LOCK.synchronize do
75
+ if @state[:result].key?(name_s)
76
+ @state[:stash].delete(name_s)
77
+ @storage.write!(name_s, { result: @state[:result][name_s] })
78
+ else
79
+ stash = box.stash
80
+ stash = ::CacheBox::Stash.new(stash) if stash.is_a?(Hash)
81
+
82
+ stash.with do |state|
83
+ dump = { stash: state }
84
+
85
+ @state[:stash][name_s] = stash
86
+ @storage.write!(name_s, dump)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Input:
94
+ #
95
+ # name = String | Symbol # Default: 'name'
96
+ #
97
+ # Output: true | false
98
+ def has?(name = nil)
99
+ name ||= 'name'
100
+ validate_string_or_symbol!(name, 'name')
101
+ name = name.to_s
102
+
103
+ LOCK.synchronize do
104
+ return true if @state[:result].key?(name)
105
+
106
+ data = @storage.read!(name)
107
+
108
+ if data&.key?(:result)
109
+ @state[:result][name] = data[:result]
110
+ elsif data&.key?(:stash)
111
+ @state[:stash][name] = data[:stash]
112
+ end
113
+
114
+ @state[:result].key?(name)
115
+ end
116
+ end
117
+
118
+ # Input:
119
+ #
120
+ # name = String | Symbol # Default: 'name'
121
+ #
122
+ # Output: true | false
123
+ def result(name = nil)
124
+ name ||= 'name'
125
+ validate_string_or_symbol!(name, 'name')
126
+ name = name.to_s
127
+
128
+ LOCK.synchronize do
129
+ return @state[:result][name] if @state[:result].key?(name)
130
+
131
+ data = @storage.read!(name)
132
+
133
+ if data&.key?(:result)
134
+ @state[:result][name] = data[:result]
135
+ elsif data&.key?(:stash)
136
+ @state[:stash][name] = data[:stash]
137
+ end
138
+
139
+ @state[:result][name]
140
+ end
141
+ end
142
+
143
+ # Input:
144
+ #
145
+ # name = Array[String | Symbol] # Default: []
146
+ #
147
+ # Output: self
148
+ def expire!(*names)
149
+ validate_array_of_string_or_symbol!(names, 'names')
150
+
151
+ if names.empty?
152
+ LOCK.synchronize do
153
+ @state = { result: ::CacheBox::Stash.new, stash: ::CacheBox::Stash.new }
154
+
155
+ @storage.reset!
156
+ end
157
+ else
158
+ LOCK.synchronize do
159
+ names.each do |name_o|
160
+ name_s = name_o.to_s
161
+
162
+ @state[:result].delete(name_s)
163
+ @state[:stash].delete(name_s)
164
+
165
+ @storage.delete!(name_s)
166
+ end
167
+ end
168
+ end
169
+
170
+ self
171
+ end
172
+
173
+ # Input:
174
+ #
175
+ # name = Array[String | Symbol] # Default: []
176
+ #
177
+ # Output: self
178
+ def clear(*names)
179
+ validate_array_of_string_or_symbol!(names, 'names')
180
+
181
+ if names.empty?
182
+ LOCK.synchronize do
183
+ @state = { result: ::CacheBox::Stash.new, stash: ::CacheBox::Stash.new }
184
+ end
185
+ else
186
+ LOCK.synchronize do
187
+ names.each do |name_o|
188
+ name_s = name_o.to_s
189
+
190
+ @state[:result].delete(name_s)
191
+ @state[:stash].delete(name_s)
192
+ end
193
+ end
194
+ end
195
+
196
+ self
197
+ end
198
+ end
199
+ end