Skip to content

Commit

Permalink
Replace avram parsing with Lucky::ParamParser (#1616)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmcgarvey authored Nov 20, 2021
1 parent 10b19c4 commit e13a8a6
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 4 deletions.
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
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 }})

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

0 comments on commit e13a8a6

Please sign in to comment.