Home > Mobile >  How to transform nested parameters in Rails API for PATCH requests
How to transform nested parameters in Rails API for PATCH requests

Time:09-19

I'm having problems trying to implement a PATCH endpoint for a Rails API which deals with complex request objects that are structurally different from the ActiveRecord model.

As an example let's say I have the following request object:

{
    "details": {
        "color": {
            "id": 1
        }
    },
    "name": "Hello, world!"
    ...
}

However, on my model I expect a flat color_id attribute:

class CreateModel < ActiveRecord::Migration[7.0]
  def change
    create_table :model do |t|
      t.string :name, null: false
      t.integer :color_id, null: false
    end
  end
end

Therefore I need to transform the request params. For this I've found one approach which works pretty well in case of PUT requests, but not at all for PATCH:

ActionController::Parameters.new({
    color_id: params.dig(:details, :color, :id),
    name: params.dig(:name)
})

If I issue a PUT request this solution works great since PUT expects the whole object as payload, PATCH on the other hand would cause issues when passing only a subset of the properties since everything else will be set to nil due to how dig works.

Assuming I have no control over the request format, how can I transform the request params in the backend so that omitted keys will not result in nil values? Of course I could imperatively handle each property line by line, checking whether the key is present in the original params and then setting it in the new one, but is there a more elegant approach?

CodePudding user response:

I've found a generic solution using mapping logic with a lookup table. For the example above:

{
    "details": {
        "color": {
            "id": 1
        }
    },
    "name": "Hello, world!"
    ...
}

I would have the following mapping variable:

MAPPING = {
    [:details, :color, :id] => [:color_id]
}

Then I'm able to transform the params using this recursive algorithm:

def handle(params, keys)
    output = Hash.new
    params.each do |k,v| 
        sym_keys = (keys   [k]).map &:to_sym
        target_keys = MAPPING[sym_keys]
        if v.is_a?(Hash)
            keys << k
            tmp = handle(v, keys)
            output = output.deep_merge!(tmp)
        end

        unless target_keys.nil?
            target_value = target_keys.reverse().reduce(v) do |v, k| 
                Hash[k, v]
            end
            output = output.deep_merge!(target_value)
        end
    end
    output
end

def transform(params)
    output = handle(params, [])
end
  • Related