You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/service/Canonical.ts

161 lines
4.9 KiB

3 years ago
/**
* Base type for a canonical definition.
*/
import {Canon} from "./Canon";
import {universalPath, UniversalPath, ErrorWithContext} from "@extollo/util";
3 years ago
import {Logging} from "./Logging";
import {Inject} from "@extollo/di";
import * as nodePath from 'path'
import {Unit} from "../lifecycle/Unit";
export interface CanonicalDefinition {
canonicalName: string,
originalName: string,
imported: any,
}
export type CanonicalResolver<T> = (key: string) => T | undefined
3 years ago
/**
* Base type for a canonical name reference.
*/
export interface CanonicalReference {
resource?: string,
item: string,
particular?: string,
}
export abstract class Canonical<T> extends Unit {
@Inject()
protected readonly logging!: Logging
@Inject()
protected readonly canon!: Canon
/**
* The base path directory where the canonical definitions reside.
* @type string
*/
protected appPath: string[] = []
/**
* The file suffix of files in the base path that should be loaded.
* @type string
*/
protected suffix: string = '.js'
/**
* The singular, programmatic name of one of these canonical items.
* @example middleware
* @type string
*/
protected canonicalItem: string = ''
/**
* Object mapping canonical names to loaded file references.
* @type object
*/
protected loadedItems: { [key: string]: T } = {}
/**
* Object mapping canonical namespaces to resolver functions.
* @protected
*/
protected loadedNamespaces: { [key: string]: CanonicalResolver<T> } = {}
3 years ago
/**
* Resolve a canonical reference from its string form to a CanonicalReference.
* @param {string} reference
* @return CanonicalReference
*/
public static resolve(reference: string): CanonicalReference {
const rscParts = reference.split('::')
const resource = rscParts.length > 1 ? rscParts[0] + 's' : undefined
const rsc_less = rscParts.length > 1 ? rscParts[1] : rscParts[0]
const prtParts = rsc_less.split('.')
const item = prtParts[0]
const particular = prtParts.length > 1 ? prtParts.slice(1).join('.') : undefined
return {
resource, item, particular
}
}
public all(): string[] {
return Object.keys(this.loadedItems)
}
public get path(): UniversalPath {
return this.app().appPath(...this.appPath)
}
public get canonicalItems() {
return `${this.canonicalItem}s`
}
public get(key: string): T | undefined {
if ( key.startsWith('@') ) {
const [namespace, ...rest] = key.split(':')
key = rest.join(':')
if ( !this.loadedNamespaces[namespace] ) {
throw new ErrorWithContext(`Unable to find namespace for ${this.canonicalItem}: ${namespace}`, {
canonicalItem: this.canonicalItem,
namespace,
key,
})
}
return this.loadedNamespaces[namespace](key)
}
3 years ago
return this.loadedItems[key]
}
public registerNamespace(name: string, resolver: CanonicalResolver<T>) {
if ( !name.startsWith('@') ) {
throw new ErrorWithContext(`Canonical namespaces must start with @.`, { name })
}
if ( this.loadedNamespaces[name] ) {
this.logging.warn(`Replacing canonical namespace resolver for: ${name}`)
}
this.loadedNamespaces[name] = resolver
}
3 years ago
public async up() {
for await ( const entry of this.path.walk() ) {
if ( !entry.endsWith(this.suffix) ) {
this.logging.debug(`Skipping file with invalid suffix: ${entry}`)
continue
}
const definition = await this.buildCanonicalDefinition(entry)
this.logging.verbose(`Registering canonical ${this.canonicalItem} "${definition.canonicalName}" from ${entry}`)
this.loadedItems[definition.canonicalName] = await this.initCanonicalItem(definition)
}
this.canon.registerCanonical(this)
}
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]]
3 years ago
}
protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {
const originalName = filePath.replace(this.path.toLocal, '').substr(1)
const pathRegex = new RegExp(nodePath.sep, 'g')
const canonicalName = originalName.replace(pathRegex, ':')
.split('').reverse().join('')
.substr(this.suffix.length)
.split('').reverse().join('')
const fullUniversalPath = universalPath(filePath)
this.logging.verbose(`Importing from: ${fullUniversalPath}`)
const imported = await import(fullUniversalPath.toLocal)
return { canonicalName, originalName, imported }
}
}