• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
Aucun tag

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

CLI interface to medialist (fossil mirror)


Commit MetaInfo

Révision9e9949aab6d811a10c7cb508db4051b82708548c (tree)
l'heure2022-01-10 10:43:36
Auteurmio <stigma@disr...>
Commitermio

Message de Log

add the initial version of medialist.d (the library version of medialist)

FossilOrigin-Name: 89e126b74a95c36ce6a4b7274bdfca8c4688aad5ad8d2e55bde4ece2ddbef649

Change Summary

Modification

--- /dev/null
+++ b/medialist.d
@@ -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+}