CLI interface to medialist (fossil mirror)
Révision | 9e9949aab6d811a10c7cb508db4051b82708548c (tree) |
---|---|
l'heure | 2022-01-10 10:43:36 |
Auteur | mio <stigma@disr...> |
Commiter | mio |
add the initial version of medialist.d (the library version of medialist)
FossilOrigin-Name: 89e126b74a95c36ce6a4b7274bdfca8c4688aad5ad8d2e55bde4ece2ddbef649
@@ -0,0 +1,512 @@ | ||
1 | +module medialist; | |
2 | + | |
3 | +import std.datetime.date; | |
4 | +import std.datetime.systime; | |
5 | +import std.file; | |
6 | +import std.stdio; | |
7 | +import std.string; | |
8 | + | |
9 | +struct MediaList | |
10 | +{ | |
11 | +package: | |
12 | + string filePath; | |
13 | + bool isOpen = false; | |
14 | +} | |
15 | + | |
16 | +enum MLCommand | |
17 | +{ | |
18 | + /** | |
19 | + * Args: ["Item Name", "(Optional) Progress", "(Optional) Status". | |
20 | + */ | |
21 | + add, | |
22 | + delete_, | |
23 | + update, | |
24 | +} | |
25 | + | |
26 | +enum MLError | |
27 | +{ | |
28 | + success, | |
29 | + invalidArgs, | |
30 | + fileDoesNotExist, | |
31 | + fileAlreadyOpen, | |
32 | +} | |
33 | + | |
34 | +MediaList* ml_open_list(string filePath) | |
35 | +{ | |
36 | + MediaList* ml = new MediaList(filePath); | |
37 | + | |
38 | + if (false == exists(filePath)) { | |
39 | + File f = File(filePath, "w+"); | |
40 | + | |
41 | + ml.isOpen = true; | |
42 | + | |
43 | + f.writeln("# This file is in the mTSV format."); | |
44 | + f.writeln("# For more information about this format,"); | |
45 | + f.writeln("# please view the website:"); | |
46 | + f.writeln("# http://yume-neru.neocities.org/p/mtsv.html"); | |
47 | + f.writeln("title\tprogress\tstatus\tstart_date\tend_date\tlast_updated"); | |
48 | + | |
49 | + ml.isOpen = false; | |
50 | + } | |
51 | + | |
52 | + return ml; | |
53 | +} | |
54 | + | |
55 | +/** | |
56 | + * Finalize and free the resources held by the MediaList structure. | |
57 | + * | |
58 | + * Params: | |
59 | + * list = The MediaList structure. | |
60 | + * | |
61 | + * See_Also: ml_open_list | |
62 | + */ | |
63 | +void ml_free_list(MediaList* list) | |
64 | +{ | |
65 | + import core.memory : GC; | |
66 | + | |
67 | + destroy(list); | |
68 | + GC.free(list); | |
69 | +} | |
70 | + | |
71 | +MLError ml_send_command(MediaList* list, MLCommand command, string[] args) | |
72 | +{ | |
73 | + MLError res; | |
74 | + | |
75 | + switch (command) | |
76 | + { | |
77 | + case MLCommand.add: | |
78 | + res = _ml_add(list, args); | |
79 | + break; | |
80 | + case MLCommand.delete_: | |
81 | + break; | |
82 | + case MLCommand.update: | |
83 | + break; | |
84 | + default: | |
85 | + assert(0); | |
86 | + } | |
87 | + | |
88 | + return MLError.success; | |
89 | +} | |
90 | + | |
91 | +private MLError _ml_add(MediaList* list, string[] args) | |
92 | +{ | |
93 | + string title; | |
94 | + string progress = "-/-"; | |
95 | + string status = "UNKNOWN"; | |
96 | + | |
97 | + DateTime currentDate = cast(DateTime)Clock.currTime; | |
98 | + | |
99 | + if (args.length < 1) | |
100 | + return MLError.invalidArgs; | |
101 | + | |
102 | + title = args[0]; | |
103 | + | |
104 | + if (args.length >= 2) | |
105 | + progress = (args[1] is null) ? "-/-" : args[1]; | |
106 | + | |
107 | + if (args.length >= 3) | |
108 | + status = (args[2] is null) ? "UNKNOWN" : args[2]; | |
109 | + | |
110 | + if (true == list.isOpen) | |
111 | + return MLError.fileAlreadyOpen; | |
112 | + | |
113 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
114 | + int currentIndent = 0; | |
115 | + | |
116 | + list.isOpen = true; | |
117 | + scope(exit) list.isOpen = false; | |
118 | + | |
119 | + File listFile = File(list.filePath, "a"); | |
120 | + | |
121 | + foreach(const ref int[2] headerPosition; headerPositions) { | |
122 | + while (currentIndent < headerPosition[1]) { | |
123 | + listFile.write("\t"); | |
124 | + currentIndent += 1; | |
125 | + } | |
126 | + | |
127 | + switch(headerPosition[0]) | |
128 | + { | |
129 | + case MLHeaders.title: | |
130 | + listFile.write(title); | |
131 | + break; | |
132 | + case MLHeaders.progress: | |
133 | + listFile.write(progress); | |
134 | + break; | |
135 | + case MLHeaders.status: | |
136 | + listFile.write(status); | |
137 | + break; | |
138 | + case MLHeaders.lastUpdated: | |
139 | + listFile.writef("%d-%02d-%02d", currentDate.year, currentDate.month, | |
140 | + currentDate.day); | |
141 | + break; | |
142 | + default: | |
143 | + break; | |
144 | + } | |
145 | + } | |
146 | + | |
147 | + listFile.write("\n"); | |
148 | + | |
149 | + return MLError.success; | |
150 | +} | |
151 | + | |
152 | +private enum MLHeaders | |
153 | +{ | |
154 | + title = 0, | |
155 | + progress = 1, | |
156 | + status = 2, | |
157 | + startDate = 3, | |
158 | + endDate = 4, | |
159 | + lastUpdated = 5 | |
160 | +} | |
161 | + | |
162 | +private int[2][6] _ml_get_header_positions(MediaList* list) | |
163 | +{ | |
164 | + list.isOpen = true; | |
165 | + scope(exit) list.isOpen = false; | |
166 | + | |
167 | + File f = File(list.filePath); | |
168 | + string line; | |
169 | + | |
170 | + /* | |
171 | + * [ | |
172 | + * [ MLHeaders, tabIndent ], | |
173 | + * [ MLHeaders, tabIndent ], | |
174 | + * [ MLHeaders, tabIndent ], | |
175 | + * [...] | |
176 | + * ] | |
177 | + * | |
178 | + * We keep the tab indent so we don't mess up any other programs or custom | |
179 | + * configurations. | |
180 | + */ | |
181 | + int[2][6] headerPositions = -1; | |
182 | + int arrayPosition = 0; | |
183 | + | |
184 | + while ((line = f.readln) !is null) { | |
185 | + /* skip configuration and comments */ | |
186 | + if (line[0] == '#') | |
187 | + continue; | |
188 | + | |
189 | + /* first non-comment/non-configuration line is the header */ | |
190 | + string[] sections = line.strip().split("\t"); | |
191 | + | |
192 | + foreach(int idx, ref string section; sections) { | |
193 | + switch(section.toLower) | |
194 | + { | |
195 | + case "title": | |
196 | + headerPositions[arrayPosition] = [MLHeaders.title, idx]; | |
197 | + arrayPosition += 1; | |
198 | + break; | |
199 | + case "progress": | |
200 | + headerPositions[arrayPosition] = [MLHeaders.progress, idx]; | |
201 | + arrayPosition += 1; | |
202 | + break; | |
203 | + case "status": | |
204 | + headerPositions[arrayPosition] = [MLHeaders.status, idx]; | |
205 | + arrayPosition += 1; | |
206 | + break; | |
207 | + case "start_date": | |
208 | + headerPositions[arrayPosition] = [MLHeaders.startDate, idx]; | |
209 | + arrayPosition += 1; | |
210 | + break; | |
211 | + case "end_date": | |
212 | + headerPositions[arrayPosition] = [MLHeaders.endDate, idx]; | |
213 | + arrayPosition += 1; | |
214 | + break; | |
215 | + case "last_updated": | |
216 | + headerPositions[arrayPosition] = [MLHeaders.lastUpdated, idx]; | |
217 | + arrayPosition += 1; | |
218 | + break; | |
219 | + default: | |
220 | + break; | |
221 | + } | |
222 | + | |
223 | + if (arrayPosition > MLHeaders.max) | |
224 | + break; | |
225 | + } | |
226 | + } | |
227 | + | |
228 | + return headerPositions; | |
229 | +} | |
230 | + | |
231 | + | |
232 | +/* | |
233 | + * The rest of this file consists of unittests. | |
234 | + */ | |
235 | + | |
236 | +version(unittest) | |
237 | +{ | |
238 | + | |
239 | +template Tuple(T...) | |
240 | +{ | |
241 | + alias Tuple = T; | |
242 | +} | |
243 | + | |
244 | +shared static this() | |
245 | +{ | |
246 | + import core.runtime; | |
247 | + | |
248 | + /* Don't run the actual unittests :O */ | |
249 | + Runtime.moduleUnitTester = { return true; }; | |
250 | +} | |
251 | + | |
252 | +void main() | |
253 | +{ | |
254 | + alias tests = Tuple!(__traits(getUnitTests, medialist)); | |
255 | + writefln("1..%d", tests.length); | |
256 | + | |
257 | + | |
258 | + foreach(i, test; tests) { | |
259 | + alias attributes = Tuple!(__traits(getAttributes, tests[i])); | |
260 | + | |
261 | + try { | |
262 | + test(); | |
263 | + writefln("ok %d - %s", i + 1, attributes[0]); | |
264 | + } catch (Throwable t) { | |
265 | + writefln("not ok %d - %s", i + 1, attributes[0]); | |
266 | + auto lines = t.msg.splitLines; | |
267 | + foreach(line; lines) { | |
268 | + writefln(" %s", line); | |
269 | + } | |
270 | + } | |
271 | + } | |
272 | +} | |
273 | + | |
274 | +} | |
275 | + | |
276 | +@("Create a new list") | |
277 | +unittest | |
278 | +{ | |
279 | + import std.conv : to; | |
280 | + import std.path : buildPath; | |
281 | + | |
282 | + const listPath = buildPath(tempDir(), "unittest1.tsv"); | |
283 | + scope(exit) remove(listPath); | |
284 | + | |
285 | + MediaList* list = ml_open_list(listPath); | |
286 | + scope(exit) ml_free_list(list); | |
287 | + | |
288 | + assert(null !is list, "Memory allocation failed (list is null)"); | |
289 | + assert(true == exists(listPath), "A new file wasn't created for the list"); | |
290 | + | |
291 | + int[2][6] headerPositions = -1; | |
292 | + | |
293 | + headerPositions = _ml_get_header_positions(list); | |
294 | + | |
295 | + foreach(size_t idx, const ref int[2] headerPosition; headerPositions) { | |
296 | + assert(headerPosition[0] != -1, | |
297 | + "Empty header type: " ~ to!string(idx)); | |
298 | + assert(headerPosition[1] != -1, | |
299 | + "Empty header position: " ~ to!string(idx)); | |
300 | + } | |
301 | +} | |
302 | + | |
303 | +@("Can add a new item with no progress or status") | |
304 | +unittest | |
305 | +{ | |
306 | + import std.path : buildPath; | |
307 | + | |
308 | + const listPath = buildPath(tempDir(), "unittest2.tsv"); | |
309 | + scope(exit) remove(listPath); | |
310 | + | |
311 | + MediaList* list = ml_open_list(listPath); | |
312 | + scope(exit) ml_free_list(list); | |
313 | + | |
314 | + MLError res = ml_send_command(list, MLCommand.add, ["Item 1"]); | |
315 | + assert(res == MLError.success); | |
316 | + | |
317 | + auto f = File(listPath); | |
318 | + string line; | |
319 | + bool readHeader = false; | |
320 | + string[] sections; | |
321 | + | |
322 | + while ((line = f.readln) !is null) { | |
323 | + if (line[0] == '#') | |
324 | + continue; | |
325 | + | |
326 | + if (false == readHeader) { | |
327 | + readHeader = true; | |
328 | + continue; | |
329 | + } | |
330 | + | |
331 | + sections = line.strip().split("\t"); | |
332 | + break; | |
333 | + } | |
334 | + | |
335 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
336 | + | |
337 | + assert(sections.length == MLHeaders.max + 1, | |
338 | + "Differing amount of headers in unittest files."); | |
339 | + | |
340 | + assert(sections[headerPositions[MLHeaders.title][1]] == "Item 1", | |
341 | + "New item with title 'Item 1' wasn't saved"); | |
342 | + assert(sections[headerPositions[MLHeaders.progress][1]] == "-/-", | |
343 | + "New item with unspecified progress was not \"-/-\""); | |
344 | + assert(sections[headerPositions[MLHeaders.status][1]] == "UNKNOWN", | |
345 | + "New item with unspecified status was not \"UNKNOWN\""); | |
346 | + | |
347 | + assert(sections[headerPositions[MLHeaders.startDate][1]] == ""); | |
348 | + assert(sections[headerPositions[MLHeaders.endDate][1]] == ""); | |
349 | + | |
350 | + DateTime currentDT = cast(DateTime)Clock.currTime; | |
351 | + string currentDS = format!"%d-%02d-%02d"(currentDT.year, | |
352 | + currentDT.month, currentDT.day); | |
353 | + assert(sections[headerPositions[MLHeaders.lastUpdated][1]] == currentDS); | |
354 | +} | |
355 | + | |
356 | +@("Can add a new item with progress but no status") | |
357 | +unittest | |
358 | +{ | |
359 | + import std.path : buildPath; | |
360 | + | |
361 | + const listPath = buildPath(tempDir(), "unittest3.tsv"); | |
362 | + scope(exit) remove(listPath); | |
363 | + | |
364 | + MediaList* list = ml_open_list(listPath); | |
365 | + scope(exit) ml_free_list(list); | |
366 | + | |
367 | + MLError res = ml_send_command(list, MLCommand.add, ["Item 1", "10/-"]); | |
368 | + assert(res == MLError.success); | |
369 | + | |
370 | + auto f = File(listPath); | |
371 | + string line; | |
372 | + bool readHeader = false; | |
373 | + string[] sections; | |
374 | + | |
375 | + while ((line = f.readln) !is null) { | |
376 | + if (line[0] == '#') | |
377 | + continue; | |
378 | + | |
379 | + if (false == readHeader) { | |
380 | + readHeader = true; | |
381 | + continue; | |
382 | + } | |
383 | + | |
384 | + sections = line.strip().split("\t"); | |
385 | + } | |
386 | + | |
387 | + assert(sections.length == MLHeaders.max + 1, | |
388 | + "Differing amount of headers in unittest files."); | |
389 | + | |
390 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
391 | + | |
392 | + assert(sections[headerPositions[MLHeaders.title][1]] == "Item 1", | |
393 | + "New item with title 'Item 1' wasn't saved"); | |
394 | + assert(sections[headerPositions[MLHeaders.progress][1]] == "10/-", | |
395 | + "New item with progress '10/-' wasn't saved"); | |
396 | + assert(sections[headerPositions[MLHeaders.status][1]] == "UNKNOWN", | |
397 | + "New item with unspecified status was not \"UNKNOWN\""); | |
398 | + | |
399 | + assert(sections[headerPositions[MLHeaders.startDate][1]] == ""); | |
400 | + assert(sections[headerPositions[MLHeaders.endDate][1]] == ""); | |
401 | + | |
402 | + DateTime currentDT = cast(DateTime)Clock.currTime; | |
403 | + string currentDS = format!"%d-%02d-%02d"(currentDT.year, | |
404 | + currentDT.month, currentDT.day); | |
405 | + assert(sections[headerPositions[MLHeaders.lastUpdated][1]] == currentDS); | |
406 | +} | |
407 | + | |
408 | +@("Can add a new item with status but no progress") | |
409 | +unittest | |
410 | +{ | |
411 | + import std.path : buildPath; | |
412 | + | |
413 | + const listPath = buildPath(tempDir(), "unittest3.tsv"); | |
414 | + scope(exit) remove(listPath); | |
415 | + | |
416 | + MediaList* list = ml_open_list(listPath); | |
417 | + scope(exit) ml_free_list(list); | |
418 | + | |
419 | + MLError res = ml_send_command(list, MLCommand.add, | |
420 | + ["Item 1", null, "COMPLETE"]); | |
421 | + assert(res == MLError.success); | |
422 | + | |
423 | + auto f = File(listPath); | |
424 | + string line; | |
425 | + bool readHeader = false; | |
426 | + string[] sections; | |
427 | + | |
428 | + while ((line = f.readln) !is null) { | |
429 | + if (line[0] == '#') | |
430 | + continue; | |
431 | + | |
432 | + if (false == readHeader) { | |
433 | + readHeader = true; | |
434 | + continue; | |
435 | + } | |
436 | + | |
437 | + sections = line.strip().split("\t"); | |
438 | + } | |
439 | + | |
440 | + assert(sections.length == MLHeaders.max + 1, | |
441 | + "Differing amount of headers in unittest files."); | |
442 | + | |
443 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
444 | + | |
445 | + assert(sections[headerPositions[MLHeaders.title][1]] == "Item 1", | |
446 | + "New item with title 'Item 1' wasn't saved"); | |
447 | + assert(sections[headerPositions[MLHeaders.progress][1]] == "-/-", | |
448 | + "New item with unspecified progress wasn't \"-/-\""); | |
449 | + assert(sections[headerPositions[MLHeaders.status][1]] == "COMPLETE", | |
450 | + "New item with progress \"COMPLETE\" wasn't saved"); | |
451 | + | |
452 | + assert(sections[headerPositions[MLHeaders.startDate][1]] == ""); | |
453 | + assert(sections[headerPositions[MLHeaders.endDate][1]] == ""); | |
454 | + | |
455 | + DateTime currentDT = cast(DateTime)Clock.currTime; | |
456 | + string currentDS = format!"%d-%02d-%02d"(currentDT.year, | |
457 | + currentDT.month, currentDT.day); | |
458 | + assert(sections[headerPositions[MLHeaders.lastUpdated][1]] == currentDS); | |
459 | +} | |
460 | + | |
461 | +@("Can add a new item with progress and status") | |
462 | +unittest | |
463 | +{ | |
464 | + import std.path : buildPath; | |
465 | + | |
466 | + const listPath = buildPath(tempDir(), "unittest3.tsv"); | |
467 | + scope(exit) remove(listPath); | |
468 | + | |
469 | + MediaList* list = ml_open_list(listPath); | |
470 | + scope(exit) ml_free_list(list); | |
471 | + | |
472 | + MLError res = ml_send_command(list, MLCommand.add, | |
473 | + ["Item 1", "10/-", "COMPLETE"]); | |
474 | + assert(res == MLError.success); | |
475 | + | |
476 | + auto f = File(listPath); | |
477 | + string line; | |
478 | + bool readHeader = false; | |
479 | + string[] sections; | |
480 | + | |
481 | + while ((line = f.readln) !is null) { | |
482 | + if (line[0] == '#') | |
483 | + continue; | |
484 | + | |
485 | + if (false == readHeader) { | |
486 | + readHeader = true; | |
487 | + continue; | |
488 | + } | |
489 | + | |
490 | + sections = line.strip().split("\t"); | |
491 | + } | |
492 | + | |
493 | + assert(sections.length == MLHeaders.max + 1, | |
494 | + "Differing amount of headers in unittest files."); | |
495 | + | |
496 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
497 | + | |
498 | + assert(sections[headerPositions[MLHeaders.title][1]] == "Item 1", | |
499 | + "New item with title 'Item 1' wasn't saved"); | |
500 | + assert(sections[headerPositions[MLHeaders.progress][1]] == "10/-", | |
501 | + "New item with progress '10/-' wasn't saved"); | |
502 | + assert(sections[headerPositions[MLHeaders.status][1]] == "COMPLETE", | |
503 | + "New item with progress \"COMPLETE\" wasn't saved"); | |
504 | + | |
505 | + assert(sections[headerPositions[MLHeaders.startDate][1]] == ""); | |
506 | + assert(sections[headerPositions[MLHeaders.endDate][1]] == ""); | |
507 | + | |
508 | + DateTime currentDT = cast(DateTime)Clock.currTime; | |
509 | + string currentDS = format!"%d-%02d-%02d"(currentDT.year, | |
510 | + currentDT.month, currentDT.day); | |
511 | + assert(sections[headerPositions[MLHeaders.lastUpdated][1]] == currentDS); | |
512 | +} |