File Manager
Viewing File: params_validate.rb
#
# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative "../constants"
require_relative "../property"
require_relative "../delayed_evaluator"
require_relative "../exceptions"
class Chef
module Mixin
module ParamsValidate
# Takes a hash of options, along with a map to validate them. Returns the original
# options hash, plus any changes that might have been made (through things like setting
# default values in the validation map)
#
# For example:
#
# validate({ :one => "neat" }, { :one => { :kind_of => String }})
#
# Would raise an exception if the value of :one above is not a kind_of? string. Valid
# map options are:
#
# @param opts [Hash<Symbol,Object>] Validation opts.
# @option opts [String] :validation_message A custom message to return
# should validation fail.
# @option opts [Object,Array] :is An object, or list of
# objects, that must match the value using Ruby's `===` operator
# (`opts[:is].any? { |v| v === value }`). (See #_pv_is.)
# @option opts [Object,Array] :equal_to An object, or list
# of objects, that must be equal to the value using Ruby's `==`
# operator (`opts[:equal_to].any? { |v| v == value }`) (See #_pv_equal_to.)
# @option opts [Regexp,Array<Regexp>] :regex An object, or
# list of objects, that must match the value with `regex.match(value)`.
# (See #_pv_regex)
# @option opts [Class,Array<Class>] :kind_of A class, or
# list of classes, that the value must be an instance of. (See
# #_pv_kind_of.)
# @option opts [Hash<String,Proc>] :callbacks A hash of
# messages -> procs, all of which match the value. The proc must
# return a truthy or falsey value (true means it matches). (See
# #_pv_callbacks.)
# @option opts [Symbol,Array<Symbol>] :respond_to A method
# name, or list of method names, the value must respond to. (See
# #_pv_respond_to.)
# @option opts [Symbol,Array<Symbol>] :cannot_be A property,
# or a list of properties, that the value cannot have (such as `:nil` or
# `:empty`). The method with a questionmark at the end is called on the
# value (e.g. `value.empty?`). If the value does not have this method,
# it is considered valid (i.e. if you don't respond to `empty?` we
# assume you are not empty). (See #_pv_cannot_be.)
# @option opts [Proc] :coerce A proc which will be called to
# transform the user input to canonical form. The value is passed in,
# and the transformed value returned as output. Lazy values will *not*
# be passed to this method until after they are evaluated. Called in the
# context of the resource (meaning you can access other properties).
# (See #_pv_coerce.) (See #_pv_coerce.)
# @option opts [Boolean] :required `true` if this property
# must be present and not `nil`; `false` otherwise. This is checked
# after the resource is fully initialized. (See #_pv_required.)
# @option opts [Boolean] :name_property `true` if this
# property defaults to the same value as `name`. Equivalent to
# `default: lazy { name }`, except that #property_is_set? will
# return `true` if the property is set *or* if `name` is set. (See
# #_pv_name_property.)
# @option opts [Boolean] :name_attribute Same as `name_property`.
# @option opts [Object] :default The value this property
# will return if the user does not set one. If this is `lazy`, it will
# be run in the context of the instance (and able to access other
# properties). (See #_pv_default.)
#
def validate(opts, map)
map = map.validation_options if map.is_a?(Property)
#--
# validate works by taking the keys in the validation map, assuming it's a hash, and
# looking for _pv_:symbol as methods. Assuming it find them, it calls the right
# one.
#++
raise ArgumentError, "Options must be a hash" unless opts.is_a?(Hash)
raise ArgumentError, "Validation Map must be a hash" unless map.is_a?(Hash)
@validation_message ||= {}
map.each do |key, validation|
unless key.is_a?(Symbol) || key.is_a?(String)
raise ArgumentError, "Validation map keys must be symbols or strings!"
end
case validation
when true
_pv_required(opts, key)
when false
true
when Hash
@validation_message[key] = validation.delete(:validation_message) if validation.key?(:validation_message)
validation.each do |check, carg|
check_method = "_pv_#{check}"
if respond_to?(check_method, true)
send(check_method, opts, key, carg)
else
raise ArgumentError, "Validation map has unknown check: #{check}"
end
end
end
end
opts
end
def lazy(&block)
DelayedEvaluator.new(&block)
end
def set_or_return(symbol, value, validation)
property = SetOrReturnProperty.new(name: symbol, **validation)
property.call(self, value)
end
private
def _validation_message(key, default)
@validation_message.key?(key) ? @validation_message[key] : default
end
# Return the value of a parameter, or nil if it doesn't exist.
def _pv_opts_lookup(opts, key)
if opts.key?(key.to_s)
opts[key.to_s]
elsif opts.key?(key.to_sym)
opts[key.to_sym]
else
nil
end
end
# Raise an exception if the parameter is not found.
def _pv_required(opts, key, is_required = true, explicitly_allows_nil = false)
if is_required
return true if opts.key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?)
return true if opts.key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?)
raise Exceptions::ValidationFailed, _validation_message(key, "Required argument #{key.inspect} is missing!")
end
true
end
#
# List of things values must be equal to.
#
# Uses Ruby's `==` to evaluate (equal_to == value). At least one must
# match for the value to be valid.
#
# `nil` passes this validation automatically.
#
# @return [Array,nil] List of things values must be equal to, or nil if
# equal_to is unspecified.
#
def _pv_equal_to(opts, key, to_be)
value = _pv_opts_lookup(opts, key)
unless value.nil?
to_be = Array(to_be)
to_be.each do |tb|
return true if value == tb
end
# Ruby will print :something as something, which confuses users so make sure to print them as symbols
# by inspecting the value instead of just printing it
raise Exceptions::ValidationFailed, _validation_message(key, "Option #{key} must be equal to one of: #{to_be.map(&:inspect).join(", ")}! You passed #{value.inspect}.")
end
end
#
# List of things values must be instances of.
#
# Uses value.kind_of?(kind_of) to evaluate. At least one must match for
# the value to be valid.
#
# `nil` automatically passes this validation.
#
def _pv_kind_of(opts, key, to_be)
value = _pv_opts_lookup(opts, key)
unless value.nil?
to_be = Array(to_be)
to_be.each do |tb|
return true if value.is_a?(tb)
end
raise Exceptions::ValidationFailed, _validation_message(key, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}.")
end
end
#
# List of method names values must respond to.
#
# Uses value.respond_to?(respond_to) to evaluate. At least one must match
# for the value to be valid.
#
def _pv_respond_to(opts, key, method_name_list)
value = _pv_opts_lookup(opts, key)
unless value.nil?
Array(method_name_list).each do |method_name|
unless value.respond_to?(method_name)
raise Exceptions::ValidationFailed, _validation_message(key, "Option #{key} must have a #{method_name} method!")
end
end
end
end
#
# List of things that must not be true about the value.
#
# Calls `value.<thing>?` All responses must be false for the value to be
# valid.
# Values which do not respond to <thing>? are considered valid (because if
# a value doesn't respond to `:readable?`, then it probably isn't
# readable.)
#
# @example
# ```ruby
# property :x, cannot_be: [ :nil, :empty ]
# x [ 1, 2 ] #=> valid
# x 1 #=> valid
# x [] #=> invalid
# x nil #=> invalid
# ```
#
def _pv_cannot_be(opts, key, predicate_method_base_name)
value = _pv_opts_lookup(opts, key)
unless value.nil?
Array(predicate_method_base_name).each do |method_name|
predicate_method = :"#{method_name}?"
if value.respond_to?(predicate_method)
if value.send(predicate_method)
raise Exceptions::ValidationFailed, _validation_message(key, "Option #{key} cannot be #{predicate_method_base_name}")
end
end
end
end
end
#
# The default value for a property.
#
# When the property is not assigned, this will be used.
#
# If this is a lazy value, it will either be passed the resource as a value,
# or if the lazy proc does not take parameters, it will be run in the
# context of the instance with instance_eval.
#
# @example
# ```ruby
# property :x, default: 10
# ```
#
# @example
# ```ruby
# property :x
# property :y, default: lazy { x+2 }
# ```
#
# @example
# ```ruby
# property :x
# property :y, default: lazy { |r| r.x+2 }
# ```
#
def _pv_default(opts, key, default_value)
value = _pv_opts_lookup(opts, key)
if value.nil?
default_value = default_value.freeze unless default_value.is_a?(DelayedEvaluator)
opts[key] = default_value
end
end
#
# List of regexes values that must match.
#
# Uses regex.match() to evaluate. At least one must match for the value to
# be valid.
#
# `nil` passes regex validation automatically.
#
# @example
# ```ruby
# property :x, regex: [ /abc/, /xyz/ ]
# ```
#
def _pv_regex(opts, key, regex)
value = _pv_opts_lookup(opts, key)
unless value.nil?
Array(regex).flatten.each do |r|
return true if r.match(value.to_s)
end
raise Exceptions::ValidationFailed, _validation_message(key, "Property #{key}'s value #{value} does not match regular expression #{regex.inspect}")
end
end
#
# List of procs we pass the value to.
#
# All procs must return true for the value to be valid. If any procs do
# not return true, the key will be used for the message: `"Property x's
# value :y <message>"`.
#
# @example
# ```ruby
# property :x, callbacks: { "is bigger than 10" => proc { |v| v <= 10 }, "is not awesome" => proc { |v| !v.awesome }}
# ```
#
def _pv_callbacks(opts, key, callbacks)
raise ArgumentError, "Callback list must be a hash!" unless callbacks.is_a?(Hash)
value = _pv_opts_lookup(opts, key)
unless value.nil?
callbacks.each do |message, zeproc|
unless zeproc.call(value)
raise Exceptions::ValidationFailed, _validation_message(key, "Option #{key}'s value #{value} #{message}!")
end
end
end
end
#
# Allows a parameter to default to the value of the resource name.
#
# @example
# ```ruby
# property :x, name_property: true
# ```
#
def _pv_name_property(opts, key, is_name_property = true)
if is_name_property
if opts[key].nil?
raise Exceptions::CannotValidateStaticallyError, "name_property cannot be evaluated without a resource." if self == Chef::Mixin::ParamsValidate
opts[key] = instance_variable_get(:"@name")
end
end
end
alias :_pv_name_attribute :_pv_name_property
#
# List of valid things values can be.
#
# Uses Ruby's `===` to evaluate (is === value). At least one must match
# for the value to be valid.
#
# If a proc is passed, it is instance_eval'd in the resource, passed the
# value, and must return a truthy or falsey value.
#
# @example Class
# ```ruby
# property :x, String
# x 'valid' #=> valid
# x 1 #=> invalid
# x nil #=> invalid
#
# @example Value
# ```ruby
# property :x, [ :a, :b, :c, nil ]
# x :a #=> valid
# x nil #=> valid
# ```
#
# @example Regex
# ```ruby
# property :x, /bar/
# x 'foobar' #=> valid
# x 'foo' #=> invalid
# x nil #=> invalid
# ```
#
# @example Proc
# ```ruby
# property :x, proc { |x| x > y }
# property :y, default: 2
# x 3 #=> valid
# x 1 #=> invalid
# ```
#
# @example Property
# ```ruby
# type = Property.new(is: String)
# property :x, type
# x 'foo' #=> valid
# x 1 #=> invalid
# x nil #=> invalid
# ```
#
# @example RSpec Matcher
# ```ruby
# include RSpec::Matchers
# property :x, a_string_matching /bar/
# x 'foobar' #=> valid
# x 'foo' #=> invalid
# x nil #=> invalid
# ```
#
def _pv_is(opts, key, to_be)
return true if !opts.key?(key.to_s) && !opts.key?(key.to_sym)
value = _pv_opts_lookup(opts, key)
to_be = [ to_be ].flatten(1)
errors = []
passed = to_be.any? do |tb|
case tb
when Proc
raise Exceptions::CannotValidateStaticallyError, "is: proc { } must be evaluated once for each resource" if self == Chef::Mixin::ParamsValidate
instance_exec(value, &tb)
when Property
begin
validate(opts, { key => tb.validation_options })
true
rescue Exceptions::ValidationFailed
# re-raise immediately if there is only one "is" so we get a better stack
raise if to_be.size == 1
errors << $!
false
end
else
tb === value
end
end
if passed
true
else
message = "Property #{key} must be one of: #{to_be.map(&:inspect).join(", ")}! You passed #{value.inspect}."
unless errors.empty?
message << " Errors:\n#{errors.map { |m| "- #{m}" }.join("\n")}"
end
raise Exceptions::ValidationFailed, _validation_message(key, message)
end
end
#
# Method to mess with a value before it is validated and stored.
#
# Allows you to transform values into a canonical form that is easy to
# work with.
#
# This is passed the value to transform, and is run in the context of the
# instance (so it has access to other resource properties). It must return
# the value that will be stored in the instance.
#
# @example
# ```ruby
# property :x, Integer, coerce: { |v| v.to_i }
# ```
#
def _pv_coerce(opts, key, coercer)
if opts.key?(key.to_s)
raise Exceptions::CannotValidateStaticallyError, "coerce must be evaluated for each resource." if self == Chef::Mixin::ParamsValidate
opts[key.to_s] = instance_exec(opts[key], &coercer)
elsif opts.key?(key.to_sym)
raise Exceptions::CannotValidateStaticallyError, "coerce must be evaluated for each resource." if self == Chef::Mixin::ParamsValidate
opts[key.to_sym] = instance_exec(opts[key], &coercer)
end
end
# We allow Chef::Mixin::ParamsValidate.validate(), but we will raise an
# error if you try to do anything requiring there to be an actual resource.
# This way, you can statically validate things if you have constant validation
# (which is the norm).
extend self
# Used by #set_or_return to avoid emitting a deprecation warning for
# "value nil" and to keep default stickiness working exactly the same
# @api private
class SetOrReturnProperty < Chef::Property
def get(resource, nil_set: false)
value = super
# All values are sticky, frozen or not
unless is_set?(resource)
set_value(resource, value)
end
value
end
def call(resource, value = NOT_PASSED)
# setting to nil does a get
if value.nil? && !explicitly_accepts_nil?(resource)
get(resource, nil_set: true)
else
super
end
end
end
end
end
end