Crystal-lang: JSON ou Hash recursivo

Sep 04 2020

Estou tentando criar um JSON ou um Hash que pode ter profundidade N. Exemplo: X pessoas com nomes exclusivos podem ter Y filhos e esses filhos podem ter Z filhos (e continua até N gerações). Quero criar um Hash (ou JSON) parecido com este:

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

Tentei trabalhar com aliases recursivos, mas não consegui.

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

A entrada pode vir de matrizes de String como

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

(Posso lidar com os loops. Meu problema é adicioná-los ao Hash, pois seu tamanho é desconhecido.)

Me deparei com esta discussão https://forum.crystal-lang.org/t/how-do-i-create-a-nested-hash-type/885 mas infelizmente não consigo alcançar o que desejo e também manter os métodos de Hash (ou JSON) (que são necessários).

Respostas

1 JonneHaß Sep 04 2020 at 17:12

Não consegui entender como você chegou ao resultado de exemplo a partir de sua entrada de exemplo, então vou usar uma configuração diferente: vamos supor que temos um formato de arquivo de configuração simples, onde as chaves são estruturadas e agrupadas por meio de uma sequência pontilhada e todos os valores são sempre strings.

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

Podemos analisá-lo para um recursivo Hashassim:

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

O resultado será:

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

Como alternativa, podemos analisá-lo em uma estrutura recursiva:

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

A saída será:

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"})

Structs recursivos atualmente são um pouco menos problemáticos, oferecem um bom lugar para métodos personalizados em seus objetos de domínio analisados ​​e geralmente têm um futuro mais certo do que apelidos recursivos, que às vezes são um pouco problemáticos.

Exemplo completo em carc.in: https://carc.in/#/r/9mxr