« get me outta code hell

homepage layout data - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2022-02-01 16:23:16 -0400
committer(quasar) nebula <qznebula@protonmail.com>2022-02-01 16:23:16 -0400
commite17bf53b3f17d8d8aa62869d4fc97883acc5c1fa (patch)
tree541fab09213494f34c3e859a018ef88ea5be1575
parente64d210cf0a798ae99bec89ca37cffcc1599411d (diff)
homepage layout data
-rw-r--r--src/thing/homepage-layout.js99
-rw-r--r--src/thing/validators.js7
-rwxr-xr-xsrc/upd8.js143
-rw-r--r--test/data-validators.js11
4 files changed, 183 insertions, 77 deletions
diff --git a/src/thing/homepage-layout.js b/src/thing/homepage-layout.js
new file mode 100644
index 0000000..4717391
--- /dev/null
+++ b/src/thing/homepage-layout.js
@@ -0,0 +1,99 @@
+import CacheableObject from './cacheable-object.js';
+
+import {
+    isColor,
+    isCountingNumber,
+    isName,
+    isString,
+    oneOf,
+    validateArrayItems,
+    validateInstanceOf,
+    validateReference,
+    validateReferenceList,
+} from './validators.js';
+
+export class HomepageLayoutRow extends CacheableObject {
+    static propertyDescriptors = {
+        // Update & expose
+
+        name: {
+            flags: {update: true, expose: true},
+            update: {validate: isName}
+        },
+
+        type: {
+            flags: {update: true, expose: true},
+
+            update: {
+                validate(value) {
+                    throw new Error(`'type' property validator must be overridden`);
+                }
+            }
+        },
+
+        color: {
+            flags: {update: true, expose: true},
+            update: {validate: isColor}
+        },
+    };
+}
+
+export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
+    static propertyDescriptors = {
+        ...HomepageLayoutRow.propertyDescriptors,
+
+        // Update & expose
+
+        type: {
+            flags: {update: true, expose: true},
+            update: {
+                validate(value) {
+                    if (value !== 'albums') {
+                        throw new TypeError(`Expected 'albums'`);
+                    }
+
+                    return true;
+                }
+            }
+        },
+
+        sourceGroupByRef: {
+            flags: {update: true, expose: true},
+            update: {validate: validateReference('group')}
+        },
+
+        sourceAlbumsByRef: {
+            flags: {update: true, expose: true},
+            update: {validate: validateReferenceList('album')}
+        },
+
+        countAlbumsFromGroup: {
+            flags: {update: true, expose: true},
+            update: {validate: isCountingNumber}
+        },
+
+        actionLinks: {
+            flags: {update: true, expose: true},
+            update: {validate: validateArrayItems(isString)}
+        },
+    }
+}
+
+export default class HomepageLayout extends CacheableObject {
+    static propertyDescriptors = {
+        // Update & expose
+
+        sidebarContent: {
+            flags: {update: true, expose: true},
+            update: {validate: isString}
+        },
+
+        rows: {
+            flags: {update: true, expose: true},
+
+            update: {
+                validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow))
+            }
+        },
+    };
+}
diff --git a/src/thing/validators.js b/src/thing/validators.js
index a465e9d..4946347 100644
--- a/src/thing/validators.js
+++ b/src/thing/validators.js
@@ -74,6 +74,13 @@ export function isInteger(number) {
     return true;
 }
 
+export function isCountingNumber(number) {
+    isInteger(number);
+    isPositive(number);
+
+    return true;
+}
+
 export function isString(value) {
     return isType(value, 'string');
 }
diff --git a/src/upd8.js b/src/upd8.js
index 9c86c26..0f60802 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -95,6 +95,9 @@ import Album, { TrackGroup } from './thing/album.js';
 import Artist from './thing/artist.js';
 import Flash, { FlashAct } from './thing/flash.js';
 import Group, { GroupCategory } from './thing/group.js';
+import HomepageLayout, {
+    HomepageLayoutAlbumsRow,
+} from './thing/homepage-layout.js';
 import Thing from './thing/thing.js';
 import Track from './thing/track.js';
 
