How to make plugins production ready
Our How to build plugins guide covered the basics for how to construct an Impact Framework plugin. This guide will help you to refine your plugin to make it production-ready. These are best practice guidelines - if you intend to contribute to one of our repositories, following these guidelines will help your PR to get merged. Consistency with our norms is useful for debugging and maintaining and for making your plugin as useful as possible for other Impact Framework developers.
1. Naming conventions
We prefer not to use abbreviations of contractions in parameter names. Using fully descriptive names makes the code more readable, which in turn helps reviewers and anyone else aiming to understand how the plugin works. It also helps to avoid ambiguity and naming collisions within and across plugins. Your name should describe what an element does as precisely as practically possible.
For example, we prefer cpu/energy
to e-cpu
and we prefer functionalUnit
to funcUnit
, fUnit
, or any other abbreviation.
In Typescript code we use lower Camel case (likeThis
) for variable and function names and Pascal/Upper Camel case for class, type, enum, and interface names (LikeThis
).
For example:
sci
is the name for the SCI value normalized per second.energy
is the name for the array of energy metrics available to be summed in thesci-e
plugin
In yaml files, we prefer to use kebab-case (like-this
) for field names. For example:
network/energy
is the field name for the energy consumed by networking for an applicationfunctional-unit
is the unit in which to express an SCI value.
Global constants can be given capitalized names, such as TIME_UNITS_IN_SECONDS
.
2. Plugin code
Imports
We prefer the following ordering of imports in your plugin code:
- Node built-in modules (e.g.
import fs from 'fs';
) - External modules (e.g.
import {z} from 'zod';
) - Internal modules (e.g.
import config from 'src/config';
) - Interfaces (e.g.
import {PluginInterface} from '@grnsft/if-core/types';
) - Types (e.g.
import {PluginParams} from '@grnsft/if-core/types';
)
Comments
Each logical unit in the code should be preceded by an appropriate explanatory comment. Sometimes it is useful to include short comments inside a function that clarifies the purpose of a particular statement. Here's an example from our codebase:
/**
* Calculates the energy consumption for a single input.
*/
const calculateEnergy = (input: PluginParams) => {
const {
'memory/capacity': totalMemory,
'memory/utilization': memoryUtil,
'energy-per-gb': energyPerGB,
} = input;
// GB * kWh/GB == kWh
return totalMemory * (memoryUtil / 100) * energyPerGB;
};
Error handling
We use custom errors across our codebase to make it as easy as possible to understand the root cause of a problem.
You can use our error handlers by importing if-core
as a dependency of your plugin. This provides you with our error handling code and predefined list of error classes that you can invoke. This gives you tight integration with IF, because the framework can recognize those error classes and automatically incorporate them into the framework's error handling routines.
Just import ERRORS
from if-core
and use the error classes that are appropriate for your use-case.
e.g.
import {ERRORS} from '@grnsft/if-core/util';
const {MissingInputDataError} = ERRORS;
...
throw new MissingInputDataError("my-plugin is missing my-parameter from inputs[0]");
Validation
Input Validation
We recommend using inputValidation
property from PluginFactory
for validation to ensure the integrity of input data. Validate input parameters against expected types, ranges, or constraints to prevent runtime errors and ensure data consistency.
You need to use zod
schema or InputValidatorFunction
. Here's an example from our codebase:
- When using function with
InputValidatorFunction
type.
// `inputValidation` from plugin definition
inputValidation: (input: PluginParams, config: ConfigParams) => {
const inputData = {
'input-parameter': input[config['input-parameter']],
};
const validationSchema = z.record(z.string(), z.number());
validate(validationSchema, inputData);
return input;
};
- When using
zod
schema
// `inputValidation` from plugin definition
inputValidation: z.object({
duration: z.number().gt(0),
vCPUs: z.number().gt(0).default(1),
memory: z.number().gt(0).default(16),
ssd: z.number().gte(0).default(0),
hdd: z.number().gte(0).default(0),
gpu: z.number().gte(0).default(0),
'usage-ratio': z.number().gt(0).default(1),
time: z.number().gt(0).optional(),
});
Config Validation
To validate the config
, you need to use configValidation
property from PluginFactory
. Validate config parameters against expected types, ranges, or constraints to prevent runtime errors and ensure data consistency.
You need to use zod
schema or ConfigValidatorFunction
:
- When using function with
ConfigValidatorFunction
type.
configValidation: (config: ConfigParams) => {
const configSchema = z.object({
coefficient: z.preprocess(
(value) => validateArithmeticExpression('coefficient', value, 'number'),
z.number()
),
'input-parameter': z.string().min(1),
'output-parameter': z.string().min(1),
});
return validate<z.infer<typeof configSchema>>(
configSchema as ZodType<any>,
config
);
};
- When using
zod
schema
configValidation: z.object({
'input-parameters': z.array(z.string()),
'output-parameter': z.string().min(1),
}),
Code Modularity
Break down complex functionality into smaller, manageable methods with well-defined responsibilities. Encapsulate related functionality into private methods to promote code reusability and maintainability.
3. Unit tests
Your plugin should have unit tests with 100% coverage. We use jest
to handle unit testing. We strive to have one describe
per function. Each possible outcome from each function is separated using it
with a precise and descriptive message.
Here's an example that covers plugin initialization and the happy path for the execute()
function.
import { ERRORS } from '@grnsft/if-core/utils';
import { Sum } from '../../../if-run/builtins/sum';
const { InputValidationError, WrongArithmeticExpressionError } = ERRORS;
describe('builtins/sum: ', () => {
describe('Sum: ', () => {
const config = {
'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'],
'output-parameter': 'energy',
};
const parametersMetadata = {};
const sum = Sum(config, parametersMetadata, {});
describe('init: ', () => {
it('successfully initalized.', () => {
expect(sum).toHaveProperty('metadata');
expect(sum).toHaveProperty('execute');
});
});
describe('execute(): ', () => {
it('successfully applies Sum strategy to given input.', async () => {
expect.assertions(1);
const expectedResult = [
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
energy: 3,
timestamp: '2021-01-01T00:00:00Z',
},
];
const result = await sum.execute([
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
timestamp: '2021-01-01T00:00:00Z',
},
]);
expect(result).toStrictEqual(expectedResult);
});
});
});
});
We have a dedicated page explaining in more detail how to write great unit tests for Impact Framework plugins.
4. Linting
We use ESLint to format our code. We use a very simple configuration file (eslintrc.json
), as follows:
{
"extends": "./node_modules/gts/",
"rules": {
"@typescript-eslint/no-explicit-any": ["off"]
}
}
For our repositories we use Github CI to enforce the linting rules for any pull requests.
Summary
On this page, we have outlined best practices for refining your plugins so that they conform to our expected norms. This will help you write clean, efficient, and understandable code!