Class: Attributor::Hash

Inherits:
Object
  • Object
show all
Includes:
Container, Dumpable, Enumerable
Defined in:
lib/attributor/types/hash.rb

Direct Known Subclasses

Model

Constant Summary collapse

MAX_EXAMPLE_DEPTH =
5
CIRCULAR_REFERENCE_MARKER =
'...'.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Container

included

Constructor Details

#initialize(contents = {}) ⇒ Hash

Returns a new instance of Hash.



530
531
532
533
534
535
# File 'lib/attributor/types/hash.rb', line 530

def initialize(contents={})
  @validating = false
  @dumping = false

  @contents = contents
end

Class Attribute Details

.extra_keysObject

Returns the value of attribute extra_keys.



32
33
34
# File 'lib/attributor/types/hash.rb', line 32

def extra_keys
  @extra_keys
end

.insensitive_mapObject (readonly)

Returns the value of attribute insensitive_map.



31
32
33
# File 'lib/attributor/types/hash.rb', line 31

def insensitive_map
  @insensitive_map
end

.key_attributeObject (readonly)

Returns the value of attribute key_attribute.



30
31
32
# File 'lib/attributor/types/hash.rb', line 30

def key_attribute
  @key_attribute
end

.key_typeObject

Returns the value of attribute key_type.



28
29
30
# File 'lib/attributor/types/hash.rb', line 28

def key_type
  @key_type
end

.optionsObject (readonly)

Returns the value of attribute options.



28
29
30
# File 'lib/attributor/types/hash.rb', line 28

def options
  @options
end

.requirementsObject (readonly)

Returns the value of attribute requirements.



33
34
35
# File 'lib/attributor/types/hash.rb', line 33

def requirements
  @requirements
end

.value_attributeObject (readonly)

Returns the value of attribute value_attribute.



29
30
31
# File 'lib/attributor/types/hash.rb', line 29

def value_attribute
  @value_attribute
end

.value_typeObject

Returns the value of attribute value_type.



28
29
30
# File 'lib/attributor/types/hash.rb', line 28

def value_type
  @value_type
end

Instance Attribute Details

#contentsObject (readonly)

TODO: Think about the format of the subcontexts to use: let’s use .at(key.to_s)



472
473
474
# File 'lib/attributor/types/hash.rb', line 472

def contents
  @contents
end

#dumpingObject (readonly)

Returns the value of attribute dumping.



528
529
530
# File 'lib/attributor/types/hash.rb', line 528

def dumping
  @dumping
end

#validatingObject (readonly)

Returns the value of attribute validating.



528
529
530
# File 'lib/attributor/types/hash.rb', line 528

def validating
  @validating
end

Class Method Details

.add_requirement(req) ⇒ Object



146
147
148
149
150
151
152
153
154
155
# File 'lib/attributor/types/hash.rb', line 146

def self.add_requirement(req)
  @requirements << req
  return unless req.attr_names
  non_existing = req.attr_names - self.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 #{self.attributes.keys}"
  end

end

.attributes(**options, &key_spec) ⇒ Object

Raises:

  • (@error)


83
84
85
86
87
# File 'lib/attributor/types/hash.rb', line 83

def self.attributes(**options, &key_spec)
  raise @error if @error

  self.keys(options, &key_spec)
end

.check_option!(name, definition) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/attributor/types/hash.rb', line 242

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 self.key_type <= String
      raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{self.key_type.name}"
    end
    :ok
  when :allow_extra
    :ok
  else
    :unknown
  end
end

.construct(constructor_block, **options) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/attributor/types/hash.rb', line 157

def self.construct(constructor_block, **options)
  return self if constructor_block.nil?

  unless @concrete
    return self.of(key:self.key_type, value: self.value_type)
    .construct(constructor_block,**options)
  end

  if options[:case_insensitive_load] && !(self.key_type <= String)
    raise Attributor::AttributorException.new(":case_insensitive_load may not be used with keys of type #{self.key_type.name}")
  end

  self.keys(options, &constructor_block)
  self
