Skip to content

Commit

Permalink
Implement ActiveModel::Validations::Confirm compiler
Browse files Browse the repository at this point in the history
This validation generates a virtual attribute that allows for checking
confirmation logic over the given attribute name
  • Loading branch information
Tonkpils committed Sep 7, 2023
1 parent 2f20cd3 commit 592b369
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 45 deletions.
97 changes: 97 additions & 0 deletions lib/tapioca/dsl/compilers/active_model_validations_confirmation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# typed: strict
# frozen_string_literal: true

begin
require "active_model"
rescue LoadError
return
end

module Tapioca
module Dsl
module Compilers
# `Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation` decorates RBI files for all
# classes that use [`ActiveModel::Validates::Confirmation`](https://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html#method-i-validates_confirmation_of).
#
# For example, with the following class:
#
# ~~~rb
# class User
# include ActiveModel::Validations
#
# validates_confirmation_of :password
#
# validates :email, confirmation: true
# end
# ~~~
#
# this compiler will produce an RBI file with the following content:
# ~~~rbi
# # typed: true
#
# class User
#
# sig { returns(T.untyped) }
# def email_confirmation; end
#
# sig { params(email_confirmation=: T.untyped).returns(T.untyped) }
# def email_confirmation=(email_confirmation); end
#
# sig { returns(T.untyped) }
# def password_confirmation; end
#
# sig { params(password_confirmation=: T.untyped).returns(T.untyped) }
# def password_confirmation=(password_confirmation); end
# end
# ~~~
class ActiveModelValidationsConfirmation < Compiler
extend T::Sig

ConstantType = type_member do
{
fixed: T.all(
T::Class[ActiveModel::Validations],
ActiveModel::Validations::HelperMethods,
ActiveModel::Validations::ClassMethods,
),
}
end

class << self
sig { override.returns(T::Enumerable[Module]) }
def gather_constants
# Collect all the classes that include ActiveModel::Validations
all_classes.select do |c|
c < ActiveModel::Validations &&
c.name.to_s != "ActiveRecord::Base"
end
end
end

sig { override.void }
def decorate
confirmation_validators = constant.validators.select do |v|
v.is_a?(ActiveModel::Validations::ConfirmationValidator)
end

return if confirmation_validators.empty?

# Create a RBI definition for each class that includes Active::Model::Validations
root.create_path(constant) do |klass|
# Create RBI definitions for all the attributes that use confirmation validation
confirmation_validators.each do |validator|
validator.attributes.each do |attr_name|
klass.create_method("#{attr_name}_confirmation", return_type: "T.untyped")
klass.create_method(
"#{attr_name}_confirmation=",
parameters: [create_param("#{attr_name}_confirmation", type: "T.untyped")],
return_type: "T.untyped",
)
end
end
end
end
end
end
end
end
36 changes: 36 additions & 0 deletions manual/compiler_activemodelvalidationsconfirmation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## ActiveModelValidationsConfirmation

`Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation` decorates RBI files for all
classes that use [`ActiveModel::Validates::Confirmation`](https://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html#method-i-validates_confirmation_of).

For example, with the following class:

~~~rb
class User
include ActiveModel::Validations

validates_confirmation_of :password

validates :email, confirmation: true
end
~~~

this compiler will produce an RBI file with the following content:
~~~rbi
# typed: true

class User

sig { returns(T.untyped) }
def email_confirmation; end

sig { params(email_confirmation=: T.untyped).returns(T.untyped) }
def email_confirmation=(email_confirmation); end

sig { returns(T.untyped) }
def password_confirmation; end

sig { params(password_confirmation=: T.untyped).returns(T.untyped) }
def password_confirmation=(password_confirmation); end
end
~~~
93 changes: 48 additions & 45 deletions spec/tapioca/cli/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2454,21 +2454,22 @@ class PostCompiler < Tapioca::Dsl::Compiler
Loaded DSL compiler classes:
PostCompiler enabled
Tapioca::Dsl::Compilers::ActiveModelAttributes enabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken enabled
Tapioca::Dsl::Compilers::ActiveSupportConcern enabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes enabled
Tapioca::Dsl::Compilers::MixedInClassAttributes enabled
Tapioca::Dsl::Compilers::SidekiqWorker enabled
Tapioca::Dsl::Compilers::SmartProperties enabled
PostCompiler enabled
Tapioca::Dsl::Compilers::ActiveModelAttributes enabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken enabled
Tapioca::Dsl::Compilers::ActiveSupportConcern enabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes enabled
Tapioca::Dsl::Compilers::MixedInClassAttributes enabled
Tapioca::Dsl::Compilers::SidekiqWorker enabled
Tapioca::Dsl::Compilers::SmartProperties enabled
OUT

assert_empty_stderr(result)
Expand All @@ -2485,21 +2486,22 @@ class PostCompiler < Tapioca::Dsl::Compiler
Loaded DSL compiler classes:
PostCompiler enabled
Tapioca::Dsl::Compilers::ActiveModelAttributes enabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum disabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken enabled
Tapioca::Dsl::Compilers::ActiveSupportConcern enabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes enabled
Tapioca::Dsl::Compilers::MixedInClassAttributes enabled
Tapioca::Dsl::Compilers::SidekiqWorker enabled
Tapioca::Dsl::Compilers::SmartProperties disabled
PostCompiler enabled
Tapioca::Dsl::Compilers::ActiveModelAttributes enabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum disabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken enabled
Tapioca::Dsl::Compilers::ActiveSupportConcern enabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes enabled
Tapioca::Dsl::Compilers::MixedInClassAttributes enabled
Tapioca::Dsl::Compilers::SidekiqWorker enabled
Tapioca::Dsl::Compilers::SmartProperties disabled
OUT

assert_empty_stderr(result)
Expand All @@ -2516,21 +2518,22 @@ class PostCompiler < Tapioca::Dsl::Compiler
Loaded DSL compiler classes:
PostCompiler disabled
Tapioca::Dsl::Compilers::ActiveModelAttributes disabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword disabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations disabled
Tapioca::Dsl::Compilers::ActiveRecordColumns disabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes disabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations disabled
Tapioca::Dsl::Compilers::ActiveRecordScope disabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken disabled
Tapioca::Dsl::Compilers::ActiveSupportConcern disabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes disabled
Tapioca::Dsl::Compilers::MixedInClassAttributes disabled
Tapioca::Dsl::Compilers::SidekiqWorker disabled
Tapioca::Dsl::Compilers::SmartProperties enabled
PostCompiler disabled
Tapioca::Dsl::Compilers::ActiveModelAttributes disabled
Tapioca::Dsl::Compilers::ActiveModelSecurePassword disabled
Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmation disabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations disabled
Tapioca::Dsl::Compilers::ActiveRecordColumns disabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes disabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations disabled
Tapioca::Dsl::Compilers::ActiveRecordScope disabled
Tapioca::Dsl::Compilers::ActiveRecordSecureToken disabled
Tapioca::Dsl::Compilers::ActiveSupportConcern disabled
Tapioca::Dsl::Compilers::ActiveSupportCurrentAttributes disabled
Tapioca::Dsl::Compilers::MixedInClassAttributes disabled
Tapioca::Dsl::Compilers::SidekiqWorker disabled
Tapioca::Dsl::Compilers::SmartProperties enabled
OUT

assert_empty_stderr(result)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"

module Tapioca
module Dsl
module Compilers
class ActiveModelValidationsConfirmationSpec < ::DslSpec
describe "Tapioca::Dsl::Compilers::ActiveModelValidationsConfirmationSpec" do
describe "initialize" do
it "gathers no constants if there are no classes using ActiveModel::Validations" do
assert_empty(gathered_constants)
end

it "gathers only classes including ActiveModel::Attributes" do
add_ruby_file("shop.rb", <<~RUBY)
require "active_record"
class Shop
end
class ShopWithValidations
include ActiveModel::Validations
end
class ShopWithActiveRecord < ActiveRecord::Base
end
RUBY
assert_equal(["ShopWithActiveRecord", "ShopWithValidations"], gathered_constants)
end
end

describe "decorate" do
it "does not generate a file when there's no confirm validations" do
add_ruby_file("shop.rb", <<~RUBY)
class Shop
include ActiveModel::Validations
validates :name, presence: true
end
RUBY

expected = <<~RBI
# typed: strong
RBI

assert_equal(expected, rbi_for(:Shop))
end

it "generates a method for each confirm validation" do
add_ruby_file("shop.rb", <<~RUBY)
class Shop
include ActiveModel::Validations
validates :name, confirmation: true
validates_confirmation_of :password
end
RUBY

expected = <<~RBI
# typed: strong
class Shop
sig { returns(T.untyped) }
def name_confirmation; end
sig { params(name_confirmation: T.untyped).returns(T.untyped) }
def name_confirmation=(name_confirmation); end
sig { returns(T.untyped) }
def password_confirmation; end
sig { params(password_confirmation: T.untyped).returns(T.untyped) }
def password_confirmation=(password_confirmation); end
end
RBI

assert_equal(expected, rbi_for(:Shop))
end
end
end
end
end
end
end

0 comments on commit 592b369

Please sign in to comment.