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

Working out Array param stuff #1527

Merged
merged 4 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion spec/lucky/action_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ class OptionalParams::Index < TestAction
param bool_with_false_default : Bool? = false
# This is to test that an explicit 'nil' can be assigned for nilable types
param nilable_with_explicit_nil : Int32? = nil
param nilable_array_with_default : Array(String)? = [] of String
param with_array_default : Array(Int32) = [26, 37, 44]

get "/optional_params" do
plain_text "optional param: #{page} #{with_int_default} #{with_int_never_nil}"
Expand Down Expand Up @@ -276,7 +278,7 @@ describe Lucky::Action do
end

it "returns optional param declarations" do
OptionalParams::Index.query_param_declarations.size.should eq 6
OptionalParams::Index.query_param_declarations.size.should eq 8
OptionalParams::Index.query_param_declarations.should contain "bool_with_false_default : Bool | ::Nil"
end
end
Expand Down Expand Up @@ -388,6 +390,21 @@ describe Lucky::Action do
OptionalParams::Index.new(build_context(path: "/?with_int_never_nil=no_int"), params()).call
end
end

it "allows nilable arrays with defaults" do
action = OptionalParams::Index.new(build_context(path: "/?page=3"), params)
action.nilable_array_with_default.should eq([] of String)
end

it "sets a value to a nilable array" do
action = OptionalParams::Index.new(build_context(path: "/?nilable_array_with_default[]=1&nilable_array_with_default[]=2"), params)
action.nilable_array_with_default.should eq(["1", "2"])
end

it "allows required arrays with defaults" do
action = OptionalParams::Index.new(build_context(path: "/?with_array_default=2222222"), params)
action.with_array_default.should eq([26, 37, 44])
end
end
end

Expand Down
142 changes: 139 additions & 3 deletions spec/lucky/params_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ describe Lucky::Params do

params = Lucky::Params.new(request)

params.nested?(:user)
params.nested?(:user)
params.nested?(:user).should eq({"name" => "Paul", "age" => "28"})
params.nested?(:user).should eq({"name" => "Paul", "age" => "28"})
end

it "works when parsing multipart params twice" do
it "works when parsing json params twice" do
request = build_request body: {page: 1}.to_json,
content_type: "application/json",
fixed_length: true
Expand Down Expand Up @@ -443,6 +443,109 @@ describe Lucky::Params do
end
end

describe "nested_arrays" do
it "gets nested arrays from form encoded params" do
request = build_request body: "user:name=paul&user:langs[]=ruby&user:langs[]=elixir",
content_type: "application/x-www-form-urlencoded"

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({"langs" => ["ruby", "elixir"]})
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is eq not includes shouldnt "name" => "paul" be in the params hash?

Copy link
Member Author

Choose a reason for hiding this comment

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

This one was a little tricky. The method only returns the nested arrays, it doesn't return any other keys. This is similar to how the file param stuff works.

The reason why is because I either had to turn the non-array pairs in to "name" => ["paul"] to keep the single Hash(String, Array(String)) type, or, have the value be a union in which case calling methods on the values becomes a little more difficult. If we merged all of them in to a single method, then the type signature becomes pretty gnarly.

end

it "gets nested arrays from JSON params" do
request = build_request body: {user: {name: "Paul", langs: ["ruby", "elixir"]}}.to_json,
content_type: "application/json"
request.query = "from=query"

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({"langs" => ["ruby", "elixir"]})
end

it "gets empty JSON params when nested key is missing" do
request = build_request body: "{}",
content_type: "application/json"
request.query = "from=query"

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({} of String => JSON::Any)
end

it "handles JSON with charset directive in Content-Type header" do
request = build_request body: {user: {name: "Paul", langs: ["ruby", "elixir"]}}.to_json,
content_type: "application/json; charset=UTF-8"

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({"langs" => ["ruby", "elixir"]})
end

it "gets nested array JSON params mixed with query params" do
request = build_request body: {user: {name: "Bunyan", tags: ["tall"]}}.to_json,
content_type: "application/json"
request.query = "user:tags[]=tale"

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({"tags" => ["tall", "tale"]})
end

it "gets nested arrays from multipart params" do
request = build_multipart_request form_parts: {
"user:name" => "Paul", "user:langs" => ["ruby", "elixir"],
}

params = Lucky::Params.new(request)

