From de9cff6fc2d8c48a4cf13342f22f0811cdc55a0d Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 20 Sep 2018 14:12:34 +0800 Subject: [PATCH] Add validator for datetime string Example usage: validates :start_at, date_time_string: true --- app/validators/date_time_string_validator.rb | 63 +++++++++++++++++++ config/locales/en.yml | 5 ++ .../date_time_string_validator_spec.rb | 58 +++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 app/validators/date_time_string_validator.rb create mode 100644 spec/validators/date_time_string_validator_spec.rb diff --git a/app/validators/date_time_string_validator.rb b/app/validators/date_time_string_validator.rb new file mode 100644 index 0000000000..f1a4eccbfb --- /dev/null +++ b/app/validators/date_time_string_validator.rb @@ -0,0 +1,63 @@ +# Validates a datetime string with relaxed rules +# +# This uses ActiveSupport::TimeZone.parse behind the scenes. +# +# https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse +# +# === Example +# +# class Post +# include ActiveModel::Validations +# +# attr_accessor :published_at +# validates :published_at, date_time_string: true +# end +# +# post = Post.new +# +# post.published_at = nil +# post.valid? # => true +# +# post.published_at = "" +# post.valid? # => true +# +# post.published_at = [] +# post.valid? # => false +# post.errors[:published_at] # => ["must be a string"] +# +# post.published_at = 1 +# post.valid? # => false +# post.errors[:published_at] # => ["must be a string"] +# +# post.published_at = "2018-09-20 01:02:00 +10:00" +# post.valid? # => true +# +# post.published_at = "Not Valid" +# post.valid? # => false +# post.errors[:published_at] # => ["must be valid"] +class DateTimeStringValidator < ActiveModel::EachValidator + NOT_STRING_ERROR = I18n.t("validators.date_time_string_validator.not_string_error") + INVALID_FORMAT_ERROR = I18n.t("validators.date_time_string_validator.invalid_format_error") + + def validate_each(record, attribute, value) + return if value.nil? || value == "" + + validate_attribute_is_string(record, attribute, value) + validate_attribute_is_datetime_string(record, attribute, value) + end + + protected + + def validate_attribute_is_string(record, attribute, value) + return if value.is_a?(String) + + record.errors.add(attribute, NOT_STRING_ERROR) + end + + def validate_attribute_is_datetime_string(record, attribute, value) + return unless value.is_a?(String) + + datetime = Time.zone.parse(value) + record.errors.add(attribute, INVALID_FORMAT_ERROR) if datetime.blank? + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index f0c72f289a..cea3d34d6b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -100,6 +100,11 @@ en: order_cycle: cloned_order_cycle_name: "COPY OF %{order_cycle}" + validators: + date_time_string_validator: + not_string_error: "must be a string" + invalid_format_error: "must be valid" + enterprise_mailer: confirmation_instructions: subject: "Please confirm the email address for %{enterprise}" diff --git a/spec/validators/date_time_string_validator_spec.rb b/spec/validators/date_time_string_validator_spec.rb new file mode 100644 index 0000000000..d5151fc7c5 --- /dev/null +++ b/spec/validators/date_time_string_validator_spec.rb @@ -0,0 +1,58 @@ +require "spec_helper" + +describe DateTimeStringValidator do + class TestModel + include ActiveModel::Validations + + attr_accessor :timestamp + + validates :timestamp, date_time_string: true + end + + describe "internationalization" do + it "has translation for NOT_STRING_ERROR" do + expect(described_class::NOT_STRING_ERROR).not_to be_blank + end + + it "has translation for INVALID_FORMAT_ERROR" do + expect(described_class::INVALID_FORMAT_ERROR).not_to be_blank + end + end + + describe "validation" do + let(:instance) { TestModel.new } + + it "does not add error when nil" do + instance.timestamp = nil + expect(instance).to be_valid + end + + it "does not add error when blank string" do + instance.timestamp = nil + expect(instance).to be_valid + end + + it "adds error NOT_STRING_ERROR when blank but neither nil nor a string" do + instance.timestamp = [] + expect(instance).not_to be_valid + expect(instance.errors[:timestamp]).to eq([described_class::NOT_STRING_ERROR]) + end + + it "adds error NOT_STRING_ERROR when not a string" do + instance.timestamp = 1 + expect(instance).not_to be_valid + expect(instance.errors[:timestamp]).to eq([described_class::NOT_STRING_ERROR]) + end + + it "does not add error when value can be parsed" do + instance.timestamp = "2018-09-20 01:02:00 +10:00" + expect(instance).to be_valid + end + + it "adds error INVALID_FORMAT_ERROR when value cannot be parsed" do + instance.timestamp = "Not Valid" + expect(instance).not_to be_valid + expect(instance.errors[:timestamp]).to eq([described_class::INVALID_FORMAT_ERROR]) + end + end +end