@@ -198,7 +201,7 @@ const CACHEBUST = 7;
 
 // MAKE THESE END IN YAML
 const WIKI_INFO_FILE = 'wiki-info.txt';
-const HOMEPAGE_INFO_FILE = 'homepage.txt';
+const HOMEPAGE_LAYOUT_FILE = 'homepage.yaml';
 const ARTIST_DATA_FILE = 'artists.yaml';
 const FLASH_DATA_FILE = 'flashes.yaml';
 const NEWS_DATA_FILE = 'news.txt';
@@ -750,6 +753,10 @@ function makeProcessDocument(thingClass, {
     // yet implemented as part of a Thing's data model!
     ignoredFields = []
 }) {
+    if (!propertyFieldMapping) {
+        throw new Error(`Expected propertyFieldMapping to be provided`);
+    }
+
     const knownFields = Object.values(propertyFieldMapping);
 
     // Invert the property-field mapping, since it'll come in handy for
@@ -1367,72 +1374,50 @@ async function processWikiInfoFile(file) {
     };
 }
 
-async function processHomepageInfoFile(file) {
-    let contents;
-    try {
-        contents = await readFile(file, 'utf-8');
-    } catch (error) {
-        return {error: `Could not read ${file} (${error.code}).`};
-    }
-
-    const contentLines = splitLines(contents);
-    const sections = Array.from(getSections(contentLines));
-
-    const [ firstSection, ...rowSections ] = sections;
-
-    const sidebar = getMultilineField(firstSection, 'Sidebar');
+const processHomepageLayoutDocument = makeProcessDocument(HomepageLayout, {
+    propertyFieldMapping: {
+        sidebarContent: 'Sidebar Content'
+    },
 
-    const validRowTypes = ['albums'];
+    ignoredFields: ['Homepage']
+});
 
-    const rows = rowSections.map(section => {
-        const name = getBasicField(section, 'Row');
-        if (!name) {
-            return {error: 'Expected "Row" (name) field!'};
-        }
+const homepageLayoutRowBaseSpec = {
+};
 
-        const color = getBasicField(section, 'Color');
+const makeProcessHomepageLayoutRowDocument = (rowClass, spec) => makeProcessDocument(rowClass, {
+    ...spec,
 
-        const type = getBasicField(section, 'Type');
-        if (!type) {
-            return {error: 'Expected "Type" field!'};
-        }
+    propertyFieldMapping: {
+        name: 'Row',
+        color: 'Color',
+        type: 'Type',
+        ...spec.propertyFieldMapping,
+    }
+});
 
-        if (!validRowTypes.includes(type)) {
-            return {error: `Expected "Type" field to be one of: ${validRowTypes.join(', ')}`};
+const homepageLayoutRowTypeProcessMapping = {
+    albums: makeProcessHomepageLayoutRowDocument(HomepageLayoutAlbumsRow, {
+        propertyFieldMapping: {
+            sourceGroupByRef: 'Group',
+            countAlbumsFromGroup: 'Count',
+            sourceAlbumsByRef: 'Albums',
+            actionLinks: 'Actions'
         }
+    })
+};
 
-        const row = {name, color, type};
-
-        switch (type) {
-            case 'albums': {
-                const group = getBasicField(section, 'Group') || null;
-                const albums = getListField(section, 'Albums') || [];
-
-                if (!group && !albums) {
-                    return {error: 'Expected "Group" and/or "Albums" field!'};
-                }
-
-                let groupCount = getBasicField(section, 'Count');
-                if (group && !groupCount) {
-                    return {error: 'Expected "Count" field!'};
-                }
-
-                if (groupCount) {
-                    if (isNaN(parseInt(groupCount))) {
-                        return {error: `Invalid Count field: "${groupCount}"`};
-                    }
-
-                    groupCount = parseInt(groupCount);
-                }
+function processHomepageLayoutRowDocument(document) {
+    const type = document['Type'];
 
-                const actions = getListField(section, 'Actions') || [];
+    const match = Object.entries(homepageLayoutRowTypeProcessMapping)
+        .find(([ key ]) => key === type);
 
-                return {...row, group, groupCount, albums, actions};
-            }
-        }
-    });
+    if (!match) {
+        throw new TypeError(`No processDocument function for row type ${type}!`);
+    }
 
-    return {sidebar, rows};
+    return match[1](document);
 }
 
 function getDurationInSeconds(string) {
@@ -2400,23 +2385,6 @@ async function main() {
     } else {
         languages.default = defaultStrings;
     }
-
-    WD.homepageInfo = await processHomepageInfoFile(path.join(dataPath, HOMEPAGE_INFO_FILE));
-
-    if (WD.homepageInfo.error) {
-        console.log(`\x1b[31;1m${WD.homepageInfo.error}\x1b[0m`);
-        return;
-    }
-
-    {
-        const errors = WD.homepageInfo.rows.filter(obj => obj.error);
-        if (errors.length) {
-            for (const error of errors) {
-                console.log(`\x1b[31;1m${error.error}\x1b[0m`);
-            }
-            return;
-        }
-    }
     */
 
     // 8ut wait, you might say, how do we know which al8um these data files
@@ -2528,7 +2496,7 @@ async function main() {
         },
 
         {
-            title: `Process artist file`,
+            title: `Process artists file`,
             files: [path.join(dataPath, ARTIST_DATA_FILE)],
 
             documentMode: documentModes.allInOne,
@@ -2541,7 +2509,7 @@ async function main() {
 
         // TODO: WD.wikiInfo.features.flashesAndGames &&
         {
-            title: `Process flash file`,
+            title: `Process flashes file`,
             files: [path.join(dataPath, FLASH_DATA_FILE)],
 
             documentMode: documentModes.allInOne,
@@ -2582,7 +2550,7 @@ async function main() {
         },
 
         {
-            title: `Process group file`,
+            title: `Process groups file`,
             files: [path.join(dataPath, GROUP_DATA_FILE)],
 
             documentMode: documentModes.allInOne,
@@ -2621,6 +2589,25 @@ async function main() {
                 wikiData.groupCategoryData = results.filter(x => x instanceof GroupCategory);
             }
         },
+
+        {
+            title: `Process homepage layout file`,
+            files: [path.join(dataPath, HOMEPAGE_LAYOUT_FILE)],
+
+            documentMode: documentModes.headerAndEntries,
+            processHeaderDocument: processHomepageLayoutDocument,
+            processEntryDocument: processHomepageLayoutRowDocument,
+
+            save(results) {
+                if (!results[0]) {
+                    return;
+                }
+
+                const { header: homepageLayout, entries: rows } = results[0];
+                Object.assign(homepageLayout, {rows});
+                Object.assign(wikiData, {homepageLayout});
+            }
+        },
     ];
 
     const processDataAggregate = openAggregate({message: `Errors processing data files`});
@@ -2765,6 +2752,8 @@ async function main() {
             if (wikiData.flashData)
                 logInfo` - ${wikiData.flashData.length} flashes (${wikiData.flashActData.length} acts)`;
             logInfo` - ${wikiData.groupData.length} groups (${wikiData.groupCategoryData.length} categories)`;
+            if (wikiData.homepageLayout)
+                logInfo` - ${1} homepage layout (${wikiData.homepageLayout.rows.length} rows)`;
         } catch (error) {
             console.error(`Error showing data summary:`, error);
         }
diff --git a/test/data-validators.js b/test/data-validators.js
index feec127..739333a 100644
--- a/test/data-validators.js
+++ b/test/data-validators.js
@@ -4,6 +4,7 @@ import { showAggregate } from '../src/util/sugar.js';
 import {
     // Basic types
     isBoolean,
+    isCountingNumber,
     isNumber,
     isString,
     isStringNonEmpty,
@@ -60,6 +61,16 @@ test('isNumber', t => {
     t.throws(() => isNumber(true), TypeError);
 });
 
+test('isCountingNumber', t => {
+    t.plan(6);
+    t.ok(isCountingNumber(3));
+    t.ok(isCountingNumber(1));
+    t.throws(() => isCountingNumber(1.75), TypeError);
+    t.throws(() => isCountingNumber(0), TypeError);
+    t.throws(() => isCountingNumber(-1), TypeError);
+    t.throws(() => isCountingNumber('612'), TypeError);
+});
+
 test('isString', t => {
     t.plan(3);
     t.ok(isString('hello!'));