Skip to content

Commit

Permalink
add attrjson, paperclip, fix statemachines bug, bump tapioca
Browse files Browse the repository at this point in the history
  • Loading branch information
stathis-alexander committed Nov 27, 2024
1 parent d5ba230 commit 9a0a956
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ruby "3.3.5"
ruby "3.3.6"

source "https://rubygems.org"
gemspec
Expand Down
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
PATH
remote: .
specs:
boba (0.0.7)
boba (0.0.9)
sorbet-static-and-runtime (~> 0.5)
tapioca (~> 0.16.2)
tapioca (~> 0.16.4)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -189,7 +189,7 @@ GEM
regexp_parser (2.9.2)
reline (0.5.10)
io-console (~> 0.5)
rexml (3.3.6)
rexml (3.3.9)
rubocop (1.65.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
Expand Down Expand Up @@ -224,7 +224,7 @@ GEM
sorbet-static-and-runtime (>= 0.5.10187)
thor (>= 0.19.2)
stringio (3.1.1)
tapioca (0.16.2)
tapioca (0.16.4)
bundler (>= 2.2.25)
netrc (>= 0.11.0)
parallel (>= 1.21.0)
Expand Down Expand Up @@ -261,7 +261,7 @@ DEPENDENCIES
rubocop-sorbet

RUBY VERSION
ruby 3.3.5p100
ruby 3.3.6p108

BUNDLED WITH
2.5.17
7 changes: 7 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Boba History

## 0.0.9

- Added `AttrJson` compiler
- Added `Paperclip` compiler
- Fixed a bug in `StateMachinesExtended` compiler where isntance methods were undefined.
- Bump tapioca dependency to latest (16.4)

## 0.0.8

- `ActiveRecordAssocationsPersisted` generate non-nilable types when there's an unconditional validation on the association, an unconditional validation on the foreign key for the association, or when there's a non-`null` db constraint on the foreign key.
Expand Down
2 changes: 1 addition & 1 deletion boba.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 3.0.0"

spec.add_dependency("sorbet-static-and-runtime", "~> 0.5")
spec.add_dependency("tapioca", "~> 0.16.2")
spec.add_dependency("tapioca", "~> 0.16.4")
end
2 changes: 1 addition & 1 deletion lib/boba/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# frozen_string_literal: true

module Boba
VERSION = "0.0.8"
VERSION = "0.0.9"
end
132 changes: 132 additions & 0 deletions lib/tapioca/dsl/compilers/attr_json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true
# typed: true

return if !defined?(AttrJson::Record)

module Tapioca
module Dsl
module Compilers
# `Tapioca::Dsl::Compilers::AttrJson` decorates RBI files for classes that use the `AttrJson` gem.
# https://github.com/jrochkind/attr_json
#
# For example, with the following ActiveRecord model:
# ~~~rb
# class Product < ActiveRecord::Base
# include AttrJson::Record
#
# attr_json :price_cents, :integer
# end
# ~~~
#
# This compiler will generate the following RBI:
# ~~~rbi
# class Product
# include AttrJsonGeneratedMethods
# extend AttrJson::Record::ClassMethods
#
# module AttrJsonGeneratedMethods
# sig { returns(::Integer) }
# def price_cents; end
#
# sig { params(value: Integer).returns(::Integer) }
# def price_cents=(value); end
# end
# end
# ~~~
class AttrJson < Tapioca::Dsl::Compiler
extend T::Sig

# Class methods module is already defined in the gem rbi, so just reference it here.
ClassMethodsModuleName = "AttrJson::Record::ClassMethods"
InstanceMethodModuleName = "AttrJsonGeneratedMethods"
ConstantType = type_member {{ fixed: T.any(T.class_of(::AttrJson::Record), T.class_of(::AttrJson::Model)) }}

class << self
extend T::Sig

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
all_classes.select { |constant| constant < ::AttrJson::Record || constant < ::AttrJson::Model }
end
end

sig { override.void }
def decorate
rbi_class = root.create_path(constant)
instance_module = RBI::Module.new(InstanceMethodModuleName)

decorate_attributes(instance_module)

rbi_class << instance_module
rbi_class.create_include(InstanceMethodModuleName)
rbi_class.create_extend(ClassMethodsModuleName) if constant < ::AttrJson::Record
end

private

def decorate_attributes(rbi_scope)
T.unsafe(constant).attr_json_registry
.definitions
.sort_by(&:name) # this is annoying, but we need to sort to force consistent ordering or the rbi checks fail
.each do |definition|
_, type, options = definition.original_args
attribute_name = definition.name
type_name = sorbet_type(type, array: !!options[:array], nilable: !!options[:nil])

# Model: attr_json(:other_model_id, :string)
# => other_model_id
# => other_model_id=
rbi_scope.create_method(attribute_name, return_type: type_name)
rbi_scope.create_method(
"#{attribute_name}=",
parameters: [create_param("value", type: type_name)],
return_type: type_name,
)
end
end

def symbol_type(type_name)
return type_name if type_name.is_a?(Symbol)
return type_name.to_sym if type_name.is_a?(String)

type_name.type
end

def sorbet_type(type_name, array: false, nilable: false)
sorbet_type = if type_name.respond_to?(:model)
type_name.model
else
case symbol_type(type_name)
when :string, :immutable_string, :text, :uuid, :binary
"String"
when :boolean
"T::Boolean"
when :integer, :big_integer
"Integer"
when :float
"Float"
when :decimal
"BigDecimal"
when :time, :datetime
"Time"
when :date
"Date"
when :money
"Money"
when :json
"T.untyped"
else
"T.untyped"
end
end

sorbet_type = "::#{sorbet_type}"
sorbet_type = "T::Array[#{sorbet_type}]" if array
sorbet_type = "T.nilable(#{sorbet_type})" if nilable # todo: improve this

sorbet_type
end
end
end
end
end
77 changes: 77 additions & 0 deletions lib/tapioca/dsl/compilers/paperclip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# typed: strict
# frozen_string_literal: true

return unless defined?(Paperclip)

module Tapioca
module Dsl
module Compilers
# `Tapioca::Dsl::Compilers::Paperclip` decorates RBI files for classes that use the `has_attached_file` method
# provided by the `paperclip` gem.
# https://github.com/thoughtbot/paperclip
#
# For example, with the following ActiveRecord model:
# ~~~rb
# class Product < ActiveRecord::Base
# has_attached_file(:marketing_image)
# end
# ~~~
#
# This compiler will generate the following RBI:
# ~~~rbi
# class Product
# include PaperclipGeneratedMethods
#
# module PaperclipGeneratedMethods
# sig { returns(::Paperclip::Attachment) }
# def marketing_image; end
#
# sig { params(value: T.untyped).void }
# def marketing_image=(value); end
# end
# end
# ~~~
class Paperclip < Tapioca::Dsl::Compiler
extend T::Sig
include RBIHelper

InstanceModuleName = "PaperclipGeneratedMethods"
ConstantType = type_member { { fixed: T.class_of(::Paperclip::Glue) } }

class << self
extend T::Sig

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
all_classes.select { |c| c < ::Paperclip::Glue }
end
end

sig { override.void }
def decorate
attachments = ::Paperclip::AttachmentRegistry.names_for(constant)
return if attachments.empty?

root.create_path(constant) do |klass|
instance_module = RBI::Module.new(InstanceModuleName)

attachments.each do |attachment_name|
# Model: has_attached_file(:marketing_image)
# => marketing_image
# => marketing_image=
instance_module.create_method(attachment_name, return_type: "::Paperclip::Attachment")
instance_module.create_method(
"#{attachment_name}=",
parameters: [create_param("value", type: "T.untyped")],
return_type: nil,
)
end

klass << instance_module
klass.create_include(InstanceModuleName)
end
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/tapioca/dsl/compilers/state_machines_extended.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class StateMachinesExtended < ::Tapioca::Dsl::Compilers::StateMachines
def decorate
return if constant.state_machines.empty?

# This is a hack to make sure the instance methods are defined on the constant. Somehow the constant is being
# loaded but the actual `state_machine` call is not being executed, so the instance methods don't exist yet.
# Instantiating an empty class fixes it.
constant.try(:new)

super()

root.create_path(T.unsafe(constant)) do |klass|
Expand Down
29 changes: 29 additions & 0 deletions manual/compiler_attrjson.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## AttrJson

`Tapioca::Dsl::Compilers::AttrJson` decorates RBI files for classes that use the `AttrJson` gem.
https://github.com/jrochkind/attr_json

For example, with the following ActiveRecord model:
~~~rb
class Product < ActiveRecord::Base
include AttrJson::Record

attr_json :price_cents, :integer
end
~~~

This compiler will generate the following RBI:
~~~rbi
class Product
include AttrJsonGeneratedMethods
extend AttrJson::Record::ClassMethods

module AttrJsonGeneratedMethods
sig { returns(::Integer) }
def price_cents; end

sig { params(value: Integer).returns(::Integer) }
def price_cents=(value); end
end
end
~~~
27 changes: 27 additions & 0 deletions manual/compiler_paperclip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Paperclip

`Tapioca::Dsl::Compilers::Paperclip` decorates RBI files for classes that use the `has_attached_file` method
provided by the `paperclip` gem.
https://github.com/thoughtbot/paperclip

For example, with the following ActiveRecord model:
~~~rb
class Product < ActiveRecord::Base
has_attached_file(:marketing_image)
end
~~~

This compiler will generate the following RBI:
~~~rbi
class Product
include PaperclipGeneratedMethods

module PaperclipGeneratedMethods
sig { returns(::Paperclip::Attachment) }
def marketing_image; end

sig { params(value: T.untyped).void }
def marketing_image=(value); end
end
end
~~~
2 changes: 2 additions & 0 deletions manual/compilers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This list is an evergeen list of currently available compilers.
<!-- START_COMPILER_LIST -->
* [ActiveRecordAssociationsPersisted](compiler_activerecordassociationspersisted.md)
* [ActiveRecordColumnsPersisted](compiler_activerecordcolumnspersisted.md)
* [AttrJson](compiler_attrjson.md)
* [MoneyRails](compiler_moneyrails.md)
* [Paperclip](compiler_paperclip.md)
* [StateMachinesExtended](compiler_statemachinesextended.md)
<!-- END_COMPILER_LIST -->

0 comments on commit 9a0a956

Please sign in to comment.