Arbitrary hash keys?

Hi! Started using Hanami for real now that 2.2 was announced. This is about receiving arbitrary hash keys.

The app is used for registrations: Seasons have many Events, Events have many Registrations. The UI displays a Season and all attending seasons. People register one or more events, and could register themselves for multiple times for multiple events. For example:

  • Season 2024-2025
    • 2024-10-01 10 am
      * 4 slots remaining
    • 2024-10-01 noon
      * 2 slots remaining

On the same form, I can register myself and another for the 2024-10-01 slots as well as a 3rd person for the 10 am slot: I register 3 people for 2 different slots.

The schema I planned to receive is (rendered as JSON for readability):

{
  "registration": {
    "email": "john.smith@example.com",
    "vpld": {
      "new0": {
        "name": "john smith",
        "role": "string"
      }
    },
    "hyuk": {
      "new0": {
        "name": "john smith",
        "role": "string"
      },
      "new1": {
        "name": "jane smith",
        "role": "string"
      },
    }
}

In this schema, vpld and hyuk are event slugs. new0 and new1 are simple indicators; it could be an array and I would be fine.

The schema I have declared in my action is:

params do
  required(:slug).filled(:string) # coming from URL
  required(:registration).hash do # form parameters
    required(:email).filled(:string) # should be enhanced to :email

    # how do I declare that I will receive arbitrary strings as keys?!?
    hash do
      hash do
        required(:name).filled(:string)
        required(:role).filled(:string)
      end
    end
  end
end

I am open to changing my schema; that wouldn’t be an issue for me.

Any help appreciated!
François

PS: I understand the params block is coming from dry-schema or dry-validation, but if we want to make Hanami successful, these types of questions will have to be answered here. If it is preferable, I will ask the question in another forum.

Hey

I guess the question is, do you need to “validate” the key, cause if by arbitrary here you mean, that the key of the hash can be anything, then one way or another you have to do a little “workaround”, since AFAIK there is no way to define, that a key will be “anything”.

You could do:

  params do
    required(:registration).array(:hash) do
      required(:name).filled(:string)
      required(:role).filled(:string)
    end
  end

This would lead to something like:

{:registration=>[{:name=>"My Name"}, {:role=> "My Role"}, {}]}

But this leaves out required(:email).filled(:string)
But I think you could adapt your data structure a bit if possible, to nest the array of hashes under a constant key maybe? So make something more like:

{
  "registration": {
    "email": "john.smith@example.com",
    events: [
      "vpld": {
        "new0": {
          "name": "john smith",
          "role": "string"
        }
      },
      "hyuk": {
        "new0": {
          "name": "john smith",
          "role": "string"
        },
        "new1": {
          "name": "jane smith",
          "role": "string"
        },
      }
    ]
}

I think this less flat structure is best anyway, cause other way events are just thrown in into registration with everything else, and when you first look at the JSON, with dynamic cryptic keys its not even clear what “vpld” is etc. This JSON is more clear and makes it easier to map to a schema (and actual real life situation IMO)

Other than that, you might utilize rules from dry-validation, but those are not yet merged (I have a hanging PR with implementation but its awaiting review Feature/action contracts by krzykamil · Pull Request #451 · hanami/controller · GitHub). Using rules you can just use pure ruby to check the values, but that is more of a validation thing, while what you are looking for is schema that maps to the JSON and performs simple checks of structure and types.

1 Like

Oh, I finally decided on a MUCH simpler structure! It looks like this:

{
  "registration": {
    "email": "me@example.com",
    "events": [
      {
        "event_id": "vpld",
        "name": "jane doe",
        "role": "arbitrary string",
      },
      {
        "event_id": "vpld",
        "name": "john smith",
        "role": "some string",
      },
      {
        "event_id": "yujl",
        "name": "jane doe",
        "role": "another arbitrary string",
      }
    ]
  }
}

I was able to express this in Hanami:

  <% count = -1 %>
  <%= form_for :registration, routes.path(create_registrations, slug: season.slug) do |f| %>
    <% events.each do |event| %>
      <% f.fields_for(:events) do |f0| %>
        <% count += 1 %>
        <% f0.fields_for(count) do |f1| %>
          <%= f1.hidden_field :event_id, event.slug %>
          <%= f1.text_field :name %>
          <%= f1.text_field :role %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>

This gives me a structure over which I can create the registrations. My only problem was with form elements whose name was empty, but a quick maybe took care of that. My final params declaration is thusly:

params do
  required(:slug).filled(:string)
  required(:registration).hash do
    required(:email).filled(:string)
    required(:events).array(:hash) do
      required(:event_id).filled(:string)
      required(:name).maybe(:string)
      required(:role).maybe(:string)
    end
  end
end

Incidentally, are you in the habit of ensuring that path parameters are present in params? This is what slug above does: my route declaration is /registrations/:slug, which corresponds to a season slug.

Cheers and thanks!
François