Skip to content

Commit

Permalink
fix money rails to read column type option, add a bunch of specs
Browse files Browse the repository at this point in the history
  • Loading branch information
stathis-alexander committed Dec 3, 2024
1 parent eaf44f1 commit 99331a8
Show file tree
Hide file tree
Showing 14 changed files with 741 additions and 108 deletions.
14 changes: 3 additions & 11 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,9 @@ AllCops:
Include:
- "sorbet/rbi/shims/**/*.rbi"

RSpec/BeforeAfterAll:
Enabled: false

RSpec/ExampleLength:
Enabled: false

RSpec/LeadingSubject:
Enabled: false

RSpec/NoExpectationExample:
Enabled: false
RSpec:
Exclude:
- 'spec/**/*'

Sorbet:
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ group :development do
gem "rubocop-shopify"
gem "rubocop-sorbet"
gem "state_machines"
gem "sqlite3"
end
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ GEM
prism (>= 0.28.0)
sorbet-static-and-runtime (>= 0.5.10187)
thor (>= 0.19.2)
sqlite3 (2.3.1-arm64-darwin)
sqlite3 (2.3.1-x86_64-darwin)
sqlite3 (2.3.1-x86_64-linux-gnu)
state_machines (0.6.0)
stringio (3.1.1)
tapioca (0.16.5)
Expand Down Expand Up @@ -295,6 +298,7 @@ DEPENDENCIES
rubocop-rspec
rubocop-shopify
rubocop-sorbet
sqlite3
state_machines

RUBY VERSION
Expand Down
45 changes: 35 additions & 10 deletions lib/boba/active_record/attribute_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ module AttributeService
class << self
extend T::Sig

sig { params(constant: T.class_of(::ActiveRecord::Base), attribute: String).returns(T::Boolean) }
def has_unconditional_presence_validator?(constant, attribute)
return false unless constant.respond_to?(:validators_on)
sig do
params(
constant: T.class_of(::ActiveRecord::Base),
attribute: String,
column_name: String,
).returns(T::Boolean)
end
def nilable_attribute?(constant, attribute, column_name: attribute)
return false if has_non_null_database_constraint?(constant, column_name)

constant.validators_on(attribute).any? do |validator|
next false unless validator.is_a?(::ActiveRecord::Validations::PresenceValidator)
!has_unconditional_presence_validator?(constant, attribute)
end

!validator.options.key?(:if) && !validator.options.key?(:unless) && !validator.options.key?(:on)
end
sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
def virtual_attribute?(constant, column_name)
constant.columns_hash[column_name].nil?
end

sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
Expand All @@ -28,9 +35,27 @@ def has_non_null_database_constraint?(constant, column_name)
false
end

sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
def virtual_attribute?(constant, column_name)
constant.columns_hash[column_name].nil?
sig { params(constant: T.class_of(::ActiveRecord::Base), attribute: String).returns(T::Boolean) }
def has_unconditional_presence_validator?(constant, attribute)
return false unless constant.respond_to?(:validators_on)

constant.validators_on(attribute).any? do |validator|
unconditional_presence_validator?(validator)
end
end

private

sig { params(validator: ActiveModel::Validator).returns(T::Boolean) }
def unconditional_presence_validator?(validator)
return false unless validator.is_a?(::ActiveRecord::Validations::PresenceValidator)

unconditional_validator?(validator)
end

sig { params(validator: ActiveModel::Validator).returns(T::Boolean) }
def unconditional_validator?(validator)
!validator.options.key?(:if) && !validator.options.key?(:unless) && !validator.options.key?(:on)
end
end
end
Expand Down
44 changes: 18 additions & 26 deletions lib/boba/active_record/reflection_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ class << self
T.any(::ActiveRecord::Reflection::ThroughReflection, ::ActiveRecord::Reflection::AssociationReflection)
end

