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.