Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sift supports specifying types of scope arguments #48

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

andrew-wheeler
Copy link

@andrew-wheeler andrew-wheeler commented May 5, 2020

Please check out the README.md changes for the idea. I'm extremely open to feedback.

I was motivated to do this as a feature request from @bradleyrzeller

There's some significant refactoring in this PR. I went through several attempts before I arrived at these changes. Hopefully they seem ok. I'll add comments about my rationale.

@@ -372,7 +372,7 @@ Style/StringLiterals:
Style/TrailingCommaInArguments:
Description: 'Checks for trailing comma in argument lists.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
EnforcedStyleForMultiline: comma
EnforcedStyleForMultiline: no_comma
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was driving me nuts, but I'm definitely willing to revert this.

@@ -66,7 +66,16 @@ def sort_fields

class_methods do
def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: [])
filters << Filter.new(parameter, type, internal_name, default, validate, scope_params)
if type.respond_to?(:key?)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely open to feedback on how to structure supplying the scope types.

else
handler.call(collection, parameterize(value), params, scope_params)
handler(value, params).call(collection)
Copy link
Author

@andrew-wheeler andrew-wheeler May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A fairly large change here: handlers are initialized with data from the request, but they are always called with just the collection. This allows us to treat the default handler, where handler, and scope handler as having the same interface once they are constructed without passing any unused values.

@@ -38,7 +39,7 @@ def validation_field
end

def type_validator
@type_validator ||= Sift::TypeValidator.new(param, type)
@type_validator ||= Sift::TypeValidator.new(param, validation_type)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows us to validate the "chief" parameter to scope filters.

@@ -51,10 +52,6 @@ def param

private

def parameterize(value)
Copy link
Author

@andrew-wheeler andrew-wheeler May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handlers take over responsibility for parsing the raw values from the request. This allows the scope handler to use its types to choose the right parsing strategy.

@@ -73,12 +70,44 @@ def valid_scope_params?(scope_params)
scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) }
end

def handler
parameter.handler
Copy link
Author

@andrew-wheeler andrew-wheeler May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't have Parameter be responsible for the handler, because scope filters need more than just the "chief" parameter to handle.

@@ -8,35 +8,5 @@ def initialize(param, type, internal_name = param)
@type = type
@internal_name = internal_name
end

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter loses responsibility for parsing values because we want to parse things other than just the "chief" parameter.

@supports_boolean = options.fetch(:supports_boolean, false)
@supports_ranges = options.fetch(:supports_ranges, false)
@supports_json = options.fetch(:supports_json, false)
def initialize(value:, type: nil)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValueParser can be responsible for determining supports_boolean, supports_ranges, and supports_json from the type, so it doesn't need to be passed in. This helps us parse values for things other than the "chief" parameter.

@@ -1,3 +1,3 @@
module Sift
VERSION = "0.13.0".freeze
VERSION = "0.14.0".freeze
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm bumping the minor here because if anything is using the Sift internals (Sift::ValueParser, Sift::Parameter) it could be broken.

Copy link
Contributor

@amayer171 amayer171 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the term "chief parameter" could use some clarification, but I think I know what you are saying its the root type.

Comment on lines 71 to 72
raise ArgumentError, "filter_on: Only type: :scope can have subtypes. Expecting the form `type: {scope: [type, {param: type}]}`"
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this validation into the Filter class since it has access to type and scope_types it could do this validation there pretty easily such as in the validate_scope_types! method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I'll make this change unless we decide on some other filter_on interface.

if type != :scope || scope_types.empty?
type
else
scope_types.first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a single scope type is allowed right? If thats the case should the argument be changed to scope_type with a default value of nil?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thought is that scope_type param is unrelated to other filters. Should we create a different filter class to have better argument cohesion for scopes and so that way we aren't complicating other filters with logic specific to scopes?

Copy link
Author

@andrew-wheeler andrew-wheeler May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope_types can have something like [:int, {foo: :datetime}] or [:int]. I can definitely see making some kind of inheritance / mixin relationship between new classes WhereFilter and ScopeFilter. I think they both support the default: option so there's some overlap. I totally agree that the frequently unused scope_params and scope_types arguments are a smell.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of this @amayer171 ?
#49

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merged...

@andrew-wheeler andrew-wheeler marked this pull request as ready for review May 5, 2020 21:55
README.md Outdated Show resolved Hide resolved
end
end
```
Passing `?filters[with_priority]=[1,2]` will call the `with_priority` scope with the array, `[1, 2]`, instead of `"[1,2]"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be much consensus over how arrays should be used in query parameters.
https://medium.com/raml-api/arrays-in-query-params-33189628fa68

But =1,2 appears to be a more common pattern than =[1, 2]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sift uses the JSON standard for array parameters. My(unpopular?) opinion is that we should use either the Rails standard filters[foo][]=1&filters[foo][]=2 or POST JSON and use the X-http-method-override header to specify GET simulation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think this PR is not affected by that decision, however. We could change this code to parse however we want:

https://github.com/procore/sift/blob/master/lib/sift/value_parser.rb#L24-L33

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of filters[foo][]=1&filters[foo][]=2 is that really a Rails standard? Can you explain X-http-method-override with GET simulation in more detail?

Copy link
Author

@andrew-wheeler andrew-wheeler May 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@njbbaer
By setting a header with key X-Http-Method-Override and value get in a JSON POST request (for example) you can specify that the request should be handled as a GET in servers that support this header (Rails servers).

So you can POST with a body of {"filters": {"foo": [1,2]}} with rails treating this as a GET with params of { filters: {foo: [1,2]}}. This sidesteps all issues with URL length, and allows your request to use actual JSON instead of mixing URL parameters with JSON. In practice it works really well. Here is example code from the vector_processing repo:

      def large_get(path, params, timeout: 180)
        url = vapid_uri_base + path
        retries = 0
        begin
          raw_response = RestClient::Request.execute(
            url: url,
            method: :post,
            headers: {'Accept' => 'application/json;charset=utf-8', 'Content-Type' => 'application/json', 'Authorization' => authorization.authorization_header, 'X-Http-Method-Override' => 'get'},
            open_timeout: 3,
            timeout: timeout,
            verify_ssl: VectorProcessing::Vapid::Authorization.verify_ssl?,
            payload: params.to_json
          )
          JSON.parse(raw_response.body.force_encoding('UTF-8'))
        rescue RestClient::Unauthorized
          retry if authorization.refresh_success? && (retries+=1) < 5
        rescue RestClient::ExceptionWithResponse => rcewr
          raise ErrorResponseException.new("Error Response #{rcewr.response&.code || 'missing'} to X-Http-Method-Override 'get' on POST '#{url}'")
        end
      end

This requires no change to Vapid; it's supported by Rails out-of-the-box.

Co-authored-by: Nate Baer <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants