So you’re working with some kind of external API in Ruby.
And you’re getting some JSON response that you now want to play with.
{
"name": "E Street Band",
"musicians": [
{
"name": "Bruce",
"instrument": "Guitar and Vocals"
},
{
"name": "Clarence",
"instrument": "Saxophone"
}
]
}
So you take the JSON and you parse it.
@band = JSON.parse(raw_json)
Running @band.class
tells you it’s a Hash
.
Meaning you can now output the name of the band.
@band['name']
# => "E Street Band"
Which is okay, and all that. But you’re a Rubyist. And so you yearn to write.
@band.name
# => "E Street Band"
But it doesn’t work. And you are sad.
@band.name
# => NoMethodError: undefined method `name' for #<Hash:0x007f949c030b00>
So you write an API wrapper to solve all your problems.
class Band
def initialize(hash)
@name = hash['name']
@musicians = hash['musicians'].map { |musician| Musician.new(musician) }
end
attr_reader :name, :musicians
end
class Musician
def initialize(hash)
@name = hash['name']
@instrument = hash['instrument']
end
attr_reader :name, :instrument
end
Now you can write all your classic Ruby stuff.
@band.musicians.map(&:name)
# => ["Bruce", "Clarence"]
And you are happier. But still a bit sad. Because those classes feel a bit like detention.
It’d be nice if Ruby could do all that boring stuff for you, eh?
Oh, hi OpenStruct
!
OpenStruct
takes a hash as an initializer argument and gives you back a
lightweight object in return.
hash = { cat: 'felix', dog: 'fido' }
# => {:cat=>"felix", :dog=>"fido"}
pet_name_generator = OpenStruct.new(hash)
# => #<OpenStruct cat="felix", dog="fido">
pet_name_generator.cat
# => "felix"
Which is pretty cool. And what’s cooler is when we inherit from OpenStruct
.
Because now we can refactor our API wrapper classes into the jaw-droppingly simple.
class Band < OpenStruct
end
class Musician < OpenStruct
end
… nearly!
It’s not quite that simple because programming is not magic, and it’ll blindly assign the array of hashes to the musicians
method. We want an array of
Musician objects.
But that’s okay, because it’s trivial to overwrite methods using the hash syntax to get the object’s original value.
Similarly, you can add your own convenience methods using values retrieved from the API.
Like so.
class Band < OpenStruct
def musicians
self[:musicians].map { |musician| Musician.new(musician) }
end
end
class Musician < OpenStruct
def to_s
"#{name} on #{instrument}"
end
end
And you can even add a class method to take your JSON.
class Band < OpenStruct
def self.from_json(raw_json)
new(JSON.parse(raw_json))
end
def musicians
self[:musicians].map { |musician| Musician.new(musician) }
end
end
So now you can bring joy to your codebase.
@band = Band.from_json(raw_json)
puts @band.musicians
Bruce on Guitar and Vocals
Clarence on Saxophone
# => nil
So if you’re writing an API wrapper – or some other class where it’d be nice to
coerce a hash into an object and perhaps add a few more methods – then you could
do worse than spending a bit of time dabbling with OpenStruct
.