class Attributor::Attribute
It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array…) TODO: should this be a mixin since it is an abstract class?
Constants
- INTERNAL_OPTIONS
- TOP_LEVEL_OPTIONS
Attributes
custom_options[RW]
options[R]
type[R]
Public Class Methods
custom_option(name, attr_type, options = {}, &block)
click to toggle source
# File lib/attributor/attribute.rb, line 32 def self.custom_option(name, attr_type, options = {}, &block) if TOP_LEVEL_OPTIONS.include?(name) || INTERNAL_OPTIONS.include?(name) raise ArgumentError, "can not define custom_option with name #{name.inspect}, it is reserved by Attributor" end self.custom_options[name] = Attributor::Attribute.new(attr_type, options, &block) end
default_for_null()
click to toggle source
Default value for a non-specified null: option
# File lib/attributor/attribute.rb, line 247 def self.default_for_null false end
new(type, options = {}, &block)
click to toggle source
@options: metadata about the attribute @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
# File lib/attributor/attribute.rb, line 41 def initialize(type, options = {}, &block) @type = Attributor.resolve_type(type, options, block) @options = @type.respond_to?(:options) ? @type.options.merge(options) : options # We will give the values passed for options, a chance to be 'loaded' by its type, so we store native loaded values in the options begin current_option_name = nil # Use this to avoid having to wrap each loop with a begin/rescue block (self.class.custom_options.keys & @options.keys).each do |custom_key| current_option_name = custom_key @options[custom_key] = self.class.custom_options[custom_key].load(@options[custom_key]) end rescue => e raise AttributorException, "Error while loading value #{@options[current_option_name]} for custom option '#{current_option_name}': #{e.message}" end check_options! end
nullable_attribute?(options)
click to toggle source
It is only nullable if there is an explicit null: true (or if it’s not passed/set, and the default is true)
# File lib/attributor/attribute.rb, line 252 def self.nullable_attribute?(options) !options.key?(:null) ? default_for_null : options[:null] end
Public Instance Methods
==(other)
click to toggle source
# File lib/attributor/attribute.rb, line 66 def ==(other) raise ArgumentError, "can not compare Attribute with #{other.class.name}" unless other.is_a?(Attribute) type == other.type && options == other.options end
as_json_schema(shallow: true, example: nil)
click to toggle source
FiXME: pass and utilize the "shallow" parameter
required options type example
UTILIZE THIS SITE! http://jsonschema.net/#/
# File lib/attributor/attribute.rb, line 185 def as_json_schema(shallow: true, example: nil) description = self.type.as_json_schema(shallow: shallow, example: example, attribute_options: self.options ) description[:description] = self.options[:description] if self.options[:description] description[:enum] = self.options[:values] if self.options[:values] if the_default = self.options[:default] the_object = the_default.is_a?(Proc) ? the_default.call : the_default description[:default] = the_object.is_a?(Attributor::Dumpable) ? the_object.dump : the_object end #TODO description[:title] = "TODO: do we want to use a title??..." # Change the reference option to the actual class name. if ( reference = self.options[:reference] ) description[:'x-reference'] = reference.name end # TODO: not sure if that's correct (we used to get it from the described hash... description[:example] = self.dump(example) if example # add custom options as x-optionname self.class.custom_options.each do |name, _| description["x-#{name}".to_sym] = self.options[name] if self.options.key?(name) end description end
attributes()
click to toggle source
# File lib/attributor/attribute.rb, line 242 def attributes type.attributes if @type_has_attributes ||= type.respond_to?(:attributes) end
check_custom_option(name, definition)
click to toggle source
# File lib/attributor/attribute.rb, line 321 def check_custom_option(name, definition) attribute = self.class.custom_options.fetch(name) errors = attribute.validate(definition) raise AttributorException, "Custom option #{name.inspect} is invalid: #{errors.inspect}" if errors.any? :ok end
check_option!(name, definition)
click to toggle source
TODO: override in type subclass
# File lib/attributor/attribute.rb, line 292 def check_option!(name, definition) return check_custom_option(name, definition) if self.class.custom_options.include? name case name when :values raise AttributorException, "Allowed set of values requires an array. Got (#{definition})" unless definition.is_a? ::Array when :default raise AttributorException, "Default value doesn't have the correct attribute type. Got (#{definition.inspect})" unless type.valid_type?(definition) || definition.is_a?(Proc) options[:default] = load(definition) unless definition.is_a?(Proc) when :description raise AttributorException, "Description value must be a string. Got (#{definition})" unless definition.is_a? ::String when :required raise AttributorException, 'Required must be a boolean' unless definition == true || definition == false raise AttributorException, 'Required cannot be enabled in combination with :default' if definition == true && options.key?(:default) when :null raise AttributorException, 'Null must be a boolean' unless definition == true || definition == false when :example unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || type.valid_type?(definition) raise AttributorException, "Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)" end when :custom_data raise AttributorException, "custom_data must be a Hash. Got (#{definition})" unless definition.is_a?(::Hash) else return :unknown # unknown option end :ok # passes end
check_options!()
click to toggle source
# File lib/attributor/attribute.rb, line 280 def check_options! options.each do |option_name, option_value| next unless check_option!(option_name, option_value) == :unknown if type.check_option!(option_name, option_value) == :unknown raise AttributorException, "unsupported option: #{option_name} with value: #{option_value.inspect} for attribute: #{inspect}" end end true end
describe(shallow=true, example: nil)
click to toggle source
# File lib/attributor/attribute.rb, line 125 def describe(shallow=true, example: nil) description = { } # Clone the common options TOP_LEVEL_OPTIONS.each do |option_name| description[option_name] = self.describe_option(option_name) if self.options.has_key? option_name end # Make sure this option definition is not mistaken for the real generated example if (ex_def = description.delete(:example)) description[:example_definition] = ex_def end special_options = options.keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS description[:options] = {} unless special_options.empty? special_options.each do |opt_name| description[:options][opt_name] = self.describe_option(opt_name) end # Change the reference option to the actual class name. if (reference = options[:reference]) description[:options][:reference] = reference.name end description[:type] = type.describe(shallow, example: example) # Move over any example from the type, into the attribute itself if (ex = description[:type].delete(:example)) description[:example] = dump(ex) end description end
describe_option( option_name )
click to toggle source
# File lib/attributor/attribute.rb, line 175 def describe_option( option_name ) self.type.describe_option( option_name, self.options[option_name] ) end
dump(value, **opts)
click to toggle source
# File lib/attributor/attribute.rb, line 109 def dump(value, **opts) type.dump(value, **opts) end
duplicate(type: nil, options: nil)
click to toggle source
# File lib/attributor/attribute.rb, line 59 def duplicate(type: nil, options: nil) clone.tap do |cloned| cloned.instance_variable_set(:@type, type) if type cloned.instance_variable_set(:@options, options) if options end end
example(context = nil, parent: nil, values: {})
click to toggle source
# File lib/attributor/attribute.rb, line 212 def example(context = nil, parent: nil, values: {}) require 'faker' raise ArgumentError, 'attribute example cannot take a context of type String' if context.is_a? ::String if context ctx = Attributor.humanize_context(context) seed, = Digest::SHA1.digest(ctx).unpack('QQ') Random.srand(seed) else context = Attributor::DEFAULT_ROOT_CONTEXT end if options.key? :example loaded = example_from_options(parent, context) # Only validate the type, if the proc-generated example is "complex" (has attributes) errors = loaded.class.respond_to?(:attributes) ? validate_type(loaded, context) : validate(loaded, context) raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any? return loaded end return options[:values].sample if options.key? :values if type.respond_to?(:attributes) type.example(context, **values) else type.example(context, options: options) end end
example_from_options(parent, context)
click to toggle source
# File lib/attributor/attribute.rb, line 156 def example_from_options(parent, context) val = options[:example] generated = case val when ::Proc if val.arity == 2 val.call(parent, context) elsif val.arity == 1 val.call(parent) else val.call end when nil nil else val end load(generated, context) end
load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
click to toggle source
# File lib/attributor/attribute.rb, line 80 def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) value = type.load(value, context, **options) if value.nil? && self.options.key?(:default) defined_val = self.options[:default] val = case defined_val when ::Proc fake_parent = FakeParent.new # TODO: we can only support "context" as a parameter to the proc for now, since we don't have the parent... if defined_val.arity == 2 defined_val.call(fake_parent, context) elsif defined_val.arity == 1 defined_val.call(fake_parent) else defined_val.call end else defined_val end value = val # Need to load? end value rescue AttributorException, NameError raise rescue => e raise Attributor::LoadError, "Error loading attribute #{Attributor.humanize_context(context)} of type #{type.name} from value #{Attributor.errorize_value(value)}\n#{e.message}" end
parse(value, context = Attributor::DEFAULT_ROOT_CONTEXT)
click to toggle source
# File lib/attributor/attribute.rb, line 73 def parse(value, context = Attributor::DEFAULT_ROOT_CONTEXT) object = load(value, context) errors = validate(object, context) [object, errors] end
validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT)
click to toggle source
Validates stuff and checks dependencies
# File lib/attributor/attribute.rb, line 257 def validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT) raise "INVALID CONTEXT!! #{context}" unless context # Validate any requirements, absolute or conditional, and return. errors = [] if object.nil? && !self.class.nullable_attribute?(options) errors << "Attribute #{Attributor.humanize_context(context)} is not nullable" else errors.push *validate_type(object, context) # If the value is null we skip value validation: # a) If null wasn't allowed, it would have failed above. # b) If null was allowed, we always allow that as a valid value if !object.nil? && options[:values] && !options[:values].include?(object) errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} " end end return errors if errors.any? object.nil? ? errors : errors + type.validate(object, context, self) end
validate_type(value, context)
click to toggle source
# File lib/attributor/attribute.rb, line 113 def validate_type(value, context) # delegate check to type subclass if it exists return [] if value.nil? || type.valid_type?(value) msg = "Attribute #{Attributor.humanize_context(context)} received value: " msg += "#{Attributor.errorize_value(value)} is of the wrong type " msg += "(got: #{value.class.name}, expected: #{type.name})" [msg] end