Tree classes

We now have async tree wrappers for a specific object, folder, or function — but let’s generalize that code to create classes to wrap any object, folder, or function.

These classes package up our existing code, adding a constructor to accept the thing we want to wrap. Any code that we’ve written to work with async trees, like our json utility, will already accept instances of these classes, as the classes support the necessary AsyncTree interface methods.

These classes do not inherit from some shared base class. Doing so is possible but would be awkward, as these classes have substantially different constructor parameters. By defining an async tree as an interface instead of a base class, we retain more flexibility than using a class hierarchy.

Object tree class

This class accepts a plain JavaScript object and returns a corresponding async tree:

/* src/classes/ObjectTree.js */

export default class ObjectTree {
  constructor(obj) {
    this.obj = obj;
  }

  async get(key) {
    return this.obj[key];
  }

  async keys() {
    return Object.keys(this.obj);
  }
}

We can redefine our existing object.js to use this class:

/* src/classes/object.js */

import ObjectTree from "./ObjectTree.js";

export default new ObjectTree({
  "Alice.md": "Hello, **Alice**.",
  "Bob.md": "Hello, **Bob**.",
  "Carol.md": "Hello, **Carol**.",
});

We can verify that this object passes our tests, can be displayed with our json utility, and can be used as the basis for our markdown-to-HTML transformation.

File tree class

The class for a files-based tree takes a directory path as input.

/* src/classes/FileTree.js */

import * as fs from "node:fs/promises";
import path from "node:path";

export default class FileTree {
  constructor(dirname) {
    this.dirname = path.resolve(process.cwd(), dirname);
  }

  async get(key) {
    const fileName = path.resolve(this.dirname, key);
    try {
      return await fs.readFile(fileName); // Return file contents
    } catch (/** @type {any} */ error) {
      if (error.code === "ENOENT" /* File not found */) {
        return undefined;
      }
      throw error;
    }
  }

  async keys() {
    return fs.readdir(this.dirname);
  }
}

Function tree class

The constructor for a function-based tree takes a function and an array as a function domain.

/* src/classes/FunctionTree.js */

export default class FunctionTree {
  constructor(fn, domain) {
    this.fn = fn;
    this.domain = domain;
  }

  async get(key) {
    return this.fn(key);
  }

  async keys() {
    return this.domain;
  }
}

With these classes, we can quickly create new trees based on any object, folder, or function.

Test the classes

Run all the tests to confirm that the class-based trees all behave as expected.

$ cd ../classes
$ node --test# tests 4
# pass 4
# fail 0

All tests pass, so now we have three different general ways to implement a tree that all work the same way. The markdown-to-HTML transformation doesn’t need any modification to work with these new class-based trees, as it can already work with anything implementing the AsyncTree interface.

 

Next: Deep trees »