Class: Attributor::Attribute
- Inherits:
-
Object
- Object
- Attributor::Attribute
- Defined in:
- lib/attributor/attribute.rb
Overview
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?
Constant Summary collapse
- TOP_LEVEL_OPTIONS =
[ :description, :values, :default, :example, :required, :required_if ]
- INTERNAL_OPTIONS =
Options we don’t want to expose when describing attributes
[:dsl_compiler,:dsl_compiler_options]
Instance Attribute Summary collapse
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#type ⇒ Object
readonly
Returns the value of attribute type.
Instance Method Summary collapse
- #==(attribute) ⇒ Object
- #attributes ⇒ Object
-
#check_option!(name, definition) ⇒ Object
TODO: override in type subclass.
- #check_options! ⇒ Object
- #describe(shallow = true) ⇒ Object
- #dump(value, **opts) ⇒ Object
- #example(context = nil, parent: nil, values: {}) ⇒ Object
-
#initialize(type, options = {}, &block) ⇒ Attribute
constructor
@options: metadata about the attribute @block: code definition for struct attributes (nil for predefined types or leaf/simple types).
- #load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) ⇒ Object
- #parse(value, context = Attributor::DEFAULT_ROOT_CONTEXT) ⇒ Object
-
#validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT) ⇒ Object
Validates stuff and checks dependencies.
- #validate_missing_value(context) ⇒ Object
- #validate_type(value, context) ⇒ Object
Constructor Details
#initialize(type, options = {}, &block) ⇒ Attribute
@options: metadata about the attribute @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
13 14 15 16 17 18 19 20 21 22 |
# File 'lib/attributor/attribute.rb', line 13 def initialize(type, ={}, &block) @type = Attributor.resolve_type(type, , block) = if @type.respond_to?(:options) = @type..merge() end end |
Instance Attribute Details
#options ⇒ Object (readonly)
Returns the value of attribute options.
9 10 11 |
# File 'lib/attributor/attribute.rb', line 9 def end |
#type ⇒ Object (readonly)
Returns the value of attribute type.
9 10 11 |
# File 'lib/attributor/attribute.rb', line 9 def type @type end |
Instance Method Details
#==(attribute) ⇒ Object
24 25 26 27 28 29 |
# File 'lib/attributor/attribute.rb', line 24 def ==(attribute) raise ArgumentError, "can not compare Attribute with #{attribute.class.name}" unless attribute.kind_of?(Attribute) self.type == attribute.type && self. == attribute. end |
#attributes ⇒ Object
146 147 148 149 150 151 152 |
# File 'lib/attributor/attribute.rb', line 146 def attributes if (@type_has_attributes ||= type.respond_to?(:attributes)) type.attributes else nil end end |
#check_option!(name, definition) ⇒ Object
TODO: override in type subclass
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/attributor/attribute.rb', line 245 def check_option!(name, definition) case name when :values raise AttributorException.new("Allowed set of values requires an array. Got (#{definition})") unless definition.is_a? ::Array when :default raise AttributorException.new("Default value doesn't have the correct attribute type. Got (#{definition.inspect})") unless self.type.valid_type?(definition) || definition.kind_of?(Proc) when :description raise AttributorException.new("Description value must be a string. Got (#{definition})") unless definition.is_a? ::String when :required raise AttributorException.new("Required must be a boolean") unless !!definition == definition # Boolean check raise AttributorException.new("Required cannot be enabled in combination with :default") if definition == true && .has_key?(:default) when :required_if raise AttributorException.new("Required_if must be a String, a Hash definition or a Proc") unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc) raise AttributorException.new("Required_if cannot be specified together with :required") if self.[:required] when :example unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition) raise AttributorException.new("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 else return :unknown # unknown option end :ok # passes end |
#check_options! ⇒ Object
231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/attributor/attribute.rb', line 231 def self..each do |option_name, option_value| if self.check_option!(option_name, option_value) == :unknown if self.type.check_option!(option_name, option_value) == :unknown raise AttributorException.new("unsupported option: #{option_name} with value: #{option_value.inspect} for attribute: #{self.inspect}") end end end true end |
#describe(shallow = true) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/attributor/attribute.rb', line 74 def describe(shallow=true) description = { } # Clone the common options TOP_LEVEL_OPTIONS.each do |option_name| description[option_name] = self.[option_name] if self..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 = self..keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS description[:options] = {} unless .empty? .each do |opt_name| description[:options][opt_name] = self.[opt_name] end # Change the reference option to the actual class name. if ( reference = self.[:reference] ) description[:options][:reference] = reference.name end description[:type] = self.type.describe(shallow) description end |
#dump(value, **opts) ⇒ Object
55 56 57 |
# File 'lib/attributor/attribute.rb', line 55 def dump(value, **opts) type.dump(value, opts) end |
#example(context = nil, parent: nil, values: {}) ⇒ Object
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/attributor/attribute.rb', line 100 def example(context=nil, parent: nil, values:{}) 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) end if self..has_key? :example val = self.[:example] case val when ::String # FIXME: spec this properly to use self.type.native_type val when ::Regexp self.load(val.gen,context) when ::Array # TODO: handle arrays of non native types, i.e. arrays of regexps.... ? val.pick 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 raise AttributorException, "unknown example attribute type, got: #{val}" end else if (option_values = self.[:values]) option_values.pick else if type.respond_to?(:attributes) self.type.example(context, values) else self.type.example(context, options: self.) end end end end |
#load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/attributor/attribute.rb', line 40 def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **) value = type.load(value,context,**) unless value.nil? # just in case type.load(value) returned nil, even though value is not nil. if value.nil? value = self.[:default] if self.[:default] 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) ⇒ Object
32 33 34 35 36 37 |
# File 'lib/attributor/attribute.rb', line 32 def parse(value, context=Attributor::DEFAULT_ROOT_CONTEXT) object = self.load(value,context) errors = self.validate(object,context) [ object, errors ] end |
#validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT) ⇒ Object
Validates stuff and checks dependencies
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/attributor/attribute.rb', line 156 def validate(object, context=Attributor::DEFAULT_ROOT_CONTEXT ) raise "INVALID CONTEXT!! #{context}" unless context # Validate any requirements, absolute or conditional, and return. if object.nil? # == Attributor::UNSET # With no value, we can only validate whether that is acceptable or not and return. # Beyond that, no further validation should be done. return self.validate_missing_value(context) end # TODO: support validation for other types of conditional dependencies based on values of other attributes errors = self.validate_type(object,context) # End validation if we don't even have the proper type to begin with return errors if errors.any? if self.[:values] && !self.[:values].include?(object) errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{self.options[:values].inspect} " end errors + self.type.validate(object,context,self) end |
#validate_missing_value(context) ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/attributor/attribute.rb', line 181 def validate_missing_value(context) raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable # Missing attribute was required if :required option was set return ["Attribute #{Attributor.humanize_context(context)} is required"] if self.[:required] # Missing attribute was not required if :required_if (and :required) # option was NOT set requirement = self.[:required_if] return [] unless requirement case requirement when ::String key_path = requirement predicate = nil when ::Hash # TODO: support multiple dependencies? key_path = requirement.keys.first predicate = requirement.values.first else # should never get here if the option validation worked... raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}") end # chop off the last part requirement_context = context[0..-2] requirement_context_string = requirement_context.join(Attributor::SEPARATOR) # FIXME: we're having to reconstruct a string context just to use the resolver...smell. if AttributeResolver.current.check(requirement_context_string, key_path, predicate) = "Attribute #{Attributor.humanize_context(context)} is required when #{key_path} " # give a hint about what the full path for a relative key_path would be unless key_path[0..0] == Attributor::AttributeResolver::ROOT_PREFIX << "(for #{Attributor.humanize_context(requirement_context)}) " end if predicate << "matches #{predicate.inspect}." else << "is present." end [] else [] end end |
#validate_type(value, context) ⇒ Object
60 61 62 63 64 65 66 67 68 69 |
# File 'lib/attributor/attribute.rb', line 60 def validate_type(value, context) # delegate check to type subclass if it exists unless self.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: #{self.type.name})" return [msg] end [] end |