kgiszczak / shale

Shale is a Ruby object mapper and serializer for JSON, YAML, TOML, CSV and XML. It allows you to parse JSON, YAML, TOML, CSV and XML data and convert it into Ruby data structures, as well as serialize data structures into JSON, YAML, TOML, CSV or XML.
https://shalerb.org/
MIT License
625 stars 18 forks source link

Add render_nil: true to change to_hash result #42

Open martinnicolas opened 1 month ago

martinnicolas commented 1 month ago

Hi!. I notice that calling to_hash method on a Shale::Mapper object, nil values are not rendered. I can change this using https://www.shalerb.org/#rendering-nil-values render_nil: true but only for shale methods to_json, to_csv etc. It could be a nice approach to implement something similar for to_hash. Thanks for your job!

kgiszczak commented 1 month ago

hey, you can absolutely do this:

require 'shale'

class Person < Shale::Mapper
  attribute :first_name, Shale::Type::String
  attribute :last_name, Shale::Type::String
  attribute :age, Shale::Type::Integer

  hsh do
    map 'first_name', to: :first_name, render_nil: true
    map 'last_name', to: :last_name, render_nil: false
    map 'age', to: :age, render_nil: true
  end
end

puts Person.new(
  first_name: nil,
  last_name: nil,
  age: nil
).to_hash(pretty: true)

# => {"first_name"=>nil, "age"=>nil}
martinnicolas commented 1 month ago

Hi. thats nice! but is not the my case. I would try to explain a lit better.

  require 'shale'

  class Person < Shale::Mapper
    attribute :first_name, Shale::Type::String
    attribute :last_name, Shale::Type::String
    attribute :age, Shale::Type::Integer

    csv do
      map 'first_name', to: :first_name, render_nil: true
      map 'last_name', to: :last_name, render_nil: false
      map 'age', to: :age, render_nil: true
    end
  end

  puts Person.new(
    first_name: nil,
    last_name: nil,
    age: nil
  ).to_hash

# => {}

I need the csv block because I'm importing a csv file. And I need to_hash for using https://github.com/zdennis/activerecord-import to import content into my database. Could be possible to define another block for hsh? like this?

  require 'shale'

  class Person < Shale::Mapper
    attribute :first_name, Shale::Type::String
    attribute :last_name, Shale::Type::String
    attribute :age, Shale::Type::Integer

    csv do
      map 'first_name', to: :first_name, render_nil: true
      map 'last_name', to: :last_name, render_nil: false
      map 'age', to: :age, render_nil: true
    end

    hsh do
      map 'first_name', to: :first_name, render_nil: true
      map 'last_name', to: :last_name, render_nil: false
      map 'age', to: :age, render_nil: true
    end
  end
kgiszczak commented 1 month ago

yes, you can define as many blocks as you want, this is valid:

  require 'shale'

  class Person < Shale::Mapper
    attribute :first_name, Shale::Type::String
    attribute :last_name, Shale::Type::String
    attribute :age, Shale::Type::Integer

    csv do
      map 'first_name', to: :first_name, render_nil: true
      map 'last_name', to: :last_name, render_nil: false
      map 'age', to: :age, render_nil: true
    end

    hsh do
      map 'first_name', to: :first_name, render_nil: true
      map 'last_name', to: :last_name, render_nil: false
      map 'age', to: :age, render_nil: true
    end

    json do
      ...
    end

    toml do
      ...
    end

    xml do
      ...
    end
  end
martinnicolas commented 1 month ago

Nice! I was using this little hack. To prevent defining all maps again. But I think is better to redefine the mapping block for each format. Isnt it?

def to_hash
  instance_variables.each_with_object({}) do |instance_variable_name, hash|
    hash[instance_variable_name.to_s.delete("@")] = instance_variable_get(instance_variable_name)
  end
end