sig { params(reflection: ReflectionType).returns(T::Boolean) }
def required_reflection?(reflection)
return true if has_one_and_required_reflection?(reflection)

belongs_to_and_non_optional_reflection?(reflection)
end

private

sig { params(reflection: ReflectionType).returns(T::Boolean) }
def has_one_and_required_reflection?(reflection)
return false unless reflection.has_one?
return true if !!reflection.options[:required]
return true if reflection_required_by_database_constraint?(reflection)

reflection_required_by_validation?(reflection)
Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
reflection.active_record,
reflection.name.to_s,
)
end

sig { params(reflection: ReflectionType).returns(T::Boolean) }
Expand All @@ -32,36 +43,17 @@ def belongs_to_and_non_optional_reflection?(reflection)
reflection.options[:optional]
end
return !optional unless optional.nil?
return true if reflection_required_by_database_constraint?(reflection)
return true if reflection_required_by_validation?(reflection)

# nothing defined, so fall back to the default active record config
!!reflection.active_record.belongs_to_required_by_default
end

private

# check for non-nullable database constraint on the foreign key
sig { params(reflection: ReflectionType).returns(T::Boolean) }
def reflection_required_by_database_constraint?(reflection)
Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
return true if Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
reflection.active_record,
reflection.foreign_key,
reflection.foreign_key.to_s,
)
end

# check for presence validator on the foreign key or on the association
sig { params(reflection: ReflectionType).returns(T::Boolean) }
def reflection_required_by_validation?(reflection)
return true if Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
reflection.active_record,
reflection.foreign_key,
reflection.name.to_s,
)

Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
reflection.active_record,
reflection.name,
)
# nothing defined, so fall back to the default active record config
!!reflection.active_record.belongs_to_required_by_default
end
end
end
Expand Down
50 changes: 50 additions & 0 deletions lib/boba/options/association_type_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# typed: strict
# frozen_string_literal: true

module Boba
module Options
class AssociationTypeOption < T::Enum
extend T::Sig

enums do
Nilable = new("nilable")
Persisted = new("persisted")
end

class << self
extend T::Sig

sig do
params(
options: T::Hash[String, T.untyped],
block: T.proc.params(value: String, default_association_type_option: AssociationTypeOption).void,
).returns(AssociationTypeOption)
end
def from_options(options, &block)
association_type_option = Nilable
value = options["ActiveRecordAssociationTypes"]

if value
if has_serialized?(value)
association_type_option = from_serialized(value)
else
block.call(value, association_type_option)
end
end

association_type_option
end
end

sig { returns(T::Boolean) }
def persisted?
self == AssociationTypeOption::Persisted
end

sig { returns(T::Boolean) }
def nilable?
self == AssociationTypeOption::Nilable
end
end
end
end
55 changes: 5 additions & 50 deletions lib/tapioca/dsl/compilers/active_record_associations_persisted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
return unless defined?(Tapioca::Dsl::Compilers::ActiveRecordAssociations)

require "boba/active_record/reflection_service"
require "boba/options/association_type_option"

module Tapioca
module Dsl
Expand Down Expand Up @@ -53,62 +54,18 @@ module Compilers
class ActiveRecordAssociationsPersisted < ::Tapioca::Dsl::Compilers::ActiveRecordAssociations
extend T::Sig

class AssociationTypeOption < T::Enum
extend T::Sig

enums do
Nilable = new("nilable")
Persisted = new("persisted")
end

class << self
extend T::Sig

sig do
params(
options: T::Hash[String, T.untyped],
block: T.proc.params(value: String, default_association_type_option: AssociationTypeOption).void,
).returns(AssociationTypeOption)
end
def from_options(options, &block)
association_type_option = Nilable
value = options["ActiveRecordAssociationTypes"]

if value
if has_serialized?(value)
association_type_option = from_serialized(value)
else
block.call(value, association_type_option)
end
end

association_type_option
end
end

