Lush
Lush

Reputation: 282

Ruby: many related objects with similiar method naming pattern. How to map them to standardized methods

I'm working on a program that receives responses from an API that represent 'songs' from a database. Those responses arrive in my program as Struct objects, and they are structured slightly differently depending on which table they were pulled from.

For instance, the song object pulled from the 'track' table looks like:

song_1 = <struct Song track_artist="Michael Jackson", track_title="Billie Jean"> 

And the song object returned from the 'license' table looks like:

song_2 = <struct Song license_artist="Michael Jackson", license_title="Billie Jean">

If I want to get the 'artist' from song_1, I'd call song_1.track_artist, and with song_2, song_2.license_artist. But this is problematic when running loops. I want to be able to call song.title on any of them and receive the title.

Right now, I'm putting each Struct through a 'Normalizer' object when I receive it. It uses a hash mapping to change the method name of each Struct; the mapping more or less looks like:

{ track_artist:   artist,
  track_title:    title,
  license_artist: artist,
  license_title:  title }

This seems like it might be overkill. What's the best way to go about this?

Upvotes: 0

Views: 53

Answers (1)

Alexey Kuznetsov
Alexey Kuznetsov

Reputation: 317

You could use method_missing for this

module Unifier
  def method_missing(name, *args, &block)
    meth = public_methods.find { |m| m[/_#{name}/] }
    meth ? send(meth, *args, *block) : super
  end

  def respond_to_missing?(method_name, include_private = false)
    public_methods.any? { |m| m[/_#{method_name}/] } || super
  end
end

class A
  include Unifier
  attr_reader :artist_name
  def initialize
    @artist_name = 123
  end
end

a = A.new
a.respond_to?(:name) # => true
a.name # => 123
a.respond_to?(:title) # => false
a.title # => undefined method `title' for #<A:0x007fb3f4054330 @artist_name=123> (NoMethodError)

Update For you case it will be more complex and tricky.

If you can make changes to place, where this Struct objects are created, then just patch classes, generated from Struct

song_1_class = Struct.new(:track_artist, :track_title) do
  include Unifier
end
song_1 = song_1_class.new('Michael Jackson', 'Billie Jean')

puts "#{song_1.artist} - #{song_1.title}"
# => Michael Jackson - Billie Jean

If you can work only with objects of that classes - you could patch it dynamically

# We get objects of licence_struct class
licence_struct = Struct.new(:license_artist, :license_title)
song_2 = licence_struct.new('Michael Jackson', 'Billie Jean')
song_3 = licence_struct.new('Michael Jackson', 'Black of White')


def process_song(song)
  puts "Song #{song} patched - #{song.respond_to?(:artist)}"
  "#{song.artist} - #{song.title}"
rescue NoMethodError => err
  # If we don't have methods on our struct - patch it
  # If after patching object still dont respond to our method - throw exception
  patch_object_from_error(err) ? retry : raise(err)
end

def patch_object_from_error(error)
  receiver = error.receiver
  receiver.class.class_exec { include Unifier }
  meth = error.message.match(/undefined method `(\S+)'/)[1].to_sym
  receiver.respond_to?(meth)
end

puts process_song(song_2)
# => Song #<struct license_artist="Michael Jackson", license_title="Billie Jean"> patched - false
# after retry
# => Song #<struct license_artist="Michael Jackson", license_title="Billie Jean"> patched - true
# => Michael Jackson - Billie Jean
puts process_song(song_3)
# dont need retry - class already patched
# => Song #<struct license_artist="Michael Jackson", license_title="Black of White"> patched - true
# => Michael Jackson - Black of White

Upvotes: 2

Related Questions