Vlad Faust
Vlad Faust

Reputation: 550

Generic Hashes as arguments?

I'm trying to implement a class which accepts a generic payload and then converts it to JSON:

require "json"

class Response
  alias Payload = Hash(Symbol | String, String | Bool | Int32 | Int64 | Float32 | Float64 | Nil)
  @payload : Payload

  def initialize(@payload, @method : String = "sendMessage")
  end

  def to_json
    @payload.merge({"method" => @method}).to_json
  end
end

Response.new({
  "chat_id" => update.message.not_nil!.chat.id,
  "text"    => "Crystal is awesome but complicated",
})

But I get instance variable '@payload' of Response must be Hash(String | Symbol, Bool | Float32 | Float64 | Int32 | Int64 | String | Nil), not Hash(String, Int64 | String) compiler error. How can I overcome this? Does Crystal support generic Hashes? Why it can't convert even if types overlap?

The Response is a part of a shard, so I don't know which hashes would be passed as arguments, but Payload has to be enough.

Upvotes: 1

Views: 662

Answers (1)

Johannes Müller
Johannes Müller

Reputation: 5661

Your payload hash is of type Hash(String, Int32 | String):

typeof({
  "chat_id" => update.message.not_nil!.chat.id,
  "text"    => "Crystal is awesome but complicated",
}) # => Hash(String, Int32 | String)

But the constructor expects a Hash(Symbol | String, String | Bool | Int32 | Int64 | Float32 | Float64 | Nil). These are diffferent types and cannot be magically casted. You'll need to ensure your payload has the correct type.

One way to do this is to explicitly declare the type of the hash literal:

Payload{
  "chat_id" => update.message.not_nil!.chat.id,
  "text"    => "Crystal is awesome but complicated",
}

This is of course not great, but depending on your usecase it might be sufficient.

If you want to have a general interface that allows to receive any type of hash, you'll need to cast it to the Payload type. This means copying the data into a new hash of that type:

def self.new(hash, method : String = "sendMessage")
  payload = Payload.new
  hash.each do |key, value|
    payload[key] = value
  end
  new(payload, method)
end

For a real-world example, I am using this approach in Crinja to convert a number of different type variations to the matching ones.

Upvotes: 3

Related Questions