sig { returns(T::Boolean) }
def persisted?
self == AssociationTypeOption::Persisted
end

sig { returns(T::Boolean) }
def nilable?
self == AssociationTypeOption::Nilable
end
end

private

sig { returns(AssociationTypeOption) }
sig { returns(Boba::Options::AssociationTypeOption) }
def association_type_option
@association_type_option ||= T.let(
AssociationTypeOption.from_options(options) do |value, default_association_type_option|
Boba::Options::AssociationTypeOption.from_options(options) do |value, default_association_type_option|
add_error(<<~MSG.strip)
Unknown value for compiler option `ActiveRecordAssociationTypes` given: `#{value}`.
Proceeding with the default value: `#{default_association_type_option.serialize}`.
MSG
end,
T.nilable(AssociationTypeOption),
T.nilable(Boba::Options::AssociationTypeOption),
)
end

Expand Down Expand Up @@ -190,9 +147,7 @@ def single_association_type_for(reflection)
association_class = type_for(reflection)
return as_nilable_type(association_class) unless association_type_option.persisted?

if Boba::ActiveRecord::ReflectionService.has_one_and_required_reflection?(reflection)
association_class
elsif Boba::ActiveRecord::ReflectionService.belongs_to_and_non_optional_reflection?(reflection)
if Boba::ActiveRecord::ReflectionService.required_reflection?(reflection)
association_class
else
as_nilable_type(association_class)
Expand Down
9 changes: 1 addition & 8 deletions lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,7 @@ def type_for(attribute_name, column_name = attribute_name)
def column_type_for(column_name)
return ["T.untyped", "T.untyped"] if column_type_option.untyped?

nilable_column = !Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
@constant,
column_name,
)
nilable_column &&= !Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
@constant,
column_name,
)
nilable_column = Boba::ActiveRecord::AttributeService.nilable_attribute?(@constant, column_name)

column_type = @constant.attribute_types[column_name]
getter_type = column_type_helper.send(
Expand Down
36 changes: 33 additions & 3 deletions lib/tapioca/dsl/compilers/money_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
return unless defined?(MoneyRails)

require "tapioca/helpers/rbi_helper"
require "tapioca/dsl/helpers/active_record_column_type_helper"
require "boba/active_record/attribute_service"

module Tapioca
module Dsl
Expand Down Expand Up @@ -55,6 +57,21 @@ def gather_constants
end
end

ColumnTypeOption = Tapioca::Dsl::Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption

sig { returns(ColumnTypeOption) }
def column_type_option
@column_type_option ||= T.let(
ColumnTypeOption.from_options(options) do |value, default_column_type_option|
add_error(<<~MSG.strip)
Unknown value for compiler option `ActiveRecordColumnTypes` given: `#{value}`.
Proceeding with the default value: `#{default_column_type_option.serialize}`.
MSG
end,
T.nilable(ColumnTypeOption),
)
end

sig { override.void }
def decorate
return if constant.monetized_attributes.empty?
Expand All @@ -64,10 +81,23 @@ def decorate
instance_module = RBI::Module.new(instance_module_name)

constant.monetized_attributes.each do |attribute_name, column_name|
column = T.unsafe(constant).columns_hash[column_name]
if column_type_option.untyped?
type_name = "T.untyped"
else
type_name = "::Money"

nilable_attribute = if constant < ::ActiveRecord::Base && column_type_option.persisted?
Boba::ActiveRecord::AttributeService.nilable_attribute?(
T.cast(constant, T.class_of(::ActiveRecord::Base)),
attribute_name,
column_name: column_name,
)
else
true
end

type_name = "::Money"
type_name = as_nilable_type(type_name) if column.nil? || !!column.null
type_name = as_nilable_type(type_name) if nilable_attribute
end

# Model: monetize :amount_cents
# => amount
Expand Down
Loading

0 comments on commit 99331a8

Please sign in to comment.