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