Working with External APIs and Metaprogramming — Ruby Deep Dive [07]
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.