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].freeze
- INTERNAL_OPTIONS =
Options we don’t want to expose when describing attributes
[:dsl_compiler, :dsl_compiler_options].freeze
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
- #==(other) ⇒ 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)
27 28 29 30 31 32 33 34 |
# File 'lib/attributor/attribute.rb', line 27 def initialize(type, = {}, &block) @type = Attributor.resolve_type(type, , block) @options = @options = @type..merge(@options) if @type.respond_to?(:options) end |
Instance Attribute Details
#options ⇒ Object (readonly)
Returns the value of attribute options.
23 24 25 |
# File 'lib/attributor/attribute.rb', line 23 def @options end |
#type ⇒ Object (readonly)
Returns the value of attribute type.
23 24 25 |
# File 'lib/attributor/attribute.rb', line 23 def type @type end |
Instance Method Details
#==(other) ⇒ Object
36 37 38 39 40 41 |
# File 'lib/attributor/attribute.rb', line 36 def ==(other) raise ArgumentError, "can not compare Attribute with #{other.class.name}" unless other.is_a?(Attribute) type == other.type && == other. end |
#attributes ⇒ Object
175 176 177 |
# File 'lib/attributor/attribute.rb', line 175 def attributes type.attributes if @type_has_attributes ||= type.respond_to?(:attributes) end |
#check_option!(name, definition) ⇒ Object
TODO: override in type subclass
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/attributor/attribute.rb', line 265 def check_option!(name, definition) 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) [: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 && .key?(:default) when :required_if raise AttributorException, '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, 'Required_if cannot be specified together with :required' if [:required] 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! ⇒ Object
253 254 255 256 257 258 259 260 261 262 |
# File 'lib/attributor/attribute.rb', line 253 def .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) ⇒ Object
97 98 99 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 |
# File 'lib/attributor/attribute.rb', line 97 def describe(shallow = true, example: nil) description = {} # Clone the common options TOP_LEVEL_OPTIONS.each do |option_name| description[option_name] = [option_name] if .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 = .keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS description[:options] = {} unless .empty? .each do |opt_name| description[:options][opt_name] = [opt_name] end # Change the reference option to the actual class name. if (reference = [: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 |
#dump(value, **opts) ⇒ Object
79 80 81 |
# File 'lib/attributor/attribute.rb', line 79 def dump(value, **opts) type.dump(value, **opts) end |
#example(context = nil, parent: nil, values: {}) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/attributor/attribute.rb', line 149 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 .key? :example loaded = (parent, context) errors = validate(loaded, context) raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any? return loaded end return [:values].pick if .key? :values if type.respond_to?(:attributes) type.example(context, values) else type.example(context, options: ) end end |
#example_from_options(parent, context) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/attributor/attribute.rb', line 128 def (parent, context) val = [: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 load(generated, context) end |
#load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options) ⇒ Object
50 51 52 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 |
# File 'lib/attributor/attribute.rb', line 50 def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **) value = type.load(value, context, **) if value.nil? && self..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
43 44 45 46 47 48 |
# File 'lib/attributor/attribute.rb', line 43 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) ⇒ Object
Validates stuff and checks dependencies
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/attributor/attribute.rb', line 180 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 validate_missing_value(context) end # TODO: support validation for other types of conditional dependencies based on values of other attributes errors = validate_type(object, context) # End validation if we don't even have the proper type to begin with return errors if errors.any? if [:values] && ![:values].include?(object) errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{[:values].inspect} " end errors + type.validate(object, context, self) end |
#validate_missing_value(context) ⇒ Object
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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/attributor/attribute.rb', line 204 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 [:required] # Missing attribute was not required if :required_if (and :required) # option was NOT set requirement = [: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, "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
83 84 85 86 87 88 89 90 91 92 |
# File 'lib/attributor/attribute.rb', line 83 def validate_type(value, context) # delegate check to type subclass if it exists unless 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})" return [msg] end [] end |