params.nested_arrays?(:user).should eq({"langs" => ["ruby", "elixir"]})
end

it "gets nested arrays from query params" do
request = build_request body: "filter:toppings[]=sausage", content_type: ""
request.query = "filter:toppings[]=black_olive"
params = Lucky::Params.new(request)
params.nested_arrays?("filter").should eq({"toppings" => ["sausage", "black_olive"]})
end

it "returns an empty hash when no nested array is found" do
request = build_request body: "", content_type: ""
request.query = "a[]=1"
params = Lucky::Params.new(request)
params.nested_arrays?("a").empty?.should eq true
end

it "gets nested array params after unescaping" do
request = build_request body: "post%3Atags[]=coding",
content_type: "application/x-www-form-urlencoded"

params = Lucky::Params.new(request)

params.nested_arrays?(:post).should eq({"tags" => ["coding"]})
end

it "raises if nested array params are missing" do
request = build_request body: "",
content_type: "application/x-www-form-urlencoded"

params = Lucky::Params.new(request)

expect_raises Lucky::MissingNestedParamError do
params.nested_arrays(:missing)
end
end

it "returns empty hash if nested array params are missing" do
request = build_request body: "",
content_type: "application/x-www-form-urlencoded"

params = Lucky::Params.new(request)

params.nested_arrays?(:missing).should eq({} of String => Array(String))
end
end

