Custom Types
The predefined types we've seen before are actually just simple functions, here
is for instance the definition of enotype's color
type:
exports.color = value => {
if(!value.match(/^\s*#[0-9a-f]{3}([0-9a-f]{3})?\s*$/i))
throw 'A color is required, for instance \'#B6D918\', \'#fff\' or \'#01b\'.';
return value;
};
So as we see, all that's required for a custom type definition is a function that takes a single parameter - the value - and returns something!
Additionally, depending on the type, we might also include validation as demonstrated above - we simply throw a string message if there is a problem. When that happens the message is intercepted by enolib and embedded within a rich error message with metadata and a code snippet from the document.
const enolib = require('enolib');
const extraFoo = value => value + ' with extra foo!';
const document = enolib.parse('foo: bar');
document.field('foo').requiredValue(extraFoo); // returns 'bar with extra foo!'
And just like that we are using our own type. Here's another example using the global registering magic from the previous chapter:
const enolib = require('enolib');
const fooOnly = value => {
if(value !== 'foo')
throw 'Only foo is accepted!';
return value;
};
enolib.register({ fooOnly });
const document = enolib.parse('foo: bar');
document.field('foo').fooOnlyKey(); // returns 'foo'
document.field('foo').requiredFooOnlyValue(); // throws "Only foo is accepted!" with a code snippet
As a final note why it is sometimes preferable to use the less magicky
function parameter type passing: On the one hand you might dynamically construct
a type loader multiple times in your application when it depends on some local
state (closure). On the other hand your type might not be a type per se but more of
a conversion or modification, something like replaceNewlines
or renderMarkdown
,
and in that case .requiredValue(renderMarkdown)
looks a bit more sensible than
.requiredRenderMarkdownValue()
.
Next page: Querying elements