Reputation: 282
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
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