How to serialize a struct from any type?

let’s say I have a struct with the ability to take a Hash and assign it’s properties accordingly.
I will provide an example with rethinkDB but really it can happen with every database library out there.

When querying the database the result of the query is of a type called RethinkDB::QueryResult which is really just an alias for many primitives available in Crystal.
On the other hand I got a struct which looks like:

abstract struct Cybergarden::Items::Server
  include JSON::Serializable
  property capacity
  property processors
  property price # money per second
  property maintability    

  def analyze_mps()
      mps = 0
      processor.each {|cpu| mps+=cpu.mps}
      mps
  end

  def as_h()
      {
          capacity: @capacity,
          processors: @processors.map &.as_h,
          price: @price,
          maintability: @maintability
      } 
  end

  def from_h(payload : Hash(String, String | Array(Hash(String, String | Int32) | Int32)))
      @capacity = payload["capacity"]
      @processors = payload["processors"].map Cybergarden::Items::CpuTypes[&["type"]].new().from_h()
      @price = payload["price"]
      @maintability = payload["maintabiltiy"]
  end
end

so when querying the database to find the servers a user own I would like to make a new struct instance out of the available data, the only issue is, the data is RethinkDB::QueryResult and when calling .as_h on it it’s still Hash(String, RethinkDB::QueryResult) and I still won’t be able to do something like

server = CyberServerTierOne.new.from_h(theHashFromTheQuery)

since it doesn’t expect a QueryResult but more specific Hash(String, String | Array(Hash(String, String | Int32) | Int32))
Someone has an idea on how I can implement such stuff? without doing the obvious of making all the property types to be Rethink::QueryResult
Thank you.

Have you considered to simply omit the type restriction for payload?
I assume the method implementation would work with both QueryResult and Hash if it’s just using duck typing?

Hi - There are 2 things you could get back from a query:

  • QueryResult
  • Cursor

QueryResult is a single item whereas Cursor is many items. This is how you could handle each case:

given a container class such as User:

class User
  JSON.mapping(id: String, email: String, name: String)
end

and a find method:

class UserRepo
  def initialize(@connection : RethinkDB::Connection) ; end

   # the important thing here is the use of `raw`
  def find_by_id(id) : User | Nil
    result = r.table("users").get(id).run(@connection)
    result.raw.nil? ? Nil : User.from_json(result.raw.to_json)   
  end

  # the important thing here is the use of `to_a` and `map`
  def all : Array(User) | Nil
    query = r.table("users").run(@connection)
   case query
   when RethinkDB::Cursor
     query.to_a.map{ |u| User.from_json(u.raw.to_json)  }
   else
     Nil  
  end
end

Personally I usually return a container from the db queries using the CRZ functional library:

def find_by_id(id) : Result(User, SystemError)

and then when I use it later:

Result.match user_repo.find_by_id(id), {
  [Ok, user]  => {user: user}.to_json,
  [Err, error] => {error: handle_error(error)}.to_json
}

}

Using this patterns I can send back on the API the user or the error (after categorising the error and adding an appropriate message for users to see)

Hope that helps

Can’t you create the objects without the intermediate json? It seems like some kind of mapping would be handy.