describe "get_file" do
it "gets files" do
request = build_multipart_request file_parts: {
Expand Down Expand Up @@ -524,6 +627,39 @@ describe Lucky::Params do
end
end

describe "nested_array_files" do
it "gets multipart nested array params" do
request = build_multipart_request file_parts: {
"user:photos" => ["cat", "dog"],
}

params = Lucky::Params.new(request)

files = params.nested_array_files(:user)["photos"]
files.size.should eq(2)
File.read(files[0].path).should eq("cat")
File.read(files[1].path).should eq("dog")
end

it "raises if nested array files are missing" do
request = build_multipart_request form_parts: {"this" => "that"}

params = Lucky::Params.new(request)

expect_raises Lucky::MissingNestedParamError do
params.nested_array_files(:missing)
end
end

it "returns empty hash if nested array files are missing" do
request = build_multipart_request form_parts: {"this" => "that"}

params = Lucky::Params.new(request)

params.nested_array_files?(:missing).should eq({} of String => Array(Lucky::UploadedFile))
end
end

describe "many_nested" do
it "gets nested form encoded params" do
request = build_request body: "users[0]:name=paul&users[1]:twitter_handle=@paulamason&users[0]:twitter_handle=@paulsmith&users[1]:name=paula&something:else=1",
Expand Down
6 changes: 6 additions & 0 deletions spec/support/multipart_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ module MultipartHelper
multipart_file_part(formdata, nested_name, nested_value)
end
end

private def multipart_file_part(formdata : HTTP::FormData::Builder, name : String, value : Array(String))
value.each do |val|
multipart_file_part(formdata, name + "[]", val)
end
end
end
111 changes: 110 additions & 1 deletion src/lucky/params.cr
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ class Lucky::Params
multipart_files[key.to_s]?
end

def get_all_files(key : String | Symbol) : Array(Lucky::UploadedFile)
get_all_files?(key) || raise Lucky::MissingParamError.new(key.to_s)
end

def get_all_files?(key : String | Symbol) : Array(Lucky::UploadedFile)
multipart_files.fetch_all(key.to_s)
end

# Retrieve a nested value from the params
#
# Nested params often appear in JSON requests or Form submissions. If no key
Expand Down Expand Up @@ -255,6 +263,36 @@ class Lucky::Params
end
end

# Retrieve a nested array from the params
#
# Nested params often appear in JSON requests or Form submissions. If no key
# is found a `Lucky::MissingParamError` will be raised:
#
# ```
# params.nested_array("tags") # {"tags" => ["Lucky", "Crystal"]}
# params.nested_array("missing") # Missing parameter: missing
# ```
def nested_arrays(nested_key : String | Symbol) : Hash(String, Array(String))
nested_params = nested_arrays?(nested_key)
if nested_params.keys.empty?
raise Lucky::MissingNestedParamError.new nested_key
else
nested_params
end
end

def nested_arrays?(nested_key : String | Symbol) : Hash(String, Array(String))
if json?
nested_array_json_params(nested_key.to_s).merge(nested_array_query_params(nested_key.to_s)) do |_k, v1, v2|
v1 + v2
end
else
nested_array_form_params(nested_key.to_s).merge(nested_array_query_params(nested_key.to_s)) do |_k, v1, v2|
v1 + v2
end
end
end

# Retrieve a nested file from the params
#
# Nested params often appear in JSON requests or Form submissions. If no key
Expand Down Expand Up @@ -286,6 +324,19 @@ class Lucky::Params
nested_file_params(nested_key.to_s)
end

def nested_array_files(nested_key : String | Symbol) : Hash(String, Array(Lucky::UploadedFile))
nested_file_params = nested_array_files?(nested_key)
if nested_file_params.keys.empty?
raise Lucky::MissingNestedParamError.new nested_key
else
nested_file_params
end
end

def nested_array_files?(nested_key : String | Symbol) : Hash(String, Array(Lucky::UploadedFile))?
nested_array_file_params(nested_key.to_s)
end

# Retrieve nested values from the params
#
# Nested params often appear in JSON requests or Form submissions. If no key
Expand Down Expand Up @@ -357,7 +408,7 @@ class Lucky::Params

private def nested_json_params(nested_key : String) : Hash(String, String)
nested_params = {} of String => String
nested_key_json = parsed_json[nested_key]? || JSON.parse("{}")
nested_key_json = parsed_json[nested_key]? || JSON::Any.new({} of String => JSON::Any)

nested_key_json.as_h.each do |key, value|
nested_params[key.to_s] = value.to_s
Expand All @@ -366,6 +417,19 @@ class Lucky::Params
nested_params
end

private def nested_array_json_params(nested_key : String) : Hash(String, Array(String))
nested_params = {} of String => Array(String)
nested_key_json = parsed_json[nested_key]? || JSON::Any.new({} of String => JSON::Any)

nested_key_json.as_h.each do |key, value|
if array_value = value.as_a?
nested_params[key.to_s] = array_value.map(&.to_s)
end
end

nested_params
end

private def nested_form_params(nested_key : String) : Hash(String, String)
nested_key = "#{nested_key}:"
source = multipart? ? multipart_params : form_params
Expand All @@ -378,6 +442,22 @@ class Lucky::Params
end
end

private def nested_array_form_params(nested_key : String) : Hash(String, Array(String))
nested_key = "#{nested_key}:"
nested_params = {} of String => Array(String)

source = multipart? ? multipart_params : form_params
source.each do |key, value|
if key.starts_with?(nested_key) && key.ends_with?("[]")
new_key = key.lchop(nested_key).rchop("[]")
nested_params[new_key.to_s] ||= [] of String
nested_params[new_key.to_s] << value
end
end

nested_params
end

private def nested_query_params(nested_key : String) : Hash(String, String)
nested_key = "#{nested_key}:"
query_params.to_h.reduce(empty_params) do |nested_params, (key, value)|
Expand All @@ -389,6 +469,20 @@ class Lucky::Params
end
end

private def nested_array_query_params(nested_key : String) : Hash(String, Array(String))
nested_key = "#{nested_key}:"
nested_params = {} of String => Array(String)
query_params.each do |key, value|
if key.starts_with?(nested_key) && key.ends_with?("[]")
new_key = key.lchop(nested_key).rchop("[]")
nested_params[new_key.to_s] ||= [] of String
nested_params[new_key.to_s] << value
end
end

nested_params
end

private def nested_file_params(nested_key : String) : Hash(String, Lucky::UploadedFile)
nested_key = "#{nested_key}:"
multipart_files.to_h.reduce(empty_file_params) do |nested_params, (key, value)|
Expand All @@ -400,6 +494,21 @@ class Lucky::Params
end
end

private def nested_array_file_params(nested_key : String) : Hash(String, Array(Lucky::UploadedFile))
nested_key = "#{nested_key}:"
nested_params = {} of String => Array(Lucky::UploadedFile)

multipart_files.each do |key, value|
if key.starts_with?(nested_key) && key.ends_with?("[]")
new_key = key.lchop(nested_key).rchop("[]")
nested_params[new_key.to_s] ||= [] of Lucky::UploadedFile
nested_params[new_key.to_s] << value
end
end

nested_params
end

private def zipped_many_nested_params(nested_key : String)
body_params = many_nested_body_params(nested_key)
query_params = many_nested_query_params(nested_key)
Expand Down
Loading