Search code examples
crystal-lang

Crystal-lang: Recursive JSON or Hash


I'm trying to create a JSON or a Hash that can have N depth. Example: X people with unique names might have Y kids and those kids might have Z kids (and goes on till N generations). I want to create a Hash (or JSON) that would look like this:

{
  "John" => {
              "Lara" => { 
                          "Niko" => "Doe"
                        },
              "Kobe" => "Doe"
            },
  "Jess" => {
              "Alex" => "Patrik"
            }
}

I tried working with recursive aliases but couldn't achieve that.

alias Person = Hash(String, Person) | Hash(String, String)

The input could come from arrays of String like

["John|Lara|Niko", "John|Kobe", "Jess|Alex"]
["Doe", "Patrik"]

(I can deal with the loops. My issue is adding them to the Hash as their size is unknown.)

I came across this discussion https://forum.crystal-lang.org/t/how-do-i-create-a-nested-hash-type/885 but unfortunately I can't achieve what I want and also keep the Hash's (or JSON's) methods (which are needed).


Solution

  • I couldn't quite make out how you arrived at your example result from your example input, so I'm going to use a different setup: Let's assume we have a simple configuration file format where keys are structured and grouped through a dotted sequence and all values are always strings.

    app.name = test
    app.mail.enable = true
    app.mail.host = mail.local
    server.host = localhost
    server.port = 3000
    log_level = debug
    

    We can parse it to a recursive Hash like so:

    alias ParsedConfig = Hash(String, ParsedConfig)|String
    
    config = Hash(String, ParsedConfig).new
    
    # CONFIG being our input from above
    CONFIG.each_line do |entry|
      keys, value = entry.split(" = ")
      keys = keys.split(".")
      current = config
      keys[0..-2].each do |key|
        if current.has_key?(key)
          item = current[key]
          if item.is_a?(Hash)
            current = item
          else
            raise "Malformed config"
          end
        else
          item = Hash(String, ParsedConfig).new
          current[key] = item
          current = item
        end
      end
    
      current[keys.last] = value
    end
    
    pp! config
    

    The output will be:

    config # => {"app" =>
      {"name" => "test", "mail" => {"enable" => "true", "host" => "mail.local"}},
     "server" => {"host" => "localhost", "port" => "3000"},
     "log_level" => "debug"}
    

    Alternatively we can parse it to a recursive struct:

    record ConfigGroup, entries = Hash(String, ConfigGroup|String).new
    
    config = ConfigGroup.new
    
    # CONFIG being our input from above
    CONFIG.each_line do |entry|
      keys, value = entry.split(" = ")
      keys = keys.split(".")
      current = config
      keys[0..-2].each do |key|
        if current.entries.has_key?(key)
          item = current.entries[key]
          if item.is_a?(ConfigGroup)
            current = item
          else
            raise "Malformed config"
          end
        else
          item = ConfigGroup.new
          current.entries[key] = item
          current = item
        end
      end
    
      current.entries[keys.last] = value
    end
    
    pp! config
    

    The output then will be:

    config # => ConfigGroup(
     @entries=
      {"app" =>
        ConfigGroup(
         @entries=
          {"name" => "test",
           "mail" =>
            ConfigGroup(@entries={"enable" => "true", "host" => "mail.local"})}),
       "server" => ConfigGroup(@entries={"host" => "localhost", "port" => "3000"}),
       "log_level" => "debug"})
    

    Recursive structs currently are a bit less buggy, offer a nice place for custom methods on your parsed domain objects and generally have a more certain future than recursive aliases, which are sometimes a bit buggy.

    Full example on carc.in: https://carc.in/#/r/9mxr