Class: FancyHash

Inherits:
SimpleDelegator
  • Object
show all
Extended by:
ActiveModel::Naming, ActiveModel::Translation
Includes:
ActiveModel::Conversion, ActiveModel::Validations
Defined in:
lib/fancy_hash.rb,
lib/fancy_hash/version.rb

Overview

FancyHash was born to simplify handling third party JSON payloads. Let’s say for example we have the following JSON:

payload = {
  'identificador' => 1,
  'nomeCompleto' => "John Doe",
  'dtNascimento' => "1990-01-01",
  'genero' => 1, # Let's say we know this field is going to be 1 for Male, 2 for Female
}

It would be very tedius having to remember the field names and handle type conversion everywhere, like this:

payload['dtNascimento'].to_date
payload['dtNascimento'] = Date.new(1990, 1, 2).iso8601

if payload['genero'] == 1
  # do something
end

Instead, we can do this:

class Person < FancyHash
  attribute :id, field: 'identificador', type: :integer
  attribute :name, field: 'nomeCompleto', type: :string
  attribute :birth_date, field: 'dtNascimento', type: :date
  attribute :gender, field: 'genero', type: :enum, of: { male: 1, female: 2 }
end

person = Person.new(payload) # `payload` here is a Hash that we retrieved from an hypothetical API
person.id # => 1
person.name # => "John Doe"
person.name = 'Mary Smith'
person.birth_date # => Mon, 01 Jan 1990
person.birth_date = Date.new(1990, 1, 2)
person.gender # => :male
person.male? # => true
person.female? # => false
person.gender = :female # we can use the symbols here, the FancyHash will convert it to the right value

person.__getobj__ # => { 'identificador' => 1, 'nomeCompleto' => 'Mary Smith', 'dtNascimento' => '1990-01-02', 'genero' => 2 }

This can be used for inbound payloads that we need to parse and for outbound requests we need to send so we don’t need to worry about type casting and enum mapping either way.

Defined Under Namespace

Modules: Types

Constant Summary collapse

VERSION =
'0.1.0'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hash = {}, **attributes) ⇒ FancyHash

Returns a new instance of FancyHash.

Raises:

  • (ArgumentError)


251
252
253
254
255
256
257
258
# File 'lib/fancy_hash.rb', line 251

def initialize(hash = {}, **attributes)
  raise ArgumentError, "Unexpected object class. Should be a Hash or #{self.class}, got #{hash.class} (#{hash})" unless hash.is_a?(Hash) || hash.is_a?(self.class)

  super(hash)

  defaults = self.class.defaults.transform_values { |v| v.is_a?(Proc) ? instance_exec(&v) : v }
  assign_attributes(defaults.merge(attributes))
end

Class Method Details

.assert_valid_value(_value) ⇒ Object

This is to support FancyHash instances as ActiveModel attributes



238
239
240
# File 'lib/fancy_hash.rb', line 238

def assert_valid_value(_value)
  # NOOP
end

.attribute(name, field: name.to_s, type: nil, default: nil, &block) ⇒ Object

Allows defining attributes coming from a Hash with a different attribute name. For example:

attribute :name, type: :string
attribute :born_on, field: 'birthDate', type: :date
attribute :favorite_color, field: 'favoriteColor', type: :enum, of: { red: 0, green: 1, blue: 2 }


172
173
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/fancy_hash.rb', line 172

def attribute(name, field: name.to_s, type: nil, default: nil, **, &block)
  attribute_names << name

  attribute_definitions[name] = { field:, type: }

  field = Array(field)

  defaults[name] = default unless default.nil?

  type_serializer = Types.find(type, **)

  raw_method = :"raw_#{name}"
  define_method(raw_method) { dig(*field) }

  define_method(name) do
    type_serializer.cast(send(raw_method)).tap do |value|
      Array(value).each do |v|
        instance_exec(v, &block) if block
      end
    end
  end

  if type_serializer.is_a?(ActiveModel::Type::Boolean)
    define_method(:"#{name}?") { send(name) }
  elsif type.is_a?(Class)
    define_method(:"#{name}_attributes=") { |attributes| send(:"#{raw_method}=", type.new(**attributes)) }
  end

  define_method(:"#{name}=") do |new_value|
    send(:"#{raw_method}=", type_serializer.serialize(type_serializer.cast(new_value)))
  end

  define_method(:"#{raw_method}=") do |new_raw_value|
    hsh = self

    field[0..-2].each do |key|
      hsh = hsh[key] ||= {}
    end

    hsh[field.last] = new_raw_value
  end

  return unless type == :enum

  define_singleton_method(name.to_s.pluralize) do
    type_serializer.config.symbolize_keys
  end

  type_serializer.config.each_key do |key|
    define_method(:"#{key}?") do
      send(name) == key
    end
  end
end

.attribute_definitionsObject



227
228
229
# File 'lib/fancy_hash.rb', line 227

def attribute_definitions
  @attribute_definitions ||= {}
end

.attribute_namesObject



242
243
244
# File 'lib/fancy_hash.rb', line 242

def attribute_names
  @attribute_names ||= Set.new
end

.cast(raw) ⇒ Object



231
232
233
234
235
# File 'lib/fancy_hash.rb', line 231

def cast(raw)
  return raw if raw.nil? || raw.is_a?(self)

  new(raw)
end

.defaultsObject



246
247
248
# File 'lib/fancy_hash.rb', line 246

def defaults
  @defaults ||= {}
end

.serialize(fancy_hash) ⇒ Object



159
160
161
# File 'lib/fancy_hash.rb', line 159

def serialize(fancy_hash)
  fancy_hash.is_a?(FancyHash) ? fancy_hash.__getobj__ : fancy_hash
end

.wrap_many(array) ⇒ Object



163
164
165
# File 'lib/fancy_hash.rb', line 163

def wrap_many(array)
  Array.wrap(array).map { |hash| new(hash) }
end

Instance Method Details

#assign_attributes(attributes) ⇒ Object



276
277
278
279
280
# File 'lib/fancy_hash.rb', line 276

def assign_attributes(attributes)
  attributes.each { |k, v| public_send(:"#{k}=", v) }

  self
end

#attributesObject



272
273
274
# File 'lib/fancy_hash.rb', line 272

def attributes
  self.class.attribute_names.index_with { send(_1) }
end

#classesObject



260
261
262
# File 'lib/fancy_hash.rb', line 260

def classes
  (self['_klass'] || []) + [self.class.to_s]
end

#merge(other) ⇒ Object



264
265
266
267
268
269
270
# File 'lib/fancy_hash.rb', line 264

def merge(other)
  merged = __getobj__.merge(other.__getobj__)
  merged['_klass'] ||= []
  merged['_klass'].push(self.class.to_s)

  other.class.new(self.class.new(merged))
end

#present?Boolean

Override this method so it does not get delegated to the underlying Hash, which allows us to override ‘blank?` in entities

Returns:

  • (Boolean)


284
285
286
# File 'lib/fancy_hash.rb', line 284

def present?
  !blank?
end