$ gem install coercive
Coercive is a Ruby library to validate and coerce user input.
Define your coercion modules like this:
require "coercive"
module CoerceFoo
extend Coercive
attribute :foo, string(min: 1, max: 10), required
endPass in your user input and you'll get back validated and coerced attributes:
attributes = CoerceFoo.call("foo" => "bar")
attributes["foo"]
# => "bar"
CoerceFoo.call("foo" => "more than 10 chars long")
# => Coercive::Error: {"foo"=>"too_long"}
CoerceFoo.call("bar" => "foo is not here")
# => Coercive::Error: {"foo"=>"not_present", "bar"=>"unknown"}Coercive's single entry-point is the call method that receives a Hash. It will compare each key-value pair against the definitions provided by the attribute method.
The attribute functions takes three arguments:
- The first one is the name of the attribute.
- The second one is a coerce function. Coercive comes with many available, and you can always write your own.
- The third one is a fetch function, used to look up the attribute in the input
Hash.
As you saw in the example above, required is one of the three fetch functions available. Let's get into each of them and how they work.
As the name says, Coercive will raise an error if the input lacks the attribute, and add the "not_present" error code.
CoerceFoo.call("bar" => "foo is not here")
# => Coercive::Error: {"foo"=>"not_present", "bar"=>"unknown"}The optional fetch function will grab an attribute from the input, but do nothing if it's not there. Let's look again at the example above:
module CoerceFoo
extend Coercive
attribute :foo, string(min: 1, max: 10), required
end
CoerceFoo.call("bar" => "foo is not here")
# => Coercive::Error: {"foo"=>"not_present", "bar"=>"unknown"}The "bar" attribute raises an error because it's unexpected. Coercive is thorough when it comes to the input. To make this go away, we have to add "bar" as optional:
module CoerceFoo
extend Coercive
attribute :foo, string(min: 1, max: 10), required
attribute :bar, any, optional
end
CoerceFoo.call("bar" => "foo is not here")
# => Coercive::Error: {"foo"=>"not_present"}The last fetch function Coercive has is a handy way to set a default value when an attribute is not present in the input.
module CoerceFoo
extend Coercive
attribute :foo, string(min: 1, max: 10), implicit("default")
attribute :bar, any, optional
end
CoerceFoo.call("bar" => "any")
# => {"foo"=>"default", "bar"=>"any"}Keep in mind that your default must comply with the declared type and restrictions. In this case, implicit("very long default value") will raise an error because it's longer than 10 characters.
We already got a taste for the coercion functions with string(min: 1, max:10) and there are many more! but let's start there.
The string coercion function will enforce a minimum and maximum character length, throwing "too_short" and "too_long" errors respectively if the input is not within the declared bounds.
Additionally, you can also verify your String matches a regular expression with the pattern: option.
module CoerceFoo
extend Coercive
attribute :foo, string(pattern: /\A\h+\z/), optional
end
CoerceFoo.call("foo" => "REDBEETS")
# => Coercive::Error: {"foo"=>"not_valid"}
CoerceFoo.call("foo" => "DEADBEEF")
# => {"foo"=>"DEADBEEF"}The date and datetime coercion functions will receive a String and give you Date and DateTime objects, respectively.
By default they expect an ISO 8601 string, but they provide a format option in case you need to parse something different, following the strftime format.
module CoerceFoo
extend Coercive
attribute :date_foo, date, optional
attribute :american_date, date(format: "%m-%d-%Y"), optional
attribute :datetime_foo, datetime, optional
end
CoerceFoo.call("date_foo" => "1988-05-18", "datetime_foo" => "1988-05-18T21:00:00Z", "american_date" => "05-18-1988")
# => {"date_foo"=>#<Date: 1988-05-18 ((2447300j,0s,0n),+0s,2299161j)>,
# "american_date"=>#<Date: 1988-05-18 ((2447300j,0s,0n),+0s,2299161j)>,
# "datetime_foo"=>#<DateTime: 1988-05-18T21:00:00+00:00 ((2447300j,75600s,0n),+0s,2299161j)>}
CoerceFoo.call("date_foo" => "18th May 1988")
# => Coercive::Error: {"date_foo"=>"not_valid"}The any coercion function lets anything pass through. It's commonly used with the optional fetch function when an attribute may or many not be a part of the input.
member will check that the value is one of the values of the given array.
module CoerceFoo
extend Coercive
attribute :foo, member(["one", "two", "three"]), optional
end
CoerceFoo.call("foo" => 4)
# => Coercive::Error: {"foo"=>"not_valid"}integer expects an integer value. It supports optional min and max options to check if the user input is within certain bounds.
module CoerceFoo
extend Coercive
attribute :foo, integer, optional
attribute :foo_bounds, integer(min: 1, max: 10), optional
end
CoerceFoo.call("foo" => "1")
# => {"foo"=>1}
CoerceFoo.call("foo" => "bar")
# => Coercive::Error: {"foo"=>"not_valid"}
CoerceFoo.call("foo" => "1.5")
# => Coercive::Error: {"foo"=>"not_numeric"}
CoerceFoo.call("foo" => 1.5)
# => Coercive::Error: {"foo"=>"float_not_permitted"}
CoerceFoo.call("foo_bounds" => 0)
# => Coercive::Error: {"foo_bounds"=>"too_low"}
CoerceFoo.call("foo_bounds" => 11)
# => Coercive::Error: {"foo_bounds"=>"too_high"}float expects, well, a float value. It supports optional min and max options to check if the user input is within certain bounds.
module CoerceFoo
extend Coercive
attribute :foo, float, optional
attribute :foo_bounds, float(min: 1.0, max: 5.5), optional
end
CoerceFoo.call("foo" => "bar")
# => Coercive::Error: {"foo"=>"not_valid"}
CoerceFoo.call("foo_bounds" => "0.5")
# => Coercive::Error: {"foo_bounds"=>"too_low"}
CoerceFoo.call("foo_bounds" => 6.5)
# => Coercive::Error: {"foo_bounds"=>"too_high"}
CoerceFoo.call("foo" => "0.1")
# => {"foo"=>0.1}
CoerceFoo.call("foo" => "0.1e5")
# => {"foo"=>10000.0}boolean will coerce input into true or false. You can also specifiy additional values to coerce into true or false with the true_if and false_if options.
module CoerceFoo
extend Coercive
attribute :foo, boolean, optional
attribute :foo_if, boolean(true_if: member(["1", "on"])), optional
end
CoerceFoo.call("foo" => true)
# => {"foo"=>true}
CoerceFoo.call("foo" => "true")
# => {"foo"=>true}
CoerceFoo.call("foo" => nil)
# => Coercive::Error: {"foo"=>"not_valid"}
CoerceFoo.call("foo_if" => "on")
# => {"foo_if"=>true}The array coercion is interesting because it's where Coercive starts to shine, by letting you compose coercion functions together. Let's see:
module CoerceFoo
extend Coercive
attribute :foo, array(string), optional
end
CoerceFoo.call("foo" => ["one", "two", "three"])
# => {"foo"=>["one", "two", "three"]}
CoerceFoo.call("foo" => [1, 2, 3])
# => {"foo"=>["1", "2", "3"]}
CoerceFoo.call("foo" => [nil, true])
# => {"foo"=>["", "true"]}
CoerceFoo.call("foo" => [BasicObject.new])
# => Coercive::Error: {"foo"=>["not_valid"]}hash coercion let's you manipulate the key and values, similarly to how array does.
module CoerceFoo
extend Coercive
attribute :foo, hash(string(max: 3), float), optional
end
CoerceFoo.call("foo" => {"bar" => "0.1"})
# => {"foo"=>{"bar"=>0.1}}
CoerceFoo.call("foo" => {"barrrr" => "0.1"})
# => Coercive::Error: {"foo"=>{"barrrr"=>"too_long"}}The uri coercion function really showcases how it's very easy to build custom logic to validate and coerce any kind of input. uri is meant to verify IP and URLs and has a variety of options.
module CoerceFoo
extend Coercive
attribute :foo, uri(string), optional
end
CoerceFoo.call("foo" => "http://github.com")
# => {"foo"=>"http://github.com"}
CoerceFoo.call("foo" => "not a url")
# => Coercive::Error: {"foo"=>"not_valid"}The schema_fn option allows you to compose additional coercion functions to verify the schema.
module CoerceFoo
extend Coercive
attribute :foo, uri(string, schema_fn: member(%w{http https})), optional
end
CoerceFoo.call("foo" => "https://github.com")
# => {"foo"=>"https://github.com"}
CoerceFoo.call("foo" => "ftp://github.com")
# => Coercive::Error: {"foo"=>"unsupported_schema"}There's a number of boolean options to enforce the presence of parts of a URI to be present. By default they're all false.
require_path: for example,"https://github.com/Theorem"require_port: for example,"https://github.com:433"require_user: for example,"https://[email protected]"require_password: for example,"https://:[email protected]"