Skip to content

Commit

Permalink
Convert definitions to TypeScript (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
brantburnett authored Aug 27, 2021
1 parent 2bb003b commit ef84ab8
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 316 deletions.
13 changes: 9 additions & 4 deletions app/configuration/index-validation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import _ from "lodash";
import { IndexConfigurationBase } from "./types";

export interface IndexValidatorSet {
[key: string]: (this: IndexConfigurationBase, val: any) => void;
post_validate: (this: IndexConfigurationBase) => void;
export type ValidatorSetBase<T> = {
[key in keyof T]?: (this: T, val: any) => void;
}

export interface ValidatorSetPostValidate<T> {
post_validate?: (this: T) => void;
}

export type ValidatorSet<T> = ValidatorSetBase<T> & ValidatorSetPostValidate<T>;

/**
* Validators for the incoming index properties.
*/
export const IndexValidators: IndexValidatorSet = {
export const IndexValidators: ValidatorSet<IndexConfigurationBase> = {
is_primary: function(val) {
if (val !== undefined && !_.isBoolean(val)) {
throw new Error('is_primary must be a boolean');
Expand Down
7 changes: 2 additions & 5 deletions app/configuration/node-map-validation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { forOwn, isObjectLike, isString } from "lodash";
import { ValidatorSet } from "./index-validation";
import { NodeMapConfiguration } from "./types";

export interface NodeMapValidatorSet {
[key: string]: (this: NodeMapConfiguration, val: any) => void;
}

/**
* Validators for the incoming node map properties.
*/
export const NodeMapValidators: NodeMapValidatorSet = {
export const NodeMapValidators: ValidatorSet<NodeMapConfiguration> = {
map: function(map) {
if (!isObjectLike(map)) {
throw new Error('Invalid node map');
Expand Down
30 changes: 21 additions & 9 deletions app/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ export enum ConfigurationType {
NodeMap = "nodeMap",
}

export interface ConfigurationItem {
type?: ConfigurationType;
}

export interface Lifecycle {
drop?: boolean;
}
Expand All @@ -22,7 +18,10 @@ export interface Partition {
num_partition?: number;
}

export type PostProcessHandler = (this: IndexConfigurationBase, require: NodeRequire, process: NodeJS.Process) => void;

export interface IndexConfigurationBase {
name?: string;
is_primary?: boolean;
index_key?: string | string[];
condition?: string;
Expand All @@ -32,19 +31,32 @@ export interface IndexConfigurationBase {
nodes?: string[];
retain_deleted_xattr?: boolean;
lifecycle?: Lifecycle;
post_process?: string | PostProcessHandler;
}

export interface IndexConfiguration extends IndexConfigurationBase {
export interface IndexConfiguration extends Exclude<IndexConfigurationBase, "post_process"> {
type: ConfigurationType.Index;
name: string;
}

export interface OverrideConfiguration extends IndexConfigurationBase {
type: ConfigurationType.NodeMap;
post_process?: string | (() => void);
type: ConfigurationType.Override;
}

export interface NodeMapConfiguration extends ConfigurationItem {
export interface NodeMapConfiguration {
type: ConfigurationType.NodeMap;
map: { [key: string]: string }
}

export type ConfigurationItem = IndexConfiguration | OverrideConfiguration | NodeMapConfiguration;

export function isIndex(item: ConfigurationItem): item is IndexConfiguration {
return item.type === ConfigurationType.Index;
}

export function isOverride(item: ConfigurationItem): item is OverrideConfiguration {
return item.type === ConfigurationType.Override;
}

export function isNodeMap(item: ConfigurationItem): item is NodeMapConfiguration {
return item.type === ConfigurationType.NodeMap;
}
144 changes: 61 additions & 83 deletions app/definition-loader.js → app/definition/definition-loader.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,66 @@
import chalk from 'chalk';
import fs from 'fs';
import { forOwn, isObjectLike } from 'lodash';
import yaml from 'js-yaml';
import { isObjectLike } from 'lodash';
import path from 'path';
import util from 'util';
import yaml from 'js-yaml';
import { ConfigurationItem, ConfigurationType, IndexConfigurationBase, IndexValidators, isIndex, isNodeMap, isOverride, NodeMapConfiguration, NodeMapValidators, ValidatorSet } from '../configuration';
import { Logger } from '../options';
import { IndexDefinition } from './index-definition';
import { NodeMap } from './node-map';
import { ConfigurationType, IndexValidators, NodeMapValidators } from './configuration';

const lstat = util.promisify(fs.lstat);
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);

const INDEX_EXTENSIONS = ['.json', '.yaml', '.yml'];

/**
* Set of validators, keyed by type and then by property. Each validator
* may throw an exception if that property is in error. First parameter
* is the value of that property. "this" will be the definition.
*
* May also include a "post_validate" validator, which is called last
* and without a parameter. This validator is used to validate the object
* as a whole, to ensure property values compatible with each other.
*/
const validatorSets = {
[ConfigurationType.NodeMap]: NodeMapValidators,
[ConfigurationType.Index]: IndexValidators,
};
type ConfigItemHandler = (item: ConfigurationItem) => void;

function validateDefinition<T>(validatorSet: ValidatorSet<T>, definition: T) {
let key: keyof ValidatorSet<T>;
for (key in validatorSet) {
try {
if (key !== 'post_validate') {
validatorSet[key].call(definition, definition[key]);
}
} catch (e) {
throw new Error(
`${e} in ${(definition as any).name || 'unk'}.${key}`);
}
}

// Don't perform post_validate step on overrides, as overrides
// don't have the full set of properties.
if (validatorSet.post_validate && !isOverride(definition as unknown as ConfigurationItem)) {
try {
validatorSet.post_validate.call(definition);
} catch (e) {
throw new Error(
`${e} in ${(definition as any).name || 'unk'}`);
}
}
}

/**
* Loads index definitions from disk or stdin
*/
export class DefinitionLoader {
/**
* @param {any} [logger]
*/
constructor(logger) {
private logger: Logger;

constructor(logger?: Logger) {
this.logger = logger || console;
}

/**
* Loads definitions from disk
*
* @param {array.string} paths
* @return {{definitions: array.IndexDefinition, nodeMap: NodeMap}}
*/
async loadDefinitions(paths) {
let definitions = [];
let nodeMap = new NodeMap();
async loadDefinitions(paths: string[]): Promise<{ definitions: IndexDefinition[], nodeMap: NodeMap }> {
const definitions: IndexDefinition[] = [];
const nodeMap = new NodeMap();

let files = [];
for (let filePath of paths) {
for (const filePath of paths) {
if (filePath === '-') {
// read from stdin
await this.loadFromStdIn(
Expand All @@ -61,8 +71,8 @@ export class DefinitionLoader {

try {
if ((await lstat(filePath)).isDirectory()) {
let filesInDir = await readdir(filePath);
let joinedFilesInDir = filesInDir.map(
const filesInDir = await readdir(filePath);
const joinedFilesInDir = filesInDir.map(
(fileName) => path.join(filePath, fileName));

files.push(...joinedFilesInDir);
Expand Down Expand Up @@ -90,31 +100,24 @@ export class DefinitionLoader {
}

/**
* @private
* Loads index definitions from a file
*
* @param {string} filename File to read
* @param {function(*)} handler Handler for loaded definitions
*/
async loadDefinition(filename, handler) {
let ext = path.extname(filename).toLowerCase();
let contents = await readFile(filename, 'utf8');
private async loadDefinition(filename: string, handler: ConfigItemHandler): Promise<void> {
const ext = path.extname(filename).toLowerCase();
const contents = await readFile(filename, 'utf8');

if (ext === '.json') {
handler(JSON.parse(contents));
} else if (ext === '.yaml' || ext === '.yml') {
yaml.safeLoadAll(contents, handler);
yaml.loadAll(contents, handler);
}
}

/**
* @private
* Loads index definitions from stdin
*
* @param {function(IndexDefinition)} handler Handler for loaded definitions
* @return {Promise}
*/
loadFromStdIn(handler) {
private loadFromStdIn(handler: ConfigItemHandler): Promise<void> {
return new Promise((resolve, reject) => {
process.stdin.resume();
process.stdin.setEncoding('utf8');
Expand All @@ -131,7 +134,7 @@ export class DefinitionLoader {
handler(JSON.parse(data));
} else {
// Assume it's YAML
yaml.safeLoadAll(data, handler);
yaml.loadAll(data, handler);
}

resolve();
Expand All @@ -143,30 +146,25 @@ export class DefinitionLoader {
}

/**
* @private
* Processes a definition and adds to the current definitions or
* applies overrides to the matching definition
*
* @param {array.IndexDefinition} definitions Current definitions
* @param {NodeMap} nodeMap Current node map
* @param {*} definition New definition to process
*/
processDefinition(definitions, nodeMap, definition) {
let match = definitions.find((p) => p.name === definition.name);
private processDefinition(definitions: IndexDefinition[], nodeMap: NodeMap, definition: ConfigurationItem): void {
const match = definitions.find((p) => p.name === (definition as any).name);

if (definition.type === undefined) {
definition.type = ConfigurationType.Index;
}

this.validateDefinition(definition, match);

if (definition.type === ConfigurationType.NodeMap) {
if (isNodeMap(definition)) {
if (definition.map && isObjectLike(definition.map)) {
nodeMap.merge(definition.map);
} else {
throw new Error('Invalid nodeMap');
}
} else if (definition.type === ConfigurationType.Override) {
} else if (isOverride(definition)) {
// Override definition
if (match) {
match.applyOverride(definition);
Expand All @@ -176,7 +174,7 @@ export class DefinitionLoader {
chalk.yellowBright(
`No index definition found '${definition.name}'`));
}
} else if (definition.type === ConfigurationType.Index) {
} else if (isIndex(definition)) {
// Regular index definition

if (match) {
Expand All @@ -186,52 +184,32 @@ export class DefinitionLoader {

definitions.push(new IndexDefinition(definition));
} else {
throw new Error(`Unknown definition type '${definition.type}'`);
throw new Error(`Unknown definition type '${(definition as any).type}'`);
}
}

/**
* @private
* Validates a definition based on its type, throwing an exception
* if there is a problem.
*
* @param {*} definition
* @param {IndexDefinition} [match]
* Existing index definition of the same name, if any
*/
validateDefinition(definition, match) {
private validateDefinition(definition: ConfigurationItem, match?: IndexDefinition) {
let effectiveType = definition.type;
if (effectiveType === 'override' && match) {
if (effectiveType === ConfigurationType.Override && match) {
// Use validators based on the type of definition being overriden

if (match instanceof IndexDefinition) {
effectiveType = 'index';
effectiveType = ConfigurationType.Index;
}
}

const validatorSet = validatorSets[effectiveType];
if (validatorSet) {
forOwn(validatorSet, (validator, key) => {
try {
if (key !== 'post_validate') {
validator.call(definition, definition[key]);
}
} catch (e) {
throw new Error(
`${e} in ${definition.name || 'unk'}.${key}`);
}
});
switch (effectiveType) {
case ConfigurationType.Index:
validateDefinition(IndexValidators, definition as IndexConfigurationBase);
break;

// Don't perform post_validate step on overrides, as overrides
// don't have the full set of properties.
if (validatorSet.post_validate && definition.type !== 'override') {
try {
validatorSet.post_validate.call(definition);
} catch (e) {
throw new Error(
`${e} in ${definition.name || 'unk'}`);
}
}
case ConfigurationType.NodeMap:
validateDefinition(NodeMapValidators, definition as NodeMapConfiguration);
break;
}
}
}
14 changes: 14 additions & 0 deletions app/definition/index-definition-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { isString } from 'lodash';
import { IndexConfiguration } from '../configuration';

export abstract class IndexDefinitionBase {
name: string;

constructor(configuration: IndexConfiguration) {
if (!configuration.name || !isString(configuration.name)) {
throw new Error('Index definition does not have a \'name\'');
}

this.name = configuration.name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import chaiThings from 'chai-things';
import sinonChai from 'sinon-chai';
import {stub} from 'sinon';
import {IndexDefinition} from './index-definition';
import {UpdateIndexMutation} from './plan/update-index-mutation';
import {CreateIndexMutation} from './plan/create-index-mutation';
import {MoveIndexMutation} from './plan/move-index-mutation';
import {ResizeIndexMutation} from './plan/resize-index-mutation';
import {UpdateIndexMutation} from '../plan/update-index-mutation';
import {CreateIndexMutation} from '../plan/create-index-mutation';
import {MoveIndexMutation} from '../plan/move-index-mutation';
import {ResizeIndexMutation} from '../plan/resize-index-mutation';

use(chaiArrays);
use(chaiThings);
Expand Down
Loading

0 comments on commit ef84ab8

Please sign in to comment.