Working with External APIs and Metaprogramming — Ruby Deep Dive [07]

Bhavyansh @ DiversePixel
4 min readJul 24, 2024

--

Making HTTP Requests

Ruby provides several libraries for making HTTP requests, including Net::HTTP, Faraday, and HTTParty. Each library has its own strengths and use cases.

Using Net::HTTP

Net::HTTP is a built-in Ruby library for making HTTP requests.

require 'net/http'
require 'uri'
require 'json'
uri = URI.parse("https://api.example.com/data")
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
puts data
else
puts "HTTP Request failed (#{response.code})"
end

Using Faraday

Faraday is a more flexible and user-friendly HTTP client library.

require 'faraday'
require 'json'
response = Faraday.get("https://api.example.com/data")
if response.success?
data = JSON.parse(response.body)
puts data
else
puts "HTTP Request failed (#{response.status})"
end

Using HTTParty

HTTParty is a popular and easy-to-use HTTP client library.

require 'httparty'
response = HTTParty.get("https://api.example.com/data")
if response.success?
data = response.parsed_response
puts data
else
puts "HTTP Request failed (#{response.code})"
end

Handling API Responses

Handling API responses often involves parsing JSON and managing errors.

require 'net/http'
require 'uri'
require 'json'
uri = URI.parse("https://api.example.com/data")
response = Net::HTTP.get_response(uri)
begin
data = JSON.parse(response.body)
puts data
rescue JSON::ParserError => e
puts "Failed to parse JSON: #{e.message}"
end

OAuth and Authentication

Authentication is crucial when working with APIs. OAuth is a common authentication method.

Using OAuth with Faraday

require 'faraday'
require 'faraday_middleware'
client = Faraday.new(url: 'https://api.example.com') do |conn|
conn.request :oauth, {
consumer_key: 'YOUR_CONSUMER_KEY',
consumer_secret: 'YOUR_CONSUMER_SECRET',
token: 'YOUR_ACCESS_TOKEN',
token_secret: 'YOUR_ACCESS_SECRET'
}
conn.adapter Faraday.default_adapter
end
response = client.get('/protected_resource')
if response.success?
data = JSON.parse(response.body)
puts data
else
puts "HTTP Request failed (#{response.status})"
end

Metaprogramming

Metaprogramming is a powerful feature in Ruby that allows you to write code that writes code. It enables dynamic method definitions, interception of method calls, and more.

Introduction to Metaprogramming

Metaprogramming allows you to modify the program structure at runtime. This can be incredibly powerful but should be used judiciously to maintain code readability and maintainability.

Using define_method

define_method dynamically defines a method at runtime.

class DynamicMethods
['hello', 'goodbye'].each do |method_name|
define_method(method_name) do |name|
puts "#{method_name.capitalize}, #{name}!"
end
end
end
obj = DynamicMethods.new
obj.hello('Alice') # Output: Hello, Alice!
obj.goodbye('Bob') # Output: Goodbye, Bob!

Using method_missing

method_missing catches calls to undefined methods.

class CatchAll
def method_missing(name, *args)
puts "You called: #{name}(#{args.join(', ')})"
end
end
obj = CatchAll.new
obj.any_method('arg1', 'arg2') # Output: You called: any_method(arg1, arg2)

Using send

send allows you to call methods dynamically.

class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
def greet
puts "Hello, my name is #{@name} and I am #{@age} years old."
end
end
person = Person.new('Alice', 30)
person.send(:greet) # Output: Hello, my name is Alice and I am 30 years old.

Using at_exit

at_exit do
puts "Cleaning up resources..."
# Perform cleanup operations here
end
puts "Main program running..."
# Rest of the program

Practical Examples and Use Cases

Dynamic Attribute Accessors

You can create dynamic attribute accessors using metaprogramming.

class DynamicAttributes
def self.attr_accessor_with_history(*attrs)
attrs.each do |attr|
attr_history = "#{attr}_history"
define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
instance_variable_get("@#{attr_history}") << value
end
define_method(attr_history) { instance_variable_get("@#{attr_history}") || instance_variable_set("@#{attr_history}", []) }
end
end
end
class Person
extend DynamicAttributes
attr_accessor_with_history :name, :age
end
person = Person.new
person.name = 'Alice'
person.name = 'Bob'
puts person.name_history # Output: ["Alice", "Bob"]

Best Practices and Potential Pitfalls

  • Use Sparingly: Metaprogramming can make code difficult to understand and debug. Use it only when necessary.
  • Document Thoroughly: Ensure that the purpose and functionality of metaprogrammed code are well-documented.
  • Test Extensively: Metaprogramming can introduce subtle bugs. Write comprehensive tests to cover all cases.

Managing a virtual pet store using Metaprogramming in Ruby

class PetStore
def initialize
@pets = {}
@total_sales = 0
end

def method_missing(name, *args, &block)
action, animal = name.to_s.split('_', 2)
case action
when 'add'
add_pet(animal, *args)
when 'sell'
sell_pet(animal, *args)
else
super
end
end

def respond_to_missing?(name, include_private = false)
action, _ = name.to_s.split('_', 2)
['add', 'sell'].include?(action) || super
end

def self.pet_actions(*actions)
actions.each do |action|
define_method(action) do |pet_type, &block|
@pets[pet_type] ||= []
@pets[pet_type] << block
end
end
end

pet_actions :feed, :groom, :play

def perform_action(action, pet_type)
if @pets[pet_type] && @pets[pet_type].any?
@pets[pet_type].each { |pet| pet.call(action) }
else
puts "No #{pet_type}s available for #{action}."
end
end

def report
puts "Pet Store Report"
puts "----------------"
@pets.each do |type, pets|
puts "#{type.capitalize}: #{pets.size}"
end
puts "Total Sales: $#{@total_sales}"
end

private

def add_pet(type, name)
@pets[type] ||= []
@pets[type] << name
puts "Added #{name} the #{type} to the store."
end

def sell_pet(type, price)
if @pets[type] && @pets[type].any?
sold = @pets[type].pop
@total_sales += price
puts "Sold #{sold} the #{type} for $#{price}."
else
puts "Sorry, no #{type}s available."
end
end
end

# Using our PetStore DSL
store = PetStore.new

store.add_dog("Buddy")
store.add_cat("Whiskers")
store.add_dog("Max")

store.feed(:dog) { |action| puts "Dogs are eating happily!" }
store.groom(:cat) { |action| puts "Cats are getting groomed and purring." }
store.play(:dog) { |action| puts "Dogs are playing fetch!" }

store.perform_action(:feed, :dog)
store.perform_action(:groom, :cat)
store.perform_action(:play, :dog)

store.sell_dog(500)
store.sell_cat(300)

store.report

# Try to perform an action on a pet type that doesn't exist
store.perform_action(:feed, :fish)

# Try to call a method that doesn't exist
begin
store.nonexistent_method
rescue NoMethodError => e
puts "Caught NoMethodError: #{e.message}"
end

Conclusion

Working with external APIs and metaprogramming are advanced topics in Ruby that can significantly enhance your coding abilities. By mastering HTTP requests, handling API responses, and understanding OAuth, you can effectively integrate external services into your applications. Metaprogramming, on the other hand, allows you to write more flexible and dynamic code, though it should be used judiciously to maintain code readability and maintainability.

--

--

Bhavyansh @ DiversePixel
Bhavyansh @ DiversePixel

Written by Bhavyansh @ DiversePixel

Hey I write about Tech. Join me as I share my tech learnings and insights. 🚀

No responses yet