module MergeParams::Helpers

Public Instance Methods

add_params(url = request.fullpath, new_params = {}) click to toggle source

Adds params to the query string (Unlike url_for_merge, which tries to generate a route from the params.)

# File lib/merge_params/helpers.rb, line 112
def add_params(url = request.fullpath, new_params = {})
  uri = URI(url)
  # Allow keys that are currently in query_params to be deleted by setting their value to nil in
  # new_params (including in nested hashes).
  merged_params = parse_nested_query(uri.query || '').
    deep_merge(new_params).recurse(&:compact)
  uri.query = Rack::Utils.build_nested_query(merged_params).presence
  uri.to_s
end
merge_params(new_params = {}) click to toggle source

Safely merges the given params with the params from the current request

# File lib/merge_params/helpers.rb, line 59
def merge_params(new_params = {})
  params_for_url_for.
    deep_merge(new_params.deep_symbolize_keys)
end
merge_url_for(new_params = {}) click to toggle source

Safely merges the given params with the params from the current request, then generates a route from the merged params. You can remove a key by passing nil as the value, for example {key: nil}.

# File lib/merge_params/helpers.rb, line 90
  def merge_url_for(new_params = {})
    url = url_for(merge_params(new_params))

#    # Now pass along in the *query string* any params that we couldn't pass to url_for because they
#    # were reserved options.
#    query_params_already_added = parse_nested_query(URI(url).query || '')
#    # Some params from new_params (like company_id) that we pass in may be recognized by a route and
#    # therefore no longer be query params. We use recognize_path to find those params that ended up
#    # as route params instead of query_params but are nonetheless already added to the url.
#    params_already_added = Rails.application.routes.recognize_path(url).merge(query_params_already_added)
    params_already_added = params_from_url(url)
    query_params_to_add = params_for_url_for(new_params).
      recursively_comparing(params_already_added).graph { |k,v, other|
        if v.is_a?(Hash) || v.nil? || other.nil?
          [k, v]
        end
      }
    add_params(url, query_params_to_add)
  end
params_for_url_for(params = params()) click to toggle source

Params that can safely be passed to url_for to build a route. (Used by merge_url_for.)

We exclude RESERVED_OPTIONS such as :host because such options should only come from your app code. Allowing :host to be set via query params, for example, means a bad actor could cause links that go to a different site entirely:

# Request for /things?host=somehackingsite.ru url_for(params) => “somehackingsite.ru/things

Similarly, the :controller and :action keys of `params` never come from the query string, but from `path_parameters`. (TODO: So why not just use params.except(…)?)

TODO: Why not allow :format from params? To force people to use .:format? But doesn't that also come through as params?

(And we don't even need to pass the path_parameters on to url_for because url_for already includes those (from :_recall)

# File lib/merge_params/helpers.rb, line 48
def params_for_url_for(params = params())
  params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
  params.deep_symbolize_keys.except(
    *ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS,
    :controller,
    :action,
    :format
  )
end
params_from_url(url) click to toggle source

Parsing helpers

# File lib/merge_params/helpers.rb, line 133
def params_from_url(url)
  query_params = parse_nested_query(URI(url).query || '')
  route_params = Rails.application.routes.recognize_path(url.to_s)
  params_for_url_for(
    route_params.merge(query_params)
  )
end
parse_nested_query(query) click to toggle source
# File lib/merge_params/helpers.rb, line 141
def parse_nested_query(query)
  Rack::Utils.parse_nested_query(query || '').deep_symbolize_keys
end
query_params() click to toggle source

Returns a hash of params from the query string (en.wikipedia.org/wiki/Query_string), with symbolized keys.

# File lib/merge_params/helpers.rb, line 26
def query_params
  request.query_parameters.deep_symbolize_keys
end
query_params_from_request_params() click to toggle source

request.parameters (which also includes POST params) but with only those keys that would normally be passed in a query string (without :controller, :action, :format) and with symbolized keys.

# File lib/merge_params/helpers.rb, line 19
def query_params_from_request_params
  request.parameters.deep_symbolize_keys.
    except(*request.path_parameters.deep_symbolize_keys.keys)
end
request_params() click to toggle source

request.parameters but with symbolized keys.

# File lib/merge_params/helpers.rb, line 12
def request_params
  request.parameters.deep_symbolize_keys
end
slice_params(*keys) click to toggle source

Easily extract just certain param keys.

Can't use permit().to_h — for example,

params.permit(:page, :per_page, :filters).to_h

or you'll get an error about whatever other unrelated keys happen to be set:

found unpermitted parameters: :utf8, :commit, :company_id

One good solution might be to have a permitted_params method defined with all of your permitted params for this controller, and then you could make other methods that fetch subsets of those params using slice. But if you don't want to do that, this slice_params helper is another good option.

Other options include:

  • You could add those unrelated keys to always_permitted_parameters … but that only works if all of them should be permitted everywhere — there are probably controller-specific params present that are permitted for this controller.

  • You could also change action_on_unpermitted_parameters — but unfortunately, there's no way to pass a temporary override value for that directly to permit, so the only option is to change it temporarily globally, which is inconvenient and not thread-safe.

# File lib/merge_params/helpers.rb, line 83
def slice_params(*keys)
  params_for_url_for.slice(*keys)
end