Module: MergeParams::Helpers

Extended by:
ActiveSupport::Concern
Defined in:
lib/merge_params/helpers.rb

Instance Method Summary collapse

Instance Method Details

#add_params(url = request.fullpath, new_params = {}) ⇒ Object

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



112
113
114
115
116
117
118
119
120
# 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 = {}) ⇒ Object

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



59
60
61
62
# 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 = {}) ⇒ Object

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 nil.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# 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()) ⇒ Object

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)



48
49
50
51
52
53
54
55
56
# 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) ⇒ Object

Parsing helpers



133
134
135
136
137
138
139
# 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) ⇒ Object



141
142
143
# File 'lib/merge_params/helpers.rb', line 141

def parse_nested_query(query)
  Rack::Utils.parse_nested_query(query || '').deep_symbolize_keys
end

#query_paramsObject

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



26
27
28
# File 'lib/merge_params/helpers.rb', line 26

def query_params
  request.query_parameters.deep_symbolize_keys
end

#query_params_from_request_paramsObject

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.



19
20
21
22
# 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_paramsObject

request.parameters but with symbolized keys.



12
13
14
# File 'lib/merge_params/helpers.rb', line 12

def request_params
  request.parameters.deep_symbolize_keys
end

#slice_params(*keys) ⇒ Object

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.



83
84
85
# File 'lib/merge_params/helpers.rb', line 83

def slice_params(*keys)
  params_for_url_for.slice(*keys)
end