end

.constructable?Boolean

Returns:



142
143
144
# File 'lib/attributor/types/hash.rb', line 142

def self.constructable?
  true
end

.definitionObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/attributor/types/hash.rb', line 101

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 = self.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) ⇒ Object



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/attributor/types/hash.rb', line 433

def self.describe(shallow=false, example: nil)
  hash = super(shallow)

  if key_type
    hash[:key] = {type: key_type.describe(true)}
  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 = []
      # 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 << 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] = 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
          required_names = []
        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.empty?
        hash[:requirements] << {type: :all, attributes: required_names }
      end
    end
  else
    hash[:value] = {type: value_type.describe(true)}
  end

  hash
end

.dsl_classObject



121
122
123
# File 'lib/attributor/types/hash.rb', line 121

def self.dsl_class
  @options[:dsl_compiler] || HashDSLCompiler
end

.dump(value, **opts) ⇒ Object



233
234
235
236
237
238
239
# File 'lib/attributor/types/hash.rb', line 233

def self.dump(value, **opts)
  if loaded = self.load(value)
    loaded.dump(**opts)
  else
    nil
  end
end

.example(context = nil, **values) ⇒ Object



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
229
230
# File 'lib/attributor/types/hash.rb', line 203

def self.example(context=nil, **values)

  if (key_type == Object && value_type == Object && self.keys.empty?)
    return self.new
  end

  context ||= ["#{Hash}-#{rand(10000000)}"]
  context = Array(context)

  if self.keys.any?
    result = self.new
    result.extend(ExampleMixin)

    result.lazy_attributes = self.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 = self.new(hash)
  end

  result
end

.example_contents(context, parent, **values) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/attributor/types/hash.rb', line 174

def self.example_contents(context, parent, **values)

  hash = ::Hash.new
  example_depth = context.size

  self.keys.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 = self.generate_subcontext(context,sub_attribute_name)
    block = Proc.new 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

.familyObject



57
58
59
# File 'lib/attributor/types/hash.rb', line 57

def self.family
  'hash'
end

.from_hash(object, context, recurse: false) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/attributor/types/hash.rb', line 391

def self.from_hash(object,context, recurse: false)
  hash = self.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 self.extra_keys
    sub_context = self.generate_subcontext(context,self.extra_keys)
    v = object.fetch(self.extra_keys, {})
    hash.set(self.extra_keys, v, context: sub_context, recurse: recurse)
  end

  object.each do |k,val|
    next if k == self.extra_keys

    sub_context = self.generate_subcontext(context,k)
    hash.set(k, val, context: sub_context, recurse: recurse)
  end

  # handle default values for missing keys
  self.keys.each do |key_name, attribute|
    next if hash.key?(key_name)
    sub_context = self.generate_subcontext(context,key_name)
    default = attribute.load(nil, sub_context, recurse: recurse)
    hash[key_name] = default unless default.nil?
  end

  hash
end

.generate_subcontext(context, key_name) ⇒ Object



293
294
295
# File 'lib/attributor/types/hash.rb', line 293

def self.generate_subcontext(context, key_name)
  context + ["key(#{key_name.inspect})"]
end

.inherited(klass) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/attributor/types/hash.rb', line 65

def self.inherited(klass)
  k = self.key_type
  v = self.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 = []

    @error = false
  end
end

.keys(**options, &key_spec) ⇒ Object

Raises:

  • (@error)


89
90
91
92
93
94
95
96
97
98
99
# File 'lib/attributor/types/hash.rb', line 89

def self.keys(**options, &key_spec)
  raise @error if @error

  if block_given?
    @saved_blocks << key_spec
    @options.merge!(options)
  elsif @saved_blocks.any?
    self.definition
  end
  @keys
end

.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options) ⇒ Object



261
262
263
264
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/types/hash.rb', line 261

