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, :custom_data ]
- 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, example: nil) ⇒ Object
- #dump(value, **opts) ⇒ Object
- #example(context = nil, parent: nil, values: {}) ⇒ Object
- #example_from_options(parent, context) ⇒ 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)
26 27 28 29 30 31 32 33 34 35 |
# File 'lib/attributor/attribute.rb', line 26 def initialize(type, ={}, &block) @type = Attributor.resolve_type(type, , block) @options = if @type.respond_to?(:options) @options = @type..merge(@options) end end |
Instance Attribute Details
#options ⇒ Object (readonly)
Returns the value of attribute options.
22 23 24 |
# File 'lib/attributor/attribute.rb', line 22 def @options end |
#type ⇒ Object (readonly)
Returns the value of attribute type.
22 23 24 |
# File 'lib/attributor/attribute.rb', line 22 def type @type end |
Instance Method Details
#==(attribute) ⇒ Object
37 38 39 40 41 42 |
# File 'lib/attributor/attribute.rb', line 37 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
184 185 186 187 188 189 190 |
# File 'lib/attributor/attribute.rb', line 184 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
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/attributor/attribute.rb', line 283 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) self.[:default] = self.load(definition) unless 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 when :custom_data raise AttributorException.new("custom_data must be a Hash. Got (#{definition})") unless definition.is_a?(::Hash) else return :unknown # unknown option end :ok # passes end |
#check_options! ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/attributor/attribute.rb', line 269 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, example: nil) ⇒ Object
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 |
# File 'lib/attributor/attribute.rb', line 102 def describe(shallow=true, example: nil) 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, example: example ) # Move over any example from the type, into the attribute itself if ( ex = description[:type].delete(:example) ) description[:example] = self.dump(ex) end description end |
#dump(value, **opts) ⇒ Object
82 83 84 |
# File 'lib/attributor/attribute.rb', line 82 def dump(value, **opts) type.dump(value, **opts) end |
#example(context = nil, parent: nil, values: {}) ⇒ Object
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/attributor/attribute.rb', line 155 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) else context = Attributor::DEFAULT_ROOT_CONTEXT end if self..has_key? :example loaded = (parent, context) errors = self.validate(loaded, context) raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any? loaded 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 |
#example_from_options(parent, context) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/attributor/attribute.rb', line 134 def (parent, context) val = self.[:example] generated = case val when ::Regexp val.gen 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 self.load( generated, context ) end |
#load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/attributor/attribute.rb', line 53 def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **) value = type.load(value,context,**) if value.nil? && self..has_key?(:default) defined_val = self.[: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.}" end |
#parse(value, context = Attributor::DEFAULT_ROOT_CONTEXT) ⇒ Object
45 46 47 48 49 50 |
# File 'lib/attributor/attribute.rb', line 45 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
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/attributor/attribute.rb', line 194 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.[:values].inspect} " end errors + self.type.validate(object,context,self) end |
#validate_missing_value(context) ⇒ Object
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/attributor/attribute.rb', line 219 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
87 88 89 90 91 92 93 94 95 96 |
# File 'lib/attributor/attribute.rb', line 87 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 |