Powerful switch statement with pattern matching in Ruby
Pattern matching is a technique that allows you to match and destructure data structures based on their shape and content. It provides a concise and readable way to handle different cases in your code. In Ruby, pattern matching was introduced in version 2.7 and further enhanced in version 3.0.
Matching hashes
To further illustrate the power of pattern matching, let's look at a code snippet that demonstrates its use within a custom API.
api = Struct.new(:success, :result) do
def get(resource, id)
# custom API logic
if resource == 'user' && id == 1
@success = true
@result = { id: 1, name: 'John' }
{ success: @success, data: @result }
else
@success = false
@result = { error: 'Error: Invalid resource or ID' }
{ success: @success, data: @result }
end
end
def count(resource)
# custom API logic
@success = true
@result = { count: resource == 'user' ? 1 : 0 }
{ success: @success, data: @result }
end
end
api = api.new
# api.get('user', 1) # => { success: true, data: { id: 1, name: 'John' } }
# api.get('abcd', 2) # => { success: false, data: 'Error: Invalid resource or ID' }
# api.count('user') # {:success=>true, :data=>{:count=>1}}
# api.count('different') # {:success=>true, :data=>{:count=>0}}
case api.get('user', 1)
in { success: true, data: user }
user
in { success: false, data: error }
error
end
# => {:id=>1, :name=>"John"}
In the above code, we define a custom API using a Ruby Struct
. The api
object has two methods: get
and count
. The get
method takes a resource
and an id
as arguments and performs custom API logic to fetch the requested data. If the resource
is 'user'
and the id
is 1
, the API call is considered successful, and the result contains the user's data. Otherwise, an error message is returned.
The count
method counts the number of resources based on the specified resource
parameter. It returns the count as a result.
The commented-out examples below the code snippet demonstrate the expected outputs of calling the get
and count
methods directly.
Using pattern matching, we can handle the response from the get
method in a more concise and readable way. The case
statement matches the result of api.get('user', 1)
against two patterns. If the result has success
as true
, we can extract the user data and use it. If the result has success
as false
, we can extract the error message and handle it accordingly.
Match multiple patterns
To match multiple patterns within one block and assign the matched object to a variable we need to use |
and =>
case api.get('userrrr', 1)
in { success: true | false, data: Object } => response # match multiple patterns within one block and assign the matched object to a variable: use | and =>
response
else
'error'
end
# => {:success=>false, :data=>{:error=>"Error: Invalid resource or ID"}
Match the same across multiple patterns
To match across multiple patterns, we need to use ^
case [api.count('user'), api.count('user')]
in [number, ^number] # match the same value across pattern use ^
puts "The number is the same"
else
puts "The number is not the same"
end
# => The number is the same
Match configuration as sample usage
Let's show the sample power
# catch part of the hash for configuration
def connect(params)
case params
in api: { username: } # matches subhash and puts matched value in variable user
puts "Connect via username '#{username}'"
in connection: { token: }
puts "Connect via token '#{token}'"
else
puts "Structure not recognized"
end
end
connect({ api: { username: 'admin', password: '1234' } }) # => Connect with user 'admin'
connect({ connection: { token: '1234' } }) # => Connect via token '1234'
Match against classes
To make a class available for pattern matching, it needs to implement 2 methods: deconstruct
(for array match) and deconstruct_key
(for a hash match).
ApiResponse = Struct.new(:status, :data) do
def deconstruct # for array pattern match
[status, data]
end
def deconstruct_key # for hash pattern match
{ status: status, data: data }
end
end
response = ApiResponse.new(true, { id: 1, name: "John" })
case response # this will call deconstruct in the background
in [true, user]
p user
else
p 'error'
end
# => {:id=>1, :name=>"John"}
case response # this will call deconstruct_key in the background
in { status: true, data: user }
p user
else
'error'
end
# => {:id=>1, :name=>"John"}
Summary
In summary, the code snippets demonstrate how pattern matching can enhance a custom API implementation in Ruby. It shows the benefits of using pattern matching to handle different scenarios and extract specific data elements, resulting in cleaner and more maintainable code.