class Attributor::Hash
Constants
- CIRCULAR_REFERENCE_MARKER
- MAX_EXAMPLE_DEPTH
Attributes
cached_defaults[R]
extra_keys[RW]
insensitive_map[R]
key_attribute[R]
key_type[R]
options[R]
requirements[R]
value_attribute[R]
value_type[R]
contents[R]
TODO: Think about the format of the subcontexts to use: let’s use .at(key.to_s)
dumping[R]
validating[R]
Public Class Methods
add_requirement(req)
click to toggle source
# File lib/attributor/types/hash.rb, line 161 def self.add_requirement(req) @requirements << req return unless req.attr_names non_existing = req.attr_names - attributes.keys unless non_existing.empty? raise "Invalid attribute name(s) found (#{non_existing.join(', ')}) when defining a requirement of type #{req.type} for #{Attributor.type_name(self)} ." \ "The only existing attributes are #{attributes.keys}" end end
as_json_schema( shallow: false, example: nil, attribute_options: {} )
click to toggle source
Calls superclass method
# File lib/attributor/types/hash.rb, line 486 def self.as_json_schema( shallow: false, example: nil, attribute_options: {} ) hash = super opts = self.options.merge( attribute_options ) if key_type hash[:'x-key_type'] = key_type.as_json_schema end if self.keys.any? # Spit keys if it's the root or if it's an anonymous structures if ( !shallow || self.name == nil) required_names_from_attr = [] # FIXME: change to :keys when the praxis doc browser supports displaying those hash[:properties] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes| required_names_from_attr << sub_name if sub_attribute.options[:required] == true sub_example = example.get(sub_name) if example sub_attributes[sub_name] = sub_attribute.as_json_schema(shallow: true, example: sub_example) end # Expose the more complex requirements to in the x-tended attribute extended_requirements = self.requirements.each_with_object([]) do |req, list| described_req = req.describe(shallow) if described_req[:type] == :all # Add the names of the attributes that have the required flag too described_req[:attributes] |= required_names_from_attr required_names_from_attr = [] end list << described_req end all = extended_requirements.find{|r| r[:type] == :all } if ( all && !all[:attributes].empty? ) hash[:required] = all[:attributes] end hash[:'x-requirements'] = extended_requirements unless extended_requirements.empty? end else hash[:'x-value_type'] = value_type.as_json_schema(shallow:true) end if opts[:allow_extra] hash[:additionalProperties] = if value_type == Attributor::Object true else value_type.as_json_schema(shallow: true) end end # TODO: minProperties and maxProperties and patternProperties hash end
attributes(**options, &key_spec)
click to toggle source
# File lib/attributor/types/hash.rb, line 91 def self.attributes(**options, &key_spec) raise @error if @error keys(**options, &key_spec) end
check_option!(name, _definition)
click to toggle source
# File lib/attributor/types/hash.rb, line 255 def self.check_option!(name, _definition) case name when :reference :ok # FIXME: ... actually do something smart when :dsl_compiler :ok when :case_insensitive_load unless key_type <= String raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{key_type.name}" end :ok when :allow_extra :ok else :unknown end end
construct(constructor_block, **options)
click to toggle source
# File lib/attributor/types/hash.rb, line 171 def self.construct(constructor_block, **options) return self if constructor_block.nil? unless @concrete return of(key: key_type, value: value_type) .construct(constructor_block, **options) end if options[:case_insensitive_load] && !(key_type <= String) raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{key_type.name}" end keys(**options, &constructor_block) self end
constructable?()
click to toggle source
# File lib/attributor/types/hash.rb, line 157 def self.constructable? true end
definition()
click to toggle source
# File lib/attributor/types/hash.rb, line 116 def self.definition opts = { key_type: @key_type, value_type: @value_type }.merge(@options) blocks = @saved_blocks.shift(@saved_blocks.size) compiler = dsl_class.new(self, **opts) compiler.parse(*blocks) if opts[:case_insensitive_load] == true @insensitive_map = keys.keys.each_with_object({}) do |k, map| map[k.downcase] = k end end rescue => e @error = InvalidDefinition.new(self, e) raise end
describe(shallow = false, example: nil)
click to toggle source
Calls superclass method
# File lib/attributor/types/hash.rb, line 448 def self.describe(shallow = false, example: nil) hash = super(shallow) hash[:key] = { type: key_type.describe(true) } if key_type if keys.any? # Spit keys if it's the root or if it's an anonymous structures if ( !shallow || self.name == nil) required_names_from_attr = [] # FIXME: change to :keys when the praxis doc browser supports displaying those hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes| required_names_from_attr << sub_name if sub_attribute.options[:required] == true sub_example = example.get(sub_name) if example sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example) end hash[:requirements] = requirements.each_with_object([]) do |req, list| described_req = req.describe(shallow) if described_req[:type] == :all # Add the names of the attributes that have the required flag too described_req[:attributes] |= required_names_from_attr required_names_from_attr = [] end list << described_req end # Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes unless required_names_from_attr.empty? hash[:requirements] << {type: :all, attributes: required_names_from_attr } end end else hash[:value] = { type: value_type.describe(true) } hash[:example] = example if example hash[:attributes] = {} end hash end
dsl_class()
click to toggle source
# File lib/attributor/types/hash.rb, line 136 def self.dsl_class @options[:dsl_compiler] || HashDSLCompiler end
dump(value, **opts)
click to toggle source
# File lib/attributor/types/hash.rb, line 249 def self.dump(value, **opts) if (loaded = load(value)) loaded.dump(**opts) end end
example(context = nil, **values)
click to toggle source
# File lib/attributor/types/hash.rb, line 223 def self.example(context = nil, **values) return new if key_type == Object && value_type == Object && keys.empty? context ||= ["#{Hash}-#{rand(10_000_000)}"] context = Array(context) if keys.any? result = new result.extend(ExampleMixin) result.lazy_attributes = example_contents(context, result, **values) else hash = ::Hash.new (rand(3) + 1).times do |i| example_key = key_type.example(context + ["at(#{i})"]) subcontext = context + ["at(#{example_key})"] hash[example_key] = value_type.example(subcontext) end result = new(hash) end result end
example_contents(context, parent, **values)
click to toggle source
# File lib/attributor/types/hash.rb, line 189 def self.example_contents(context, parent, **values) hash = ::Hash.new example_depth = context.size # Be smart about what attributes to use for the example: i.e. have into account complex requirements # that might have been defined in the hash like at_most(1).of ..., exactly(2).of ...etc. # But play it safe and default to the previous behavior in case there is any error processing them # ( that is until the SmartAttributeSelector class isn't fully tested and ready for prime time) begin stack = SmartAttributeSelector.new( requirements.map(&:describe), keys.keys , values) selected = stack.process rescue => e selected = keys.keys end keys.select{|n,attr| selected.include? n}.each do |sub_attribute_name, sub_attribute| if sub_attribute.attributes # TODO: add option to raise an exception in this case? next if example_depth > MAX_EXAMPLE_DEPTH end sub_context = generate_subcontext(context, sub_attribute_name) block = proc do value = values.fetch(sub_attribute_name) do sub_attribute.example(sub_context, parent: parent) end sub_attribute.load(value, sub_context) end hash[sub_attribute_name] = block end hash end
family()
click to toggle source
# File lib/attributor/types/hash.rb, line 65 def self.family 'hash' end
from_hash(object, context, recurse: false)
click to toggle source
# File lib/attributor/types/hash.rb, line 401 def self.from_hash(object, context, recurse: false) hash = new # if the hash definition includes named extra keys, initialize # its value from the object in case it provides some already. # this is to ensure it exists when we handle any extra keys # that may exist in the object later if extra_keys sub_context = generate_subcontext(context, extra_keys) v = object.fetch(extra_keys, {}) hash.set(extra_keys, v, context: sub_context, recurse: recurse) end object.each do |k, val| next if k == extra_keys sub_context = generate_subcontext(context, k) hash.set(k, val, context: sub_context, recurse: recurse) end # handle default values for missing keys keys.each do |key_name, attribute| next if hash.key?(key_name) # Cache default values to avoid a whole loading call for the attribute default = if @cached_defaults.key?(key_name) @cached_defaults[key_name] else sub_context = generate_subcontext(context, key_name) @cached_defaults[key_name] = attribute.load(nil, sub_context, recurse: recurse) end hash[key_name] = default unless default.nil? end hash end
generate_subcontext(context, key_name)
click to toggle source
# File lib/attributor/types/hash.rb, line 311 def self.generate_subcontext(context, key_name) context + ["key(#{key_name.inspect})"] end
inherited(klass)
click to toggle source
# File lib/attributor/types/hash.rb, line 73 def self.inherited(klass) k = key_type v = value_type klass.instance_eval do @saved_blocks = [] @options = { allow_extra: false } @keys = {} @key_type = k @value_type = v @key_attribute = Attribute.new(@key_type) @value_attribute = Attribute.new(@value_type) @requirements = [] @cached_defaults = {} @error = false end end
json_schema_type()
click to toggle source
# File lib/attributor/types/hash.rb, line 536 def self.json_schema_type :object end
key_type=(key_type)
click to toggle source
# File lib/attributor/types/hash.rb, line 53 def self.key_type=(key_type) @key_type = Attributor.resolve_type(key_type) @key_attribute = Attribute.new(@key_type) @concrete = true end
keys(**options, &key_spec)
click to toggle source
# File lib/attributor/types/hash.rb, line 97 def self.keys(**options, &key_spec) raise @error if @error if block_given? @saved_blocks << key_spec @options.merge!(options) elsif @saved_blocks.any? definition end @keys end
load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **_options)
click to toggle source
# File lib/attributor/types/hash.rb, line 273 def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **_options) return value if value.is_a?(self) return nil if value.nil? && !recurse context = Array(context) loaded_value = self.parse(value, context) return from_hash(loaded_value, context, recurse: recurse) if keys.any? load_generic(loaded_value, context) end
load_generic(value, context)
click to toggle source
# File lib/attributor/types/hash.rb, line 303 def self.load_generic(value, context) return new(value) if key_type == Object && value_type == Object value.each_with_object(new) do |(k, v), obj| obj[key_type.load(k, context)] = value_type.load(v, context) end end
native_type()
click to toggle source
# File lib/attributor/types/hash.rb, line 140 def self.native_type self end
new(contents = {})
click to toggle source
# File lib/attributor/types/hash.rb, line 607 def initialize(contents = {}) @validating = false @dumping = false @contents = contents end
of(key: @key_type, value: @value_type)
click to toggle source
parse(value, context)
click to toggle source
# File lib/attributor/types/hash.rb, line 285 def self.parse(value, context) if value.nil? {} elsif value.is_a?(Attributor::Hash) value.contents elsif value.is_a?(::Hash) value elsif value.is_a?(::String) decode_json(value, context) elsif value.respond_to?(:to_h) value.to_h elsif value.respond_to?(:to_hash) # Deprecate this in lieu of to_h only? value.to_hash else raise Attributor::IncompatibleTypeError.new(context: context, value_type: value.class, type: self) end end
slice!(*keys)
click to toggle source
# File lib/attributor/types/hash.rb, line 46 def self.slice!(*keys) missing_keys = keys - @keys.keys raise AttributorException, "Cannot slice! this type, because it does not contain one or more of the requested keys: #{missing_keys}" unless missing_keys.empty? instance_variable_set(:@keys, @keys.slice(*keys)) self end
valid_type?(type)
click to toggle source
# File lib/attributor/types/hash.rb, line 144 def self.valid_type?(type) type.is_a?(self) || type.is_a?(::Hash) end
validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute)
click to toggle source
# File lib/attributor/types/hash.rb, line 439 def self.validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute) context = [context] if context.is_a? ::String unless object.is_a?(self) raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}." end object.validate(context) end
value_type=(value_type)
click to toggle source
# File lib/attributor/types/hash.rb, line 59 def self.value_type=(value_type) @value_type = Attributor.resolve_type(value_type) @value_attribute = Attribute.new(@value_type) @concrete = true end
Public Instance Methods
==(other)
click to toggle source
# File lib/attributor/types/hash.rb, line 630 def ==(other) contents == other || (other.respond_to?(:contents) ? contents == other.contents : false) end
[](k)
click to toggle source
# File lib/attributor/types/hash.rb, line 543 def [](k) @contents[k] end
[]=(k, v)
click to toggle source
# File lib/attributor/types/hash.rb, line 551 def []=(k, v) @contents[k] = v end
_get_attr(k)
click to toggle source
# File lib/attributor/types/hash.rb, line 547 def _get_attr(k) self[k] end
delete(key)
click to toggle source
# File lib/attributor/types/hash.rb, line 601 def delete(key) @contents.delete(key) end
dump(**opts)
click to toggle source
# File lib/attributor/types/hash.rb, line 702 def dump(**opts) return CIRCULAR_REFERENCE_MARKER if @dumping @dumping = true contents.each_with_object({}) do |(k, v), hash| k = key_attribute.dump(k, **opts) v = if (attribute_for_value = self.class.keys[k]) attribute_for_value.dump(v, **opts) else value_attribute.dump(v, **opts) end hash[k] = v end ensure @dumping = false end
each(&block)
click to toggle source
# File lib/attributor/types/hash.rb, line 555 def each(&block) @contents.each(&block) end
Also aliased as: each_pair
empty?()
click to toggle source
# File lib/attributor/types/hash.rb, line 573 def empty? @contents.empty? end
generate_subcontext(context, key_name)
click to toggle source
# File lib/attributor/types/hash.rb, line 319 def generate_subcontext(context, key_name) self.class.generate_subcontext(context, key_name) end
get(key, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key))
click to toggle source
# File lib/attributor/types/hash.rb, line 323 def get(key, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key)) key = self.class.key_attribute.load(key, context) return self.get_generic(key, context) if self.class.keys.empty? value = @contents[key] # FIXME: getting an unset value here should not force it in the hash if (attribute = self.class.keys[key]) loaded_value = attribute.load(value, context) return nil if loaded_value.nil? return self[key] = loaded_value end if self.class.options[:case_insensitive_load] key = self.class.insensitive_map[key.downcase] return get(key, context: context) end if self.class.options[:allow_extra] return @contents[key] = self.class.value_attribute.load(value, context) if self.class.extra_keys.nil? extra_keys_key = self.class.extra_keys if @contents.key? extra_keys_key return @contents[extra_keys_key].get(key, context: context) end end raise LoadError, "Unknown key received: #{key.inspect} for #{Attributor.humanize_context(context)}" end
get_generic(key, context)
click to toggle source
# File lib/attributor/types/hash.rb, line 354 def get_generic(key, context) if @contents.key? key value = @contents[key] loaded_value = value_attribute.load(value, context) return self[key] = loaded_value elsif self.class.options[:case_insensitive_load] key = key.downcase @contents.each do |k, _v| return get(key, context: context) if key == k.downcase end end nil end
key?(k)
click to toggle source
# File lib/attributor/types/hash.rb, line 577 def key?(k) @contents.key?(k) end
Also aliased as: has_key?
key_attribute()
click to toggle source
# File lib/attributor/types/hash.rb, line 622 def key_attribute self.class.key_attribute end
key_type()
click to toggle source
# File lib/attributor/types/hash.rb, line 614 def key_type self.class.key_type end
keys()
click to toggle source
# File lib/attributor/types/hash.rb, line 565 def keys @contents.keys end
merge(h)
click to toggle source
# File lib/attributor/types/hash.rb, line 582 def merge(h) case h when self.class self.class.new(contents.merge(h.contents)) when Attributor::Hash source_key_type = self.class.key_type source_value_type = self.class.value_type # Allow merging hashes, but we'll need to coerce keys and/or values if they aren't the same type coerced_contents = h.contents.each_with_object({}) do |(key, val), object| k = (source_key_type && !k.is_a?(source_key_type)) ? source_key_type.load(key) : key v = (source_value_type && !k.is_a?(source_value_type)) ? source_value_type.load(val) : val object[k] = v end self.class.new(contents.merge(coerced_contents)) else raise TypeError, "no implicit conversion of #{h.class} into Attributor::Hash" end end
set(key, value, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key), recurse: false)
click to toggle source
# File lib/attributor/types/hash.rb, line 368 def set(key, value, context: generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT, key), recurse: false) key = self.class.key_attribute.load(key, context) if self.class.keys.empty? return self[key] = self.class.value_attribute.load(value, context) end if (attribute = self.class.keys[key]) return self[key] = attribute.load(value, context, recurse: recurse) end if self.class.options[:case_insensitive_load] key = self.class.insensitive_map[key.downcase] return set(key, value, context: context) end if self.class.options[:allow_extra] return self[key] = self.class.value_attribute.load(value, context) if self.class.extra_keys.nil? extra_keys_key = self.class.extra_keys unless @contents.key? extra_keys_key extra_keys_value = self.class.keys[extra_keys_key].load({}) @contents[extra_keys_key] = extra_keys_value end return self[extra_keys_key].set(key, value, context: context) end raise LoadError, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}" end
size()
click to toggle source
# File lib/attributor/types/hash.rb, line 561 def size @contents.size end
to_h()
click to toggle source
# File lib/attributor/types/hash.rb, line 315 def to_h Attributor.recursive_to_h(@contents) end
validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
click to toggle source
# File lib/attributor/types/hash.rb, line 634 def validate(context = Attributor::DEFAULT_ROOT_CONTEXT) @validating = true context = [context] if context.is_a? ::String if self.class.keys.any? extra_keys = @contents.keys - self.class.keys.keys if extra_keys.any? && !self.class.options[:allow_extra] return extra_keys.collect do |k| "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}" end end self.validate_keys(context) else self.validate_generic(context) end ensure @validating = false end
validate_generic(context)
click to toggle source
# File lib/attributor/types/hash.rb, line 687 def validate_generic(context) @contents.each_with_object([]) do |(key, value), errors| # FIXME: the sub contexts and error messages don't really make sense here unless key_type == Attributor::Object sub_context = context + ["key(#{key.inspect})"] errors.concat key_attribute.validate(key, sub_context) end unless value_type == Attributor::Object sub_context = context + ["value(#{value.inspect})"] errors.concat value_attribute.validate(value, sub_context) end end end
validate_keys(context)
click to toggle source
# File lib/attributor/types/hash.rb, line 653 def validate_keys(context) errors = [] keys_provided = [] self.class.keys.each do |key, attribute| sub_context = self.class.generate_subcontext(context, key) value = _get_attr(key) keys_provided << key if @contents.key?(key) if value.respond_to?(:validating) # really, it's a thing with sub-attributes next if value.validating end # Isn't this handled by the requirements validation? NO! we might want to combine if attribute.options[:required] && !@contents.key?(key) errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."] end if @contents[key].nil? if !Attribute.nullable_attribute?(attribute.options) && @contents.key?(key) errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] end # No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no # context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways)) else errors.concat attribute.validate(value, sub_context) end end self.class.requirements.each do |requirement| validation_errors = requirement.validate(keys_provided, context) errors.concat(validation_errors) unless validation_errors.empty? end errors end
value_attribute()
click to toggle source
# File lib/attributor/types/hash.rb, line 626 def value_attribute self.class.value_attribute end
value_type()
click to toggle source
# File lib/attributor/types/hash.rb, line 618 def value_type self.class.value_type end
values()
click to toggle source
# File lib/attributor/types/hash.rb, line 569 def values @contents.values end