-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add URI::Params::Serializable
#14684
Changes from 1 commit
d3998ec
55fdf6f
02fd005
31b2ca8
4e433c5
f7df31b
282c841
24e3631
8a55775
7dd2117
50a6b7c
b8af960
b62e3d5
1fa505a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
require "spec" | ||
require "uri/params/serializable" | ||
|
||
private enum Color | ||
Red | ||
Green | ||
Blue | ||
end | ||
|
||
describe ".from_form_data" do | ||
describe Bool do | ||
it "a truthy value" do | ||
Bool.from_form_data(URI::Params.parse("alive=true"), "alive").should be_true | ||
Bool.from_form_data(URI::Params.parse("alive=on"), "alive").should be_true | ||
Bool.from_form_data(URI::Params.parse("alive=yes"), "alive").should be_true | ||
Bool.from_form_data(URI::Params.parse("alive=1"), "alive").should be_true | ||
end | ||
|
||
it "a falsey value" do | ||
Bool.from_form_data(URI::Params.parse("alive=false"), "alive").should be_false | ||
Bool.from_form_data(URI::Params.parse("alive=off"), "alive").should be_false | ||
Bool.from_form_data(URI::Params.parse("alive=no"), "alive").should be_false | ||
Bool.from_form_data(URI::Params.parse("alive=0"), "alive").should be_false | ||
end | ||
|
||
it "any other value" do | ||
Bool.from_form_data(URI::Params.parse("alive=foo"), "alive").should be_nil | ||
end | ||
|
||
it "missing value" do | ||
Bool.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
|
||
describe String do | ||
it "valid value" do | ||
String.from_form_data(URI::Params.parse("name=John Doe"), "name").should eq "John Doe" | ||
end | ||
|
||
it "missing value" do | ||
String.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
|
||
describe Enum do | ||
it "valid value" do | ||
Color.from_form_data(URI::Params.parse("color=green"), "color").should eq Color::Green | ||
end | ||
|
||
it "missing value" do | ||
Color.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
|
||
describe Time do | ||
it "valid value" do | ||
Time.from_form_data(URI::Params.parse("time=2016-11-16T09:55:48-03:00"), "time").try &.to_utc.should eq(Time.utc(2016, 11, 16, 12, 55, 48)) | ||
Time.from_form_data(URI::Params.parse("time=2016-11-16T09:55:48-0300"), "time").try &.to_utc.should eq(Time.utc(2016, 11, 16, 12, 55, 48)) | ||
Time.from_form_data(URI::Params.parse("time=20161116T095548-03:00"), "time").try &.to_utc.should eq(Time.utc(2016, 11, 16, 12, 55, 48)) | ||
end | ||
|
||
it "missing value" do | ||
Time.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
|
||
it Nil do | ||
Nil.from_form_data(URI::Params.new, "name").should be_nil | ||
Nil.from_form_data(URI::Params.parse("name=null"), "name").should be_nil | ||
end | ||
|
||
describe Number do | ||
describe Int do | ||
it "valid numbers" do | ||
Int64.from_form_data(URI::Params.parse("value=123"), "value").should eq 123_i64 | ||
UInt8.from_form_data(URI::Params.parse("value=7"), "value").should eq 7_u8 | ||
Int64.from_form_data(URI::Params.parse("value=-12"), "value").should eq -12_i64 | ||
end | ||
|
||
it "with whitespace" do | ||
expect_raises ArgumentError do | ||
Int32.from_form_data(URI::Params.parse("value= 123"), "value") | ||
end | ||
end | ||
|
||
it "missing value" do | ||
Int32.from_form_data(URI::Params.new, "value").should be_nil | ||
UInt8.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
|
||
describe Float do | ||
it "valid numbers" do | ||
Float32.from_form_data(URI::Params.parse("value=123.0"), "value").should eq 123_f32 | ||
Float64.from_form_data(URI::Params.parse("value=123.0"), "value").should eq 123_f64 | ||
end | ||
|
||
it "with whitespace" do | ||
expect_raises ArgumentError do | ||
Float64.from_form_data(URI::Params.parse("value= 123.0"), "value") | ||
end | ||
end | ||
|
||
it "missing value" do | ||
Float64.from_form_data(URI::Params.new, "value").should be_nil | ||
end | ||
end | ||
end | ||
|
||
describe Union do | ||
it "valid" do | ||
String?.from_form_data(URI::Params.parse("name=John Doe"), "name").should eq "John Doe" | ||
end | ||
|
||
it "invalid" do | ||
expect_raises ArgumentError do | ||
(Int32 | Float64).from_form_data(URI::Params.parse("value=foo"), "value") | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
require "spec" | ||
require "uri/params/serializable" | ||
|
||
private record SimpleType, page : Int32, strict : Bool, per_page : UInt8 do | ||
include URI::Params::Serializable | ||
end | ||
|
||
private record SimpleTypeDefaults, page : Int32, strict : Bool, per_page : Int32 = 10 do | ||
include URI::Params::Serializable | ||
end | ||
|
||
private record SimpleTypeNilable, page : Int32, strict : Bool, per_page : Int32? = nil do | ||
include URI::Params::Serializable | ||
end | ||
|
||
private record SimpleTypeNilableDefault, page : Int32, strict : Bool, per_page : Int32? = 20 do | ||
include URI::Params::Serializable | ||
end | ||
|
||
record Filter, status : String?, total : Float64? do | ||
include URI::Params::Serializable | ||
end | ||
|
||
record Search, filter : Filter?, limit : Int32 = 25, offset : Int32 = 0 do | ||
include URI::Params::Serializable | ||
end | ||
|
||
record GrandChild, name : String do | ||
include URI::Params::Serializable | ||
end | ||
|
||
record Child, status : String?, grand_child : GrandChild do | ||
include URI::Params::Serializable | ||
end | ||
|
||
record Parent, child : Child do | ||
include URI::Params::Serializable | ||
end | ||
|
||
describe URI::Params::Serializable do | ||
it "simple type" do | ||
SimpleType.from_form_data(URI::Params.parse("page=10&strict=true&per_page=5")).should eq SimpleType.new(10, true, 5) | ||
end | ||
|
||
it "missing required property" do | ||
expect_raises URI::SerializableError, "Missing required property: 'page'." do | ||
SimpleType.from_form_data(URI::Params.parse("strict=true&per_page=5")) | ||
end | ||
end | ||
|
||
it "with default values" do | ||
SimpleTypeDefaults.from_form_data(URI::Params.parse("page=10&strict=true")).should eq SimpleTypeDefaults.new(10, true, 10) | ||
end | ||
|
||
it "with nilable values" do | ||
SimpleTypeNilable.from_form_data(URI::Params.parse("page=10&strict=true")).should eq SimpleTypeNilable.new(10, true, nil) | ||
end | ||
|
||
it "with nilable default" do | ||
SimpleTypeNilableDefault.from_form_data(URI::Params.parse("page=10&strict=true")).should eq SimpleTypeNilableDefault.new(10, true, 20) | ||
end | ||
|
||
describe "nested type" do | ||
it "happy path" do | ||
Search.from_form_data(URI::Params.parse("offset=10&filter[status]=active&filter[total]=3.14")) | ||
.should eq Search.new Filter.new("active", 3.14), offset: 10 | ||
end | ||
|
||
it "missing nilable nested data" do | ||
Search.from_form_data(URI::Params.parse("offset=10")) | ||
.should eq Search.new Filter.new(nil, nil), offset: 10 | ||
end | ||
|
||
it "missing required nested property" do | ||
expect_raises URI::SerializableError, "Missing required property: 'child[grand_child][name]'." do | ||
Parent.from_form_data(URI::Params.parse("child[status]=active")) | ||
end | ||
end | ||
|
||
it "doubly nested" do | ||
Parent.from_form_data(URI::Params.parse("child[status]=active&child[grand_child][name]=Fred")) | ||
.should eq Parent.new Child.new("active", GrandChild.new("Fred")) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
def Array.from_form_data(params : URI::Params, name : String) | ||
params.fetch_all(name).map { |item| T.from_form_data(params, name).as T } | ||
end | ||
|
||
def Bool.from_form_data(params : URI::Params, name : String) | ||
case params[name]? | ||
when "true", "1", "yes", "on" then true | ||
when "false", "0", "no", "off" then false | ||
end | ||
end | ||
|
||
def Number.from_form_data(params : URI::Params, name : String) | ||
return nil unless value = params[name]? | ||
|
||
new value, whitespace: false | ||
end | ||
|
||
def String.from_form_data(params : URI::Params, name : String) | ||
params[name]? | ||
end | ||
|
||
def Enum.from_form_data(params : URI::Params, name : String) | ||
return nil unless value = params[name]? | ||
|
||
parse value | ||
end | ||
|
||
def Time.from_form_data(params : URI::Params, name : String) | ||
return nil unless value = params[name]? | ||
|
||
Time::Format::ISO_8601_DATE_TIME.parse value | ||
end | ||
|
||
def Union.from_form_data(params : URI::Params, name : String) | ||
# Process non nilable types first as they are more likely to work. | ||
{% for type in T.sort_by { |t| t.nilable? ? 1 : 0 } %} | ||
begin | ||
return {{type}}.from_form_data params, name | ||
rescue | ||
# Noop to allow next T to be tried. | ||
end | ||
{% end %} | ||
raise ArgumentError.new "Invalid #{self}: #{params[name]}" | ||
end | ||
|
||
def Nil.from_form_data(params : URI::Params, name : String) : Nil | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
require "uri" | ||
|
||
require "./from_form_data" | ||
|
||
module URI::Params::Serializable | ||
macro included | ||
def self.from_form_data(params : ::URI::Params) | ||
new_from_form_data(params) | ||
end | ||
|
||
# :nodoc: | ||
# | ||
# This is needed so that nested types can pass the name thru internally. | ||
# Has to be public so the generated code can call it, but should be considered an implementation detail. | ||
def self.from_form_data(params : ::URI::Params, name : String) | ||
new_from_form_data(params, name) | ||
end | ||
|
||
protected def self.new_from_form_data(params : ::URI::Params, name : String? = nil) | ||
instance = allocate | ||
instance.initialize(__uri_params: params, name: name) | ||
GC.add_finalizer(instance) if instance.responds_to?(:finalize) | ||
instance | ||
end | ||
|
||
macro inherited | ||
def self.from_form_data(params : ::URI::Params) | ||
new_from_form_data(params) | ||
end | ||
|
||
# :nodoc: | ||
def self.from_form_data(params : ::URI::Params, name : String) | ||
new_from_form_data(params, name) | ||
end | ||
end | ||
end | ||
|
||
# :nodoc: | ||
def initialize(*, __uri_params params : ::URI::Params, name : String?) | ||
{% begin %} | ||
{% for ivar, idx in @type.instance_vars %} | ||
%name{idx} = name.nil? ? {{ivar.name.stringify}} : "#{name}[#{{{ivar.name.stringify}}}]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A top level call to Would be possible to change this, but this is what I went with because I tested PHP does parse it out into a nested assoc array tho, but that would require making |
||
|
||
if v = {{ivar.type}}.from_form_data(params, %name{idx}) | ||
@{{ivar.name.id}} = v | ||
else | ||
{% unless ivar.type.resolve.nilable? || ivar.has_default_value? %} | ||
raise URI::SerializableError.new "Missing required property: '#{%name{idx}}'." | ||
{% end %} | ||
end | ||
{% end %} | ||
{% end %} | ||
end | ||
end | ||
|
||
class URI::SerializableError < URI::Error | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
whitespace: false
here so that it doesn't considerage= 25
to be valid.