def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false,  **options)
  context = Array(context)

  if value.nil?
    if recurse
      loaded_value = {}
    else
      return nil
    end
  elsif value.is_a?(self)
    return value
  elsif value.kind_of?(Attributor::Hash)
    loaded_value = value.contents
  elsif value.is_a?(::Hash)
    loaded_value = value
  elsif value.is_a?(::String)
    loaded_value = decode_json(value,context)
  elsif value.respond_to?(:to_hash)
    loaded_value = value.to_hash
  else
    raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
  end

  return self.from_hash(loaded_value,context, recurse: recurse) if self.keys.any?
  return self.new(loaded_value) if (key_type == Object && value_type == Object)

  loaded_value.each_with_object(self.new) do| (k, v), obj |
    obj[self.key_type.load(k,context)] = self.value_type.load(v,context)
  end

end

.native_typeObject



125
126
127
# File 'lib/attributor/types/hash.rb', line 125

def self.native_type
  self
end

.of(key: @key_type, value: @value_type) ⇒ Object

Examples:

Hash.of(key: String, value: Integer)



134
135
136
137
138
139
140
# File 'lib/attributor/types/hash.rb', line 134

def self.of(key: @key_type, value: @value_type)
  ::Class.new(self) do
    self.key_type = key
    self.value_type = value
    @keys = {}
  end
end

.valid_type?(type) ⇒ Boolean

Returns:



129
130
131
# File 'lib/attributor/types/hash.rb', line 129

def self.valid_type?(type)
  type.kind_of?(self) || type.kind_of?(::Hash)
end

.validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute) ⇒ Object



423
424
425
426
427
428
429
430
431
# File 'lib/attributor/types/hash.rb', line 423

def self.validate(object,context=Attributor::DEFAULT_ROOT_CONTEXT,_attribute)
  context = [context] if context.is_a? ::String

  unless object.kind_of?(self)
    raise ArgumentError, "#{self.name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
  end

  object.validate(context)
end

Instance Method Details

#==(other) ⇒ Object



554
555
556
# File 'lib/attributor/types/hash.rb', line 554

def ==(other)
  contents == other || (other.respond_to?(:contents) ? contents == other.contents : false)
end

#[](k) ⇒ Object



474
475
476
# File 'lib/attributor/types/hash.rb', line 474

def [](k)
  @contents[k]
end

#[]=(k, v) ⇒ Object



482
483
484
# File 'lib/attributor/types/hash.rb', line 482

def []=(k,v)
  @contents[k] = v
end

#_get_attr(k) ⇒ Object



478
479
480
# File 'lib/attributor/types/hash.rb', line 478

def _get_attr(k)
  self[k]
end

#delete(key) ⇒ Object



524
525
526
# File 'lib/attributor/types/hash.rb', line 524

def delete(key)
  @contents.delete(key)
end

#dump(**opts) ⇒ Object



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'lib/attributor/types/hash.rb', line 608

def dump(**opts)
  return CIRCULAR_REFERENCE_MARKER if @dumping
  @dumping = true

  contents.each_with_object({}) do |(k,v),hash|
    k = self.key_attribute.dump(k,opts)

    if (attribute_for_value = self.class.keys[k])
      v = attribute_for_value.dump(v,opts)
    else
      v = self.value_attribute.dump(v,opts)
    end

    hash[k] = v
  end
ensure
  @dumping = false
end

#each(&block) ⇒ Object Also known as: each_pair



486
487
488
# File 'lib/attributor/types/hash.rb', line 486

def each(&block)
  @contents.each(&block)
end

#empty?Boolean

Returns:



504
505
506
# File 'lib/attributor/types/hash.rb', line 504

def empty?
  @contents.empty?
end

#generate_subcontext(context, key_name) ⇒ Object



297
298
299
# File 'lib/attributor/types/hash.rb', line 297

def generate_subcontext(context, key_name)
  self.class.generate_subcontext(context,key_name)
end

#get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key)) ⇒ Object

Raises:



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/attributor/types/hash.rb', line 301

def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
  key = self.class.key_attribute.load(key, context)

  if self.class.keys.empty?
    if @contents.key? key
      value = @contents[key]
      loaded_value = value_attribute.load(value, context)
      return self[key] = loaded_value
    else
      if self.class.options[:case_insensitive_load]
        key = key.downcase
        @contents.each do |k,v|
          if key == k.downcase
            return self.get(key, context: context)
          end
        end
      end
    end
    return nil
  end

  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)
    if loaded_value.nil?
      return nil
    else
      return self[key] = loaded_value
    end
  end

  if self.class.options[:case_insensitive_load]
    key = self.class.insensitive_map[key.downcase]
    return self.get(key, context: context)
  end

  if self.class.options[:allow_extra]
    if self.class.extra_keys.nil?
      return @contents[key] = self.class.value_attribute.load(value, context)
    else
      extra_keys_key = self.class.extra_keys

      if @contents.key? extra_keys_key
        return @contents[extra_keys_key].get(key, context: context)
      end

    end
  end


  raise LoadError, "Unknown key received: #{key.inspect} for #{Attributor.humanize_context(context)}"
end

#key?(k) ⇒ Boolean Also known as: has_key?

Returns:



508
509
510
# File 'lib/attributor/types/hash.rb', line 508

def key?(k)
  @contents.key?(k)
end

#key_attributeObject



545
546
547
# File 'lib/attributor/types/hash.rb', line 545

def key_attribute
  self.class.key_attribute
end

#key_typeObject



537
538
539
# File 'lib/attributor/types/hash.rb', line 537

def key_type
  self.class.key_type
end

#keysObject



496
497
498
# File 'lib/attributor/types/hash.rb', line 496

def keys
  @contents.keys
end

#merge(h) ⇒ Object



513
514
515
516
517
518
519
520
521
522
# File 'lib/attributor/types/hash.rb', line 513

def merge(h)
  case h
  when self.class
    self.class.new(contents.merge(h.contents))
  when Attributor::Hash
    raise ArgumentError, "cannot merge Attributor::Hash instances of different types" unless h.is_a?(self.class)
  else
    raise TypeError, "no implicit conversion of #{h.class} into Attributor::Hash"
  end
end

#set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key), recurse: false) ⇒ Object

Raises:



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/attributor/types/hash.rb', line 357

def set(key, value, context: self.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 self.set(key, value, context: context)
  end

  if self.class.options[:allow_extra]
    if self.class.extra_keys.nil?
      return self[key] = self.class.value_attribute.load(value, context)
    else
      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
  end

  raise LoadError, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
end

#sizeObject



492
493
494
# File 'lib/attributor/types/hash.rb', line 492

def size
  @contents.size
end

#validate(context = Attributor::DEFAULT_ROOT_CONTEXT) ⇒ Object



558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/attributor/types/hash.rb', line 558

def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
  context = [context] if context.is_a? ::String
  errors = []

  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

    keys_with_values = Array.new

    self.class.keys.each do |key, attribute|
      sub_context = self.class.generate_subcontext(context,key)

      value = @contents[key]
      unless value.nil?
        keys_with_values << key
      end

      if value.respond_to?(:validating) # really, it's a thing with sub-attributes
        next if value.validating
      end

      errors.push(*attribute.validate(value, sub_context))
    end
    self.class.requirements.each do |req|
      validation_errors = req.validate(keys_with_values, context)
      errors.push(*validation_errors) unless validation_errors.empty?
    end
  else
    @contents.each do |key, value|
      # 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.push(*key_attribute.validate(key, sub_context))
      end

      unless value_type == Attributor::Object
        sub_context = context + ["value(#{value.inspect})"]
        errors.push(*value_attribute.validate(value, sub_context))
      end
    end
  end
  errors
end

#value_attributeObject



549
550
551
# File 'lib/attributor/types/hash.rb', line 549

def value_attribute
  self.class.value_attribute
end

#value_typeObject



541
542
543
# File 'lib/attributor/types/hash.rb', line 541

def value_type
  self.class.value_type
end

#valuesObject



500
501
502
# File 'lib/attributor/types/hash.rb', line 500

def values
  @contents.values
end