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, :null, :custom_data].freeze
- INTERNAL_OPTIONS =
Options we don’t want to expose when describing attributes
[:dsl_compiler, :dsl_compiler_options].freeze
Class Attribute Summary collapse
-
.custom_options ⇒ Object
Returns the value of attribute custom_options.
Instance Attribute Summary collapse
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#type ⇒ Object
readonly
Returns the value of attribute type.
Class Method Summary collapse
- .custom_option(name, attr_type, options = {}, &block) ⇒ Object
-
.default_for_null ⇒ Object
Default value for a non-specified null: option.
-
.nullable_attribute?(options) ⇒ Boolean
It is only nullable if there is an explicit null: true (or if it’s not passed/set, and the default is true).
Instance Method Summary collapse
- #==(other) ⇒ Object
-
#as_json_schema(shallow: true, example: nil) ⇒ Object
FiXME: pass and utilize the “shallow” parameter required options type example UTILIZE THIS SITE! jsonschema.net/#/.
- #attributes ⇒ Object
- #check_custom_option(name, definition) ⇒ Object
-
#check_option!(name, definition) ⇒ Object
TODO: override in type subclass.
- #check_options! ⇒ Object
- #describe(shallow = true, example: nil) ⇒ Object
- #describe_option(option_name) ⇒ Object
- #dump(value, **opts) ⇒ Object
- #duplicate(type: nil, options: nil) ⇒ 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_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)
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/attributor/attribute.rb', line 41 def initialize(type, = {}, &block) @type = Attributor.resolve_type(type, , block) @options = @type.respond_to?(:options) ? @type..merge() : # 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..keys & @options.keys).each do |custom_key| current_option_name = custom_key @options[custom_key] = self.class.[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.}" end end |
Class Attribute Details
.custom_options ⇒ Object
Returns the value of attribute custom_options.
29 30 31 |
# File 'lib/attributor/attribute.rb', line 29 def @custom_options end |
Instance Attribute Details
#options ⇒ Object (readonly)
Returns the value of attribute options.
24 25 26 |
# File 'lib/attributor/attribute.rb', line 24 def @options end |
#type ⇒ Object (readonly)
Returns the value of attribute type.
24 25 26 |
# File 'lib/attributor/attribute.rb', line 24 def type @type end |
Class Method Details
.custom_option(name, attr_type, options = {}, &block) ⇒ Object
32 33 34 35 36 37 |
# File 'lib/attributor/attribute.rb', line 32 def self.custom_option(name, attr_type, = {}, &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.[name] = Attributor::Attribute.new(attr_type, , &block) end |
.default_for_null ⇒ Object
Default value for a non-specified null: option
247 248 249 |
# File 'lib/attributor/attribute.rb', line 247 def self.default_for_null false end |
.nullable_attribute?(options) ⇒ Boolean
It is only nullable if there is an explicit null: true (or if it’s not passed/set, and the default is true)
252 253 254 |
# File 'lib/attributor/attribute.rb', line 252 def self.nullable_attribute?() !.key?(:null) ? default_for_null : [:null] end |
Instance Method Details
#==(other) ⇒ Object
66 67 68 69 70 71 |
# 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 && == other. end |
#as_json_schema(shallow: true, example: nil) ⇒ Object
FiXME: pass and utilize the “shallow” parameter required options type example UTILIZE THIS SITE! jsonschema.net/#/
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 |
# 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. ) description[:description] = self.[:description] if self.[:description] description[:enum] = self.[:values] if self.[:values] if the_default = self.[: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.[: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..each do |name, _| description["x-#{name}".to_sym] = self.[name] if self..key?(name) end description end |
#attributes ⇒ Object
242 243 244 |
# 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) ⇒ Object
321 322 323 324 325 326 327 328 |
# File 'lib/attributor/attribute.rb', line 321 def check_custom_option(name, definition) attribute = self.class..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) ⇒ Object
TODO: override in type subclass
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/attributor/attribute.rb', line 292 def check_option!(name, definition) return check_custom_option(name, definition) if self.class..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) [: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 :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! ⇒ Object
280 281 282 283 284 285 286 287 288 289 |
# File 'lib/attributor/attribute.rb', line 280 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
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# 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..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 = .keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS description[:options] = {} unless .empty? .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 = [: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) ⇒ Object
175 176 177 |
# File 'lib/attributor/attribute.rb', line 175 def describe_option( option_name ) self.type.describe_option( option_name, self.[option_name] ) end |
#dump(value, **opts) ⇒ Object
109 110 111 |
# File 'lib/attributor/attribute.rb', line 109 def dump(value, **opts) type.dump(value, **opts) end |
#duplicate(type: nil, options: nil) ⇒ Object
59 60 61 62 63 64 |
# 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, ) if end end |
#example(context = nil, parent: nil, values: {}) ⇒ Object
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 |
# 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 .key? :example loaded = (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 [:values].sample 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
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/attributor/attribute.rb', line 156 def (parent, context) val = [: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) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/attributor/attribute.rb', line 80 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
73 74 75 76 77 78 |
# 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) ⇒ Object
Validates stuff and checks dependencies
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# 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?() 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? && [:values] && ![:values].include?(object) errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{[:values].inspect} " end end return errors if errors.any? object.nil? ? errors : errors + type.validate(object, context, self) end |
#validate_type(value, context) ⇒ Object
113 114 115 116 117 118 119 120 121 |
# 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 |