length: four shapes for one option

Free preview 3 min to read

Two earlier courses pointed here

In ActiveRecord: model introduction, the lesson "Validations: a rule before saving" introduced validates as a rule Rails checks before it saves a record. It ended by saying the options, the ways a check can fail, and how to report failures to a user would get a course of their own. In ActiveRecord: creating records, the lesson "When save returns false" went one step further. It showed .save returning false when a validation fails and article.errors.full_messages explaining why, and it pointed ahead to the same course. That course also covered .save! and .create! raising ActiveRecord::RecordInvalid, so this one doesn't repeat it.

This is that course. It starts with an option two models in this project already use: length.

length already has two shapes in this codebase

# from Identity::User, this project's real model
validates :username, presence: true,
                     uniqueness: { case_sensitive: false, message: "is not available" },
                     length: { minimum: 3, maximum: 30 },
                     format: { with: /\A[a-zA-Z0-9_]+\z/ }

length: { minimum: 3, maximum: 30 } rejects a username shorter than 3 characters or longer than 30. That's two separate limits on the same attribute. Catalog::Course and Catalog::Lesson use the simpler form, with one limit:

# from Catalog::Course, this project's real model
validates :title_en, presence: true, length: { maximum: 255 }

This one has an upper limit and no lower one. Both lines use the same length: option with different keys.

Two more shapes, tried on a scratch table

Neither in: nor is: appears anywhere in this codebase today. in: takes a range that sets both limits at once, and is: demands an exact length. Here they are on a throwaway ScratchArticle model built for this lesson:

class ScratchArticle < ApplicationRecord
  validates :title, length: { in: 3..20 }
  validates :code, length: { is: 4 }
end

a = ScratchArticle.new(title: "ab")
a.valid?
a.errors[:title]
# => ["is too short (minimum is 3 characters)"]

b = ScratchArticle.new(title: "a" * 25, code: "abc")
b.valid?
b.errors[:title]
# => ["is too long (maximum is 20 characters)"]
b.errors[:code]
# => ["is the wrong length (should be 4 characters)"]

in: 3..20 is minimum: 3, maximum: 20 written as one range, with the same failures and the same messages. is: 4 is stricter than either. It isn't a range at all: the value has one allowed length.

Four keys, one option

minimum, maximum, in and is all belong to the one length: option; they aren't four different validations. A model picks the shape that matches its rule: an upper limit alone, like the title on Catalog::Course; both limits, like the username on Identity::User; a range written as one value; or an exact count, for something like a fixed-width code.

Rails reads whichever keys are present. The message names the limit that failed ("too short", "too long", "the wrong length") instead of a generic "invalid".

Key takeaways

  • ActiveRecord: model introduction introduced validates, and ActiveRecord: creating records showed .save returning false with .errors.full_messages explaining why. This course picks up from both.
  • length: { minimum:, maximum: } on Identity::User and length: { maximum: } on Catalog::Course are the same option with different keys.
  • in: (a range) and is: (an exact length) don't appear in this codebase. On a scratch table, in: 3..20 behaves like minimum: 3, maximum: 20, and is: 4 accepts only that one length.
  • Each shape has its own error message ("too short", "too long", "the wrong length"), which you read from .errors[:attribute].
Lesson 1 of 7 · free

Keep going with lesson 2

uniqueness: scoped, and case-insensitive on purpose

Scoping uniqueness to a group with scope:, ignoring capitalization with case_sensitive: false, and replacing the default message.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...