1 /**
2  * Wrappers for automatically loading/saving data.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.persistence;
15 
16 import std.traits;
17 
18 enum FlushPolicy
19 {
20 	none,
21 	manual,
22 	atScopeExit,
23 	atThreadExit,
24 	// TODO: immediate flushing. Could work only with values without mutable indirections.
25 	// TODO: this can actually be a bitmask
26 }
27 
28 bool delayed(FlushPolicy policy) { return policy > FlushPolicy.manual; }
29 
30 struct None {}
31 
32 /// Cache values in-memory, and automatically load/save them as needed via the specified functions.
33 /// Actual loading/saving is done via alias functions.
34 /// KeyGetter may return .init (of its return type) if the resource does not yet exist,
35 /// but once it returns non-.init it may not return .init again.
36 /// A bool key can be used to load a resource from disk only once (lazily),
37 /// as is currently done with LoadPolicy.once.
38 /// Delayed flush policies require a bool key, to avoid mid-air collisions.
39 mixin template CacheCore(alias DataGetter, alias KeyGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none)
40 {
41 	import std.traits;
42 	import ae.sys.memory;
43 
44 	alias _CacheCore_Data = ReturnType!DataGetter;
45 	alias _CacheCore_Key  = ReturnType!KeyGetter;
46 
47 	enum _CacheCore_readOnly = flushPolicy == FlushPolicy.none;
48 
49 	_CacheCore_Data cachedData;
50 	_CacheCore_Key cachedDataKey;
51 
52 	void _CacheCore_update()
53 	{
54 		auto newKey = KeyGetter();
55 
56 		// No going back to Key.init after returning non-.init
57 		assert(cachedDataKey == _CacheCore_Key.init || newKey != _CacheCore_Key.init);
58 
59 		if (newKey != cachedDataKey)
60 		{
61 			static if (flushPolicy == FlushPolicy.atThreadExit)
62 			{
63 				if (cachedDataKey == _CacheCore_Key.init) // first load
64 					_CacheCore_registerFlush();
65 			}
66 			cachedData = DataGetter();
67 			cachedDataKey = newKey;
68 		}
69 	}
70 
71 	static if (_CacheCore_readOnly)
72 		@property     auto _CacheCore_data() { _CacheCore_update(); return cast(immutable)cachedData; }
73 	else
74 		@property ref auto _CacheCore_data() { _CacheCore_update(); return                cachedData; }
75 
76 	static if (!_CacheCore_readOnly)
77 	{
78 		void save(bool exiting=false)()
79 		{
80 			if (cachedDataKey != _CacheCore_Key.init || cachedData != _CacheCore_Data.init)
81 			{
82 				DataPutter(cachedData);
83 				static if (!exiting)
84 					cachedDataKey = KeyGetter();
85 			}
86 		}
87 
88 		static if (flushPolicy.delayed())
89 		{
90 			// A bool key implies that data will be loaded only once (lazy loading).
91 			static assert(is(_CacheCore_Key==bool), "Delayed flush with automatic reload allows mid-air collisions");
92 		}
93 
94 		static if (flushPolicy == FlushPolicy.atScopeExit)
95 		{
96 			~this()
97 			{
98 				save!true();
99 			}
100 		}
101 
102 		static if (flushPolicy == FlushPolicy.atThreadExit)
103 		{
104 			void _CacheCore_registerFlush()
105 			{
106 				// https://d.puremagic.com/issues/show_bug.cgi?id=12038
107 				assert(!onStack(cast(void*)&this));
108 				_CacheCore_pending ~= &this;
109 			}
110 
111 			static typeof(this)*[] _CacheCore_pending;
112 
113 			static ~this()
114 			{
115 				foreach (p; _CacheCore_pending)
116 					p.save!true();
117 			}
118 		}
119 	}
120 }
121 
122 /// FileCache policy for when to (re)load data from disk.
123 enum LoadPolicy
124 {
125 	automatic, /// "onModification" for FlushPolicy.none/manual, "once" for delayed
126 	once,
127 	onModification,
128 }
129 
130 struct FileCache(alias DataGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none, LoadPolicy loadPolicy = LoadPolicy.automatic)
131 {
132 	string fileName;
133 
134 	static if (loadPolicy == LoadPolicy.automatic)
135 		enum _FileCache_loadPolicy = flushPolicy.delayed() ? LoadPolicy.once : LoadPolicy.onModification;
136 	else
137 		enum _FileCache_loadPolicy = loadPolicy;
138 
139 	ReturnType!DataGetter _FileCache_dataGetter()
140 	{
141 		import std.file : exists;
142 		assert(fileName, "Filename not set");
143 		static if (flushPolicy == FlushPolicy.none)
144 			return DataGetter(fileName); // no existence checks if we are never saving it ourselves
145 		else
146 		if (fileName.exists)
147 			return DataGetter(fileName);
148 		else
149 			return ReturnType!DataGetter.init;
150 	}
151 
152 	static if (is(DataPutter == None))
153 		alias _FileCache_dataPutter = None;
154 	else
155 		void _FileCache_dataPutter(T)(T t)
156 		{
157 			assert(fileName, "Filename not set");
158 			DataPutter(fileName, t);
159 		}
160 
161 	static if (_FileCache_loadPolicy == LoadPolicy.onModification)
162 	{
163 		import std.datetime : SysTime;
164 
165 		SysTime _FileCache_keyGetter()
166 		{
167 			import std.file  : exists, timeLastModified;
168 
169 			SysTime result;
170 			if (fileName.exists)
171 				result = fileName.timeLastModified();
172 			return result;
173 		}
174 	}
175 	else
176 	{
177 		bool _FileCache_keyGetter() { return true; }
178 	}
179 
180 	mixin CacheCore!(_FileCache_dataGetter, _FileCache_keyGetter, _FileCache_dataPutter, flushPolicy);
181 
182 	alias _CacheCore_data this;
183 }
184 
185 // Sleep between writes to make sure timestamps differ
186 version(unittest) import core.thread;
187 
188 unittest
189 {
190 	import std.file;
191 
192 	enum FN = "test.txt";
193 	auto cachedData = FileCache!read(FN);
194 
195 	std.file.write(FN, "One");
196 	scope(exit) remove(FN);
197 	assert(cachedData == "One");
198 
199 	Thread.sleep(10.msecs);
200 	std.file.write(FN, "Two");
201 	assert(cachedData == "Two");
202 	auto mtime = FN.timeLastModified();
203 
204 	Thread.sleep(10.msecs);
205 	std.file.write(FN, "Three");
206 	FN.setTimes(mtime, mtime);
207 	assert(cachedData == "Two");
208 }
209 
210 // ****************************************************************************
211 
212 template JsonFileCache(T, FlushPolicy flushPolicy = FlushPolicy.none)
213 {
214 	import std.file;
215 	import ae.utils.json;
216 
217 	static T getJson(T)(string fileName)
218 	{
219 		return fileName.readText.jsonParse!T;
220 	}
221 
222 	static void putJson(T)(string fileName, in T t)
223 	{
224 		std.file.write(fileName, t.toJson());
225 	}
226 
227 	alias JsonFileCache = FileCache!(getJson!T, putJson!T, flushPolicy);
228 }
229 
230 unittest
231 {
232 	import std.file;
233 
234 	enum FN = "test1.json";
235 	std.file.write(FN, "{}");
236 	scope(exit) remove(FN);
237 
238 	auto cache = JsonFileCache!(string[string])(FN);
239 	assert(cache.length == 0);
240 }
241 
242 unittest
243 {
244 	import std.file;
245 
246 	enum FN = "test2.json";
247 	scope(exit) if (FN.exists) remove(FN);
248 
249 	auto cache = JsonFileCache!(string[string], FlushPolicy.manual)(FN);
250 	assert(cache.length == 0);
251 	cache["foo"] = "bar";
252 	cache.save();
253 
254 	auto cache2 = JsonFileCache!(string[string])(FN);
255 	assert(cache2["foo"] == "bar");
256 }
257 
258 unittest
259 {
260 	import std.file;
261 
262 	enum FN = "test3.json";
263 	scope(exit) if (FN.exists) remove(FN);
264 
265 	{
266 		auto cache = JsonFileCache!(string[string], FlushPolicy.atScopeExit)(FN);
267 		cache["foo"] = "bar";
268 	}
269 
270 	auto cache2 = JsonFileCache!(string[string])(FN);
271 	assert(cache2["foo"] == "bar");
272 }
273 
274 // ****************************************************************************
275 
276 // http://d.puremagic.com/issues/show_bug.cgi?id=7016
277 static import ae.utils.json;
278 
279 /// std.functional.memoize variant with automatic persistence
280 struct PersistentMemoized(alias fun, FlushPolicy flushPolicy = FlushPolicy.atThreadExit)
281 {
282 	import std.traits;
283 	import std.typecons;
284 	import ae.utils.json;
285 
286 	alias ReturnType!fun[string] AA;
287 	private JsonFileCache!(AA, flushPolicy) memo;
288 
289 	this(string fileName) { memo.fileName = fileName; }
290 
291 	ReturnType!fun opCall(ParameterTypeTuple!fun args)
292 	{
293 		string key;
294 		static if (args.length==1 && is(typeof(args[0]) : string))
295 			key = args[0];
296 		else
297 			key = toJson(tuple(args));
298 		auto p = key in memo;
299 		if (p) return *p;
300 		auto r = fun(args);
301 		return memo[key] = r;
302 	}
303 }
304 
305 unittest
306 {
307 	import std.file;
308 
309 	static int value = 42;
310 	int getValue(int x) { return value; }
311 
312 	enum FN = "test4.json";
313 	scope(exit) if (FN.exists) remove(FN);
314 
315 	{
316 		auto getValueMemoized = PersistentMemoized!(getValue, FlushPolicy.atScopeExit)(FN);
317 
318 		assert(getValueMemoized(1) == 42);
319 		value = 24;
320 		assert(getValueMemoized(1) == 42);
321 		assert(getValueMemoized(2) == 24);
322 	}
323 
324 	value = 0;
325 
326 	{
327 		auto getValueMemoized = PersistentMemoized!(getValue, FlushPolicy.atScopeExit)(FN);
328 		assert(getValueMemoized(1) == 42);
329 		assert(getValueMemoized(2) == 24);
330 	}
331 }
332 
333 // ****************************************************************************
334 
335 // http://d.puremagic.com/issues/show_bug.cgi?id=7016
336 static import ae.sys.file;
337 
338 /// A string hashset, stored one line per entry.
339 struct PersistentStringSet
340 {
341 	import ae.utils.aa : HashSet;
342 
343 	static HashSet!string load(string fileName)
344 	{
345 		import std.file : readText;
346 		import std.string : splitLines;
347 
348 		return HashSet!string(fileName.readText().splitLines());
349 	}
350 
351 	static void save(string fileName, HashSet!string data)
352 	{
353 		import std.array : join;
354 		import ae.sys.file : atomicWrite;
355 
356 		atomicWrite(fileName, data.keys.join("\n"));
357 	}
358 
359 	alias Cache = FileCache!(load, save, FlushPolicy.manual);
360 	Cache cache;
361 
362 	this(string fileName) { cache = Cache(fileName); }
363 
364 	auto opIn_r(string key)
365 	{
366 		return key in cache;
367 	}
368 
369 	void add(string key)
370 	{
371 		assert(key !in cache);
372 		cache.add(key);
373 		cache.save();
374 	}
375 
376 	void remove(string key)
377 	{
378 		assert(key in cache);
379 		cache.remove(key);
380 		cache.save();
381 	}
382 
383 	@property string[] lines() { return cache.keys; }
384 	@property size_t length() { return cache.length; }
385 }
386 
387 unittest
388 {
389 	import std.file;
390 
391 	enum FN = "test.txt";
392 	scope(exit) if (FN.exists) remove(FN);
393 
394 	{
395 		auto s = PersistentStringSet(FN);
396 		assert("foo" !in s);
397 		assert(s.length == 0);
398 		s.add("foo");
399 	}
400 	{
401 		auto s = PersistentStringSet(FN);
402 		assert("foo" in s);
403 		assert(s.length == 1);
404 		s.remove("foo");
405 	}
406 	{
407 		auto s = PersistentStringSet(FN);
408 		assert("foo" !in s);
409 		std.file.write(FN, "foo\n");
410 		assert("foo" in s);
411 		std.file.write(FN, "bar\n");
412 		assert(s.lines == ["bar"]);
413 	}
414 }