Customizing Hash

Hi! I’m trying to write a dry-types type useable for parsing a localization API that has data of a strange shape. It’s mostly a hash of String to Hash.map(Types::String, Types::String), but there’s one key which has a completely different value. Something like this:

{
    "blog": {
        "next": "Next page",
        "previous": "Previous page"
    },
    "wizard": {
        "continue": "Click here to continue"
    },
    "garbage": 123
}

I’d prefer to model this as a Hash rather than a Struct because I don’t care what pages are present or what strings they support. However, the presence of this garbage key makes things challenging. I don’t care about this one garbage so I’d like to drop it.

I see dry-rb - dry-types v1.2 - Custom Types shows a bunch of information about defining specific kinds of custom types, but nothing about my use case. Is it enough to e.g. subclass Nominal and override call? I did that and it worked, but from looking at the code, I get the impression that the actual calling contract is that I should support calling call_unsafe, call_safe,

For the record, this worked:

    without_garbage = Class.new(Dry::Types::Nominal) do
      def initialize(wrapped)
        @wrapped = wrapped
        super(::Hash)
      end

      def call(input = Undefined, &block)
        if input.is_a?(Hash)
          input.delete("garbage")
        end
        @wrapped.call(input, &block)
      end
    end
    attribute :localized_strings, without_garbage.new(Types::Hash.map(Types::String, Types::Hash.map(Types::String, Types::String)))

I actually found an example on dry-rb - dry-types v1.2 - Hash Schemas which uses .constructor, and I was able to adapt it:

attribute(:localized_strings, Types::Hash.map(Types::String, Types::Hash.map(Types::String, Types::String)).constructor do |value| 
  if value.is_a?(Hash)
    value.delete("garbage")
  end
  value
end)

Is this correct? I saw some mention of .constructor here and there on the forum, but I couldn’t find anything in the docs about it.

I don’t recommend using dry-types for parsing/preprocessing/mapping data. I know it’s tempting to do due to flexibility of the types, and it even may work well in various cases, but it’s not a dedicated solution so you may hit a wall or end up with too complicated code. You especially should not use dry-struct with complex dry-types for complex mappings. Again, you can get away this if things are simple but it’s not the right abstraction.

We just had a short convo about this in this thread.

I feel like we should finally come up with a nice solution. There’s dry-transformer that you could evaluate too. It’ll become a generic data mapping solution eventually that works well with dry-types. I’ll be working on this for sure later this year because rom 6.0 will use it under the hood.

Thanks for your response. I agree, dry-struct is clearly meant for defining data objects. Even the fact that attribute :some_attribute, MyClass constructs a MyClass feels kind of weird.

My next question was going to be how I could rename fields, using dry-struct, dry-schema, or anything else. I will hold off for now :slight_smile:

Looking forward to hearing about the nice solution!