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

Replace avram parsing with ParamParser #1616

Merged
merged 1 commit into from
Nov 20, 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
127 changes: 127 additions & 0 deletions spec/lucky/param_parser_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require "../spec_helper"

enum TimeComponent
Year
Month
Day
Hour
Minute
Second
end

struct FormattedTime
Copy link
Member Author

Choose a reason for hiding this comment

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

All of this was directly taken from Avram https://github.com/luckyframework/avram/blob/9a5a921bb05cc3031d9436d2fdc6927606314876/spec/type_extensions/time_spec.cr#L3-L27

@stephendolan saved me from having to figure out all these tests myself 😅

property label : String
property value : String
property components : Array(TimeComponent)

def initialize(@label : String, @value : String, excluded_components : Array(TimeComponent) = [] of TimeComponent)
@components = [
TimeComponent::Year,
TimeComponent::Month,
TimeComponent::Day,
TimeComponent::Hour,
TimeComponent::Minute,
TimeComponent::Second,
] - excluded_components
end
end

macro numbers_tests(klass, input, output)
describe "parse {{ klass }}" do
it "turns string into number" do
Lucky::ParamParser.parse({{ input }}, {{ klass }}).should eq({{ output }})
end

it "returns nil if param not number" do
Lucky::ParamParser.parse("abc", {{ klass }}).should be_nil
end

it "returns nil if param blank" do
Lucky::ParamParser.parse("", {{ klass }}).should be_nil
end
end
end

describe Lucky::ParamParser do
numbers_tests(Int16, "1", 1_i16)
numbers_tests(Int32, "12", 12)
numbers_tests(Int64, "144", 144_i64)
numbers_tests(Float64, "1.23", 1.23_f64)

describe "parse String" do
it "does not change the value" do
Lucky::ParamParser.parse("foo", String).should eq("foo")
end
end

describe "parse Bool" do
it "parses forms of true" do
Lucky::ParamParser.parse("true", Bool).should be_true
Lucky::ParamParser.parse("1", Bool).should be_true
end

it "parses forms of false" do
Lucky::ParamParser.parse("false", Bool).should be_false
Lucky::ParamParser.parse("0", Bool).should be_false
end

it "returns nil for other values" do
Lucky::ParamParser.parse("asdf", Bool).should be_nil
end
end

describe "parse UUID" do
it "parses uuid string" do
uuid = "0881a13e-e283-45a0-9dba-6d05463eec45"

Lucky::ParamParser.parse(uuid, UUID).should eq(UUID.new(uuid))
end

it "returns nil if not uuid" do
Lucky::ParamParser.parse("INVALID", UUID).should be_nil
end
end

describe "parse Time" do
it "parses various formats successfully" do
time = Time.utc
[
FormattedTime.new("ISO 8601", time.to_s("%FT%X%z")),
FormattedTime.new("RFC 2822", time.to_rfc2822),
FormattedTime.new("RFC 3339", time.to_rfc3339),
FormattedTime.new("DateTime HTML Input", time.to_s("%Y-%m-%dT%H:%M:%S")),
FormattedTime.new("DateTime HTML Input (no seconds)", time.to_s("%Y-%m-%dT%H:%M"), excluded_components: [TimeComponent::Second]),
FormattedTime.new("HTTP Date", time.to_s("%a, %d %b %Y %H:%M:%S GMT")),
].each do |formatted_time|
result = Lucky::ParamParser.parse(formatted_time.value, Time)

result.should_not be_nil
result = result.not_nil!
result.year.should eq(time.year) if formatted_time.components.includes? TimeComponent::Year
result.month.should eq(time.month) if formatted_time.components.includes? TimeComponent::Month
result.day.should eq(time.day) if formatted_time.components.includes? TimeComponent::Day
result.hour.should eq(time.hour) if formatted_time.components.includes? TimeComponent::Hour
result.minute.should eq(time.minute) if formatted_time.components.includes? TimeComponent::Minute
result.second.should eq(time.second) if formatted_time.components.includes? TimeComponent::Second
end
end

it "returns nil if unable to parse" do
Lucky::ParamParser.parse("INVALID", Time).should be_nil
end
end

describe "parse Array(T)" do
it "handles strings" do
Lucky::ParamParser.parse(["a", "b"], Array(String)).should eq(["a", "b"])
end

it "handles numbers" do
Lucky::ParamParser.parse(["1", "2"], Array(Int32)).should eq([1, 2])
end

it "handles bools" do
Lucky::ParamParser.parse(["1", "0", "true"], Array(Bool)).should eq([true, false, true])
end
end
end
69 changes: 69 additions & 0 deletions src/lucky/param_parser.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Lucky::ParamParser
TIME_FORMATS = [
Time::Format::ISO_8601_DATE_TIME,
Time::Format::RFC_2822,
Time::Format::RFC_3339,
# HTML datetime-local inputs are basically RFC 3339 without the timezone:
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local
Time::Format.new("%Y-%m-%dT%H:%M:%S", Time::Location::UTC),
Time::Format.new("%Y-%m-%dT%H:%M", Time::Location::UTC),
# Dates and times go last, otherwise it will parse strings with both
# dates *and* times incorrectly.
Time::Format::HTTP_DATE,
Time::Format::ISO_8601_DATE,
Time::Format::ISO_8601_TIME,
]

def self.parse(param : String, klass : String.class) : String
param
end

def self.parse(param : String, klass : Int16.class) : Int16?
param.to_i16?
end

def self.parse(param : String, klass : Int32.class) : Int32?
param.to_i?
end

def self.parse(param : String, klass : Int64.class) : Int64?
param.to_i64?
end

def self.parse(param : String, klass : Float64.class) : Float64?
param.to_f?
end

def self.parse(param : String, klass : Bool.class) : Bool?
if %w(true 1).includes? param
true
elsif %w(false 0).includes? param
false
else
nil
end
end

def self.parse(param : String, klass : UUID.class) : UUID?
UUID.new(param)
rescue
nil
end

def self.parse(param : String, klass : Time.class) : Time?
TIME_FORMATS.each do |format|
begin
parsed = format.parse(param)
return parsed if parsed
rescue e : Time::Format::Error
nil
end
end
end

def self.parse(param : Array(String), klass : Array(T).class) : Array(T)? forall T
casts = param.map { |val| parse(val, T) }

casts.any?(Nil) ? nil : casts.map(&.not_nil!)
end
end
8 changes: 4 additions & 4 deletions src/lucky/routable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -479,17 +479,17 @@ module Lucky::Routable
{% end %}
end

result = {{ base_type }}.adapter.parse(val)
result = Lucky::ParamParser.parse(val, {{ base_type }})
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 is the line where we are switching from Avram code to this new module


if result.is_a? Avram::Type::SuccessfulCast
result.value
else
if result.nil?
raise Lucky::InvalidParamError.new(
param_name: "{{ type_declaration.var.id }}",
param_value: val.to_s,
param_type: "{{ type }}"
)
end

result
end
end
end