Custom Types
The predefined types we've seen before are actually just simple lambdas/procs, here
is for instance the definition of enotype's color
type:
COLOR_REGEXP = /^\s*#[0-9a-f]{3}([0-9a-f]{3})?\s*$/i
color = proc do |value|
raise 'A color is required, for instance \'#B6D918\', \'#fff\' or \'#01b\'.' unless value.match(COLOR_REGEXP)
value
end
So as we see, all that's required for a custom type definition is a closure 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 raise 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.
require 'enolib'
extra_foo = proc { |value| value + ' with extra foo!' }
document = Enolib.parse('foo: bar')
document.field('foo').required_value(extra_foo) # 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:
require 'enolib'
my_foo_only_proc = proc do |value|
raise 'Only foo is accepted!' if value != 'foo'
value
end
Enolib.register(foo_only: my_foo_only_proc)
document = Enolib.parse('foo: bar')
document.field('foo').foo_only_key # returns 'foo'
document.field('foo').required_foo_only_value # raises "Only foo is accepted!" with a code snippet
As a final note why it is sometimes preferable to use the less magicky
method 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 replace_newlines
or render_markdown
,
and in that case .required_value(render_markdown)
looks a bit more sensible than
.required_render_markdown_value
.
Next page: Querying elements