mami/src/mami.js/args.js

185 lines
5.7 KiB
JavaScript

const MamiArgs = (argName, input, builder) => {
if(input !== undefined && typeof input !== 'object')
throw `${argName} must be an object or undefined`;
if(typeof builder !== 'function')
throw 'builder must be a function';
const args = new Map;
builder(name => {
if(typeof name !== 'string')
throw 'name must be a string';
const checkDefined = () => {
if(args.has(name))
throw `${name} has already been defined`;
};
checkDefined();
let created = false;
const checkCreated = () => {
if(created)
throw 'argument has already been defined';
};
const info = {
name: name,
type: undefined,
filter: undefined,
constraint: undefined,
fallback: undefined,
throw: undefined,
required: undefined,
min: undefined,
max: undefined,
};
const blueprint = {
type: value => {
if(typeof value !== 'string' && !Array.isArray(value))
throw 'type must be a javascript type or array of valid string values';
checkCreated();
info.type = value;
return blueprint;
},
default: value => {
checkCreated();
info.fallback = value;
if(info.type === undefined && info.constraint === undefined)
info.type = typeof info.fallback;
return blueprint;
},
filter: value => {
if(typeof value !== 'function')
throw 'filter must be a function';
checkCreated();
info.filter = value;
return blueprint;
},
constraint: value => {
if(typeof value !== 'function')
throw 'constraint must be a function';
checkCreated();
info.constraint = value;
return blueprint;
},
throw: value => {
checkCreated();
info.throw = value === undefined || value === true;
return blueprint;
},
required: value => {
checkCreated();
info.required = value === undefined || value === true;
if(info.required && info.throw === undefined)
info.throw = true;
return blueprint;
},
min: value => {
checkCreated();
if(typeof value !== 'number')
throw 'value must be a number';
info.min = value;
return blueprint;
},
max: value => {
checkCreated();
if(typeof value !== 'number')
throw 'value must be a number';
info.max = value;
return blueprint;
},
done: () => {
checkCreated();
checkDefined();
args.set(name, info);
},
};
return blueprint;
});
const inputIsNull = input === null;
if(inputIsNull)
input = {};
const output = {};
for(const [name, info] of args) {
let value = info.fallback;
if(info.name in input) {
const defaultOrThrow = ex => {
if(info.throw)
throw ex;
value = info.fallback;
};
value = input[info.name];
if(info.type !== undefined) {
if(Array.isArray(info.type)) {
if(!info.type.includes(value))
defaultOrThrow(`${info.name} must match an enum value`);
} else {
const type = typeof value;
let resolved = false;
if(type !== info.type) {
if(type === 'string') {
if(info.type === 'number') {
value = parseFloat(value);
resolved = true;
if(info.min !== undefined && value < info.min)
value = info.min;
else if(info.max !== undefined && value > info.max)
value = info.max;
} else if(info.type === 'boolean') {
value = !!value;
resolved = true;
}
} else if(info.type === 'string') {
value = value.toString();
resolved = true;
}
} else resolved = true;
if(!resolved)
defaultOrThrow(`${info.name} must be of type ${info.type}`);
}
}
if(info.constraint !== undefined && !info.constraint(value))
defaultOrThrow(`${info.name} did not fit within constraints`);
// you could have a devious streak with this one cuz the checks aren't rerun
// but surely you wouldn't fuck yourself over like that
if(info.filter !== undefined)
value = info.filter(value);
} else if(info.required) {
if(inputIsNull)
throw `${argName} must be a non-null object`;
throw `${argName} is missing required key ${info.name}`;
}
output[info.name] = value;
}
return output;
};