pmcf
Version:
Poor mans configuration management
659 lines (555 loc) • 14.5 kB
JavaScript
import { join } from "node:path";
import { allOutputs } from "npm-pkgbuild";
import {
parse,
expand,
tokens,
baseTypes,
attributeIterator,
name_attribute_writable,
string_attribute,
string_attribute_writable,
string_collection_attribute_writable,
number_attribute_writable,
description_attribute,
boolean_attribute_writable
} from "pacc";
import { addType, typeFactory } from "./types.mjs";
import { asArray } from "./utils.mjs";
const BaseTypeDefinition = {
name: "base",
owners: [],
attributes: {
owner: { type: "base", collection: false, writable: false },
type: string_attribute,
name: name_attribute_writable,
description: { ...description_attribute, writable: true },
priority: number_attribute_writable,
directory: string_attribute_writable,
packaging: string_attribute_writable,
disabled: boolean_attribute_writable,
tags: string_collection_attribute_writable
}
};
/**
*
*/
export class Base {
owner;
description;
name;
_tags = new Set();
_packaging = new Set();
_directory;
_finalize;
_properties;
static {
addType(this);
}
static get typeName() {
return this.typeDefinition.name;
}
static get typeDefinition() {
return BaseTypeDefinition;
}
static get typeFileName() {
return this.typeName + ".json";
}
static get fileNameGlob() {
return "**/" + this.typeFileName;
}
constructor(owner, data) {
this.owner = owner;
switch (typeof data) {
case "string":
this.name = data;
break;
case "object":
this.read(data, BaseTypeDefinition);
}
}
ownerFor(property, data) {
for (const type of property.type[0].owners) {
if (this.typeName === type?.name) {
return this;
}
}
for (const type of property.type[0].owners) {
const owner = this[type?.name];
if (owner) {
return owner;
}
}
return this;
}
read(data, type = this.constructor.typeDefinition) {
if (type.extends) {
this.read(data, type.extends);
}
const assign = (name, attribute, value) => {
value ??= attribute.default;
if (value !== undefined) {
if (attribute.values) {
if (attribute.values.indexOf(value) < 0) {
this.error(name, "unknown value", value, attribute.values);
}
}
if (attribute.collection) {
const current = this[name];
switch (typeof current) {
case "undefined":
this[name] = asArray(value);
break;
case "object":
if (Array.isArray(current)) {
current.push(value);
} else {
if (current instanceof Set) {
// TODO
this[name] = value;
} else if (current instanceof Map) {
// TODO
this[name] = value;
} else {
this.error("Unknown collection type", name, current);
}
}
break;
case "function":
if (value instanceof Base) {
this.addObject(value);
} else {
this.error("Unknown collection type", name, current);
}
break;
}
} else {
this[name] = value;
}
}
};
const instantiateAndAssign = (name, attribute, value) => {
if (baseTypes.has(attribute.type[0])) {
assign(name, attribute, value);
return;
}
switch (typeof value) {
case "undefined":
return;
case "function":
this.error("Invalid value", name, value);
break;
case "boolean":
case "bigint":
case "number":
case "string":
{
let object;
for (const type of attribute.type) {
object = this.typeNamed(type.name, value);
if (object) {
break;
}
}
if (object) {
assign(name, attribute, object);
} else {
if (attribute.type[0].constructWithIdentifierOnly) {
object = new attribute.type[0].clazz(
this.ownerFor(attribute, value),
value
);
object.read(value);
this.addObject(object);
} else {
this.finalize(() => {
value = this.expand(value);
for (const type of attribute.type) {
const object =
this.typeNamed(type.name, value) ||
this.owner.typeNamed(type.name, value) ||
this.root.typeNamed(type.name, value); // TODO
if (object) {
assign(name, attribute, object);
return;
}
}
this.error(
"Not found",
name,
attribute.type.map(t => t.name),
value
);
});
}
}
}
break;
case "object":
if (value instanceof attribute.type[0].clazz) {
assign(name, attribute, value);
} else {
assign(
name,
attribute,
typeFactory(
attribute.type[0],
this.ownerFor(attribute, value),
value
)
);
}
break;
}
};
if (data?.properties) {
this._properties = data.properties;
}
for (const [path, attribute] of attributeIterator(type.attributes)) {
if (attribute.writable) {
const name = path.join(".");
const value = this.expand(data[name]);
if (attribute.collection) {
if (typeof value === "object") {
if (Array.isArray(value)) {
for (const v of value) {
instantiateAndAssign(name, attribute, v);
}
} else {
if (value instanceof Base) {
assign(name, attribute, value);
} else {
for (const [objectName, objectData] of Object.entries(value)) {
if (typeof objectData === "object") {
objectData[type.identifier.name] = objectName;
}
instantiateAndAssign(name, attribute, objectData);
}
}
}
continue;
}
}
instantiateAndAssign(name, attribute, value);
}
}
}
named(name) {}
typeNamed(typeName, name) {
if (this.owner) {
const object = this.owner.typeNamed(typeName, name); // TODO split
if (object) {
return object;
}
}
const object = this.named(name);
if (object?.typeName === typeName) {
return object;
}
}
addObject(object) {
return this.owner.addObject(object);
}
forOwner(owner) {
if (this.owner !== owner) {
const newObject = Object.create(this);
newObject.owner = owner;
return newObject;
}
return this;
}
isNamed(name) {
return name[0] === "/" ? this.fullName === name : this.name === name;
}
relativeName(name) {
return name?.[0] === "/"
? name.substring(this.owner.fullName.length + 1)
: name;
}
get typeName() {
// @ts-ignore
return this.constructor.typeDefinition.name;
}
/**
* @return {Iterable<Base>}
*/
get extends() {
return [];
}
*_extendedPropertyIterator(propertyName, seen) {
if (!seen.has(this)) {
seen.add(this);
const value = this[propertyName];
if (value !== undefined) {
yield value;
}
for (const e of this.extends) {
yield* e._extendedPropertyIterator(propertyName, seen);
}
}
}
_extendedProperty(propertyName, seen) {
if (!seen.has(this)) {
seen.add(this);
for (const e of this.extends) {
const value =
e[propertyName] ?? e._extendedProperty(propertyName, seen);
if (value !== undefined) {
return value;
}
}
}
}
extendedProperty(propertyName) {
const value = this[propertyName];
if (value !== undefined) {
return value;
}
return this._extendedProperty(propertyName, new Set());
}
get root() {
return this.owner.root;
}
get location() {
return this.owner?.location;
}
get host() {
return this.owner?.host;
}
get network() {
return this.owner?.network;
}
get domain() {
return this.owner?.domain;
}
get domains() {
return this.owner?.domains ?? new Set();
}
get localDomains() {
return this.owner?.localDomains ?? new Set();
}
get administratorEmail() {
return this.owner?.administratorEmail;
}
get locales() {
return this.owner?.locales;
}
get country() {
return this.owner?.country;
}
get timezone() {
return this.owner?.timezone;
}
set priority(value) {
this._priority = value;
}
get priority() {
return this._priority ?? this.owner?.priority;
}
get smtp() {
return this.findService({ type: "smtp" });
}
/**
*
* @param {any} filter
* @returns service with the highest priority
*/
findService(filter) {
let best;
for (const service of this.findServices(filter)) {
if (!best || service.priority > best.priority) {
best = service;
}
}
return best;
}
*findServices(filter) {
if (this.owner) {
yield* this.owner.findServices(filter);
}
}
set directory(directory) {
this._directory = directory;
}
get directory() {
return this._directory ?? join(this.owner.directory, this.name);
}
get fullName() {
return this.name
? join(this.owner.fullName, "/", this.name)
: this.owner.fullName;
}
set packaging(value) {
this._packaging.add(value);
}
get derivedPackaging() {
return this.owner?.packaging;
}
get packaging() {
const dp = this.derivedPackaging;
if (dp) {
return this._packaging.union(dp);
}
return this._packaging;
}
get outputs() {
return new Set(allOutputs.filter(o => this.packaging.has(o.name)));
}
async *preparePackages(stagingDir) {}
get tags() {
return this._tags;
}
set tags(value) {
if (value instanceof Set) {
this._tags = this._tags.union(value);
} else {
this._tags.add(value);
}
}
get isTemplate() {
return false;
}
get properties() {
return this._properties;
}
property(name) {
return this._properties?.[name] ?? this.owner?.property(name);
}
expand(object) {
if (this.isTemplate || object instanceof Base) {
return object;
}
const context = {
stopClass: Base,
root: this,
globals: Object.assign({}, this.properties, this.owner.properties)
};
return expand(object, context);
}
finalize(action) {
if (!this._finalize) {
this._finalize = [];
}
this._finalize.push(action);
}
execFinalize() {
this.traverse(object => object._execFinalize());
}
_execFinalize() {
if (this._finalize) {
let i = 0;
for (const action of this._finalize) {
if (action) {
this._finalize[i] = undefined;
action();
}
i++;
}
}
}
traverse(visitor, ...args) {
const visited = new Set();
this._traverse(visited, visitor, ...args);
return visited;
}
_traverse(visited, visitor, ...args) {
if (visited.has(this)) {
return false;
}
visited.add(this);
visitor(this, ...args);
return true;
}
error(...args) {
console.error(`${this.toString()}:`, ...args);
}
info(...args) {
console.info(`${this.toString()}:`, ...args);
}
toString() {
return `${this.fullName}(${this.typeName})`;
}
toJSON() {
return extractFrom(this, this.constructor.typeDefinition);
}
}
export function extractFrom(
object,
typeDefinition = object?.constructor?.typeDefinition
) {
switch (typeof object) {
case "undefined":
case "string":
case "number":
case "boolean":
return object;
}
if (typeof object[Symbol.iterator] === "function") {
object = [...object];
if (object.length === 0) {
return undefined;
}
if (typeDefinition?.identifier) {
return Object.fromEntries(
object.map(o => {
o = extractFrom(o);
const name = o[typeDefinition.identifier.name];
delete o[typeDefinition.identifier.name];
return [name, o];
})
);
}
return object.length ? object : undefined;
}
const json = {};
do {
for (const [name, def] of Object.entries(typeDefinition.attributes)) {
let value = object[name];
switch (typeof value) {
case "function":
{
value = object[name]();
if (typeof value?.next === "function") {
value = [...value];
}
value = extractFrom(value, def.type[0]);
if (value !== undefined) {
json[name] = value;
}
}
break;
case "object":
if (value instanceof Base) {
json[name] = { type: value.typeName };
if (value.name) {
json[name].name = value.name;
}
} else {
if (typeof value[Symbol.iterator] === "function") {
value = extractFrom(value);
if (value !== undefined) {
json[name] = value;
}
} else {
const resultObject = Object.fromEntries(
Object.entries(value).map(([k, v]) => [
k,
v // extractFrom(v, def.type)
])
);
if (Object.keys(resultObject).length > 0) {
json[name] = resultObject;
}
}
}
break;
case "undefined":
break;
default:
json[name] = value;
}
}
typeDefinition = typeDefinition?.extends;
} while (typeDefinition);
return json;
}