Compare commits

...

3 Commits

Author SHA1 Message Date
Dylan Knutson
86203449ac cache bundle install at devcontainer build step 2025-07-21 04:03:03 +00:00
Dylan Knutson
8854dddb4a counter cache support 2025-07-20 17:56:08 +00:00
Dylan Knutson
198ab946d7 refactor devcontainer for simpler ruby install 2025-07-20 17:53:09 +00:00
24 changed files with 3357 additions and 129 deletions

33
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM mcr.microsoft.com/devcontainers/base:debian-12
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -qqy \
build-essential \
autoconf \
libssl-dev \
libyaml-dev \
zlib1g-dev \
libffi-dev \
libgmp-dev \
rustc \
watchman \
libsqlite3-dev \
pkg-config
USER vscode
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv
ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:$PATH"
RUN echo 'eval "$(rbenv init - --no-rehash bash)"' >> ~/.bashrc
RUN git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
RUN rbenv install 3.4.4 && \
rbenv global 3.4.4
# cache bundle install to make reloading the devcontainer faster
RUN mkdir -p /tmp/bundle-install-cache && \
chown -R vscode:vscode /tmp/bundle-install-cache
WORKDIR /tmp/bundle-install-cache
COPY lib/has_aux_table/version.rb /tmp/bundle-install-cache/lib/has_aux_table/version.rb
COPY Gemfile.lock Gemfile has-aux-table.gemspec /tmp/bundle-install-cache/
RUN BUNDLE_FROZEN=true MAKE="make -j$(nproc)" bundle install --jobs $(nproc)
# convenience aliases
RUN echo 'alias rspec="bundle exec rspec"' >> ~/.bashrc
RUN echo 'alias tapioca="bundle exec tapioca"' >> ~/.bashrc
RUN echo 'alias srb="bundle exec srb"' >> ~/.bashrc

View File

@@ -3,7 +3,10 @@
{
"name": "Ruby",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/ruby:1-3.4-bullseye",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/devcontainers-extra/features/npm-package:1": {
"package": "backlog.md"
@@ -16,16 +19,16 @@
"aliariff.vscode-erb-beautify",
"KoichiSasada.vscode-rdbg",
"qwtel.sqlite-viewer",
"ms-azuretools.vscode-docker"
"ms-azuretools.vscode-docker",
"ryanluker.vscode-coverage-gutters"
]
}
},
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "sudo apt update && sudo apt install --no-install-recommends --no-install-suggests -qy watchman",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@
/pkg/
/spec/reports/
/tmp/
/vendor/
/.ruby-lsp/
# rspec failure tracking
.rspec_status

9
.vscode/launch.json vendored
View File

@@ -6,17 +6,14 @@
"configurations": [
{
"type": "rdbg",
"name": "Debug current file with rdbg",
"name": "rdbg - demo_functionality.rb",
"request": "launch",
"script": "${file}",
"askParameters": true,
"rdbgPath": "~/.rbenv/shims/rdbg"
"script": "demo_functionality.rb",
},
{
"type": "rdbg",
"name": "Attach with rdbg",
"name": "rdbg - attach to rspec",
"request": "attach",
"rdbgPath": "~/.rbenv/shims/rdbg"
}
]
}

11
.vscode/settings.json vendored
View File

@@ -2,11 +2,16 @@
"editor.formatOnSave": true,
"workbench.editor.titleScrollbarSizing": "large",
"rubyLsp.formatter": "syntax_tree",
"rubyLsp.featureFlags": {
"fullTestDiscovery": true
},
"rubyLsp.addonSettings": {
"Ruby LSP RSpec": {
"debug": true
}
},
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp"
},
"rubyLsp.rubyVersionManager": {
"rbenvExecutablePath": "/usr/local/share/rbenv/bin/rbenv",
},
"rdbg.useBundler": false,
}

View File

@@ -12,6 +12,12 @@ group :development do
gem "bundler-audit"
gem "lefthook"
gem "tapioca", "~> 0.17"
gem "ruby-lsp-rspec", require: false
end
group :test do
gem "simplecov", require: false
gem "simplecov-lcov", require: false
end
group :development, :test do

View File

@@ -41,6 +41,7 @@ GEM
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.6.2)
docile (1.4.1)
drb (2.2.3)
erb (5.0.1)
erubi (1.13.1)
@@ -51,6 +52,7 @@ GEM
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
language_server-protocol (3.17.0.5)
lefthook (1.12.2)
logger (1.7.0)
method_source (1.1.0)
@@ -96,7 +98,20 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.4)
ruby-lsp (0.25.0)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 5)
ruby-lsp-rspec (0.1.26)
ruby-lsp (~> 0.25.0)
securerandom (0.4.1)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov-lcov (0.8.0)
simplecov_json_formatter (0.1.4)
sorbet (0.5.12222)
sorbet-static (= 0.5.12222)
sorbet-runtime (0.5.12222)
@@ -148,6 +163,9 @@ DEPENDENCIES
pry
rake (~> 13.0)
rspec (~> 3.0)
ruby-lsp-rspec
simplecov
simplecov-lcov
sorbet-runtime
sorbet-static-and-runtime
sqlite3 (~> 1.4)

View File

@@ -109,8 +109,6 @@ class E621Post < Post
belongs_to :creator, class_name: "E621User", inverse_of: :created_posts
end
puts FaPost.inspect
fa_user = FaUser.create!(username: "Alice", url_name: "alice")
fa_user_id = fa_user.id
raise if fa_user.id.nil?

View File

@@ -35,6 +35,20 @@ module HasAuxTable
include RelationExtensions
sig do
params(column_name: T.any(String, Symbol)).returns(
T.nilable(AuxTableConfig)
)
end
def aux_table_for(column_name)
@aux_table_configs ||=
T.let({}, T.nilable(T::Hash[Symbol, AuxTableConfig]))
@aux_table_configs.values.find do |config|
config.aux.is_column?(column_name)
end
end
# Main DSL method for defining auxiliary tables
sig { params(aux_name: T.any(String, Symbol)).returns(AuxTableConfig) }
def aux_table(aux_name)
@@ -58,6 +72,7 @@ module HasAuxTable
setup_relation_extensions!(config)
setup_attribute_getter_setter_hooks!(config)
setup_enum_hook!(config)
setup_update_counter_hook!(config)
config
end
@@ -236,14 +251,15 @@ module HasAuxTable
read_attribute
_write_attribute
write_attribute
_assign_attribute
].each do |method_name|
method = self.instance_method(method_name)
self.define_method(method_name) do |name, *args, **kwargs, &block|
T.bind(self, ActiveRecord::Base)
if config.aux.column_names.include?(name.to_s)
target = config.aux_model_for(self)
T.unsafe(target).send(method_name, name, *args, **kwargs, &block)
ret =
T.unsafe(target).send(method_name, name, *args, **kwargs, &block)
ret
else
T.unsafe(method).bind(self).call(name, *args, **kwargs, &block)
end
@@ -413,6 +429,31 @@ module HasAuxTable
end
end
end
sig { params(config: AuxTableConfig).void }
def setup_update_counter_hook!(config)
self.define_singleton_method(:update_counters) do |id, counters|
T.bind(self, T.class_of(ActiveRecord::Base))
main_counters = {}
aux_counters = {}
opts = {}
counters.each do |k, v|
is_aux = config.aux.is_column?(k)
is_main = config.main.is_column?(k)
if !is_aux && !is_main
opts[k] = v
elsif is_aux
aux_counters[k] = v
elsif is_main
main_counters[k] = v
end
end
super(id, main_counters.merge(opts)) if main_counters.any?
if aux_counters.any?
config.aux.klass.update_counters(id, aux_counters.merge(opts))
end
end
end
end
mixes_in_class_methods(ClassMethods)
end

View File

@@ -194,22 +194,6 @@ module HasAuxTable
end
end
sig do
params(
relation: T.any(ActiveRecord::Relation, T.class_of(ActiveRecord::Base)),
conditions: T::Hash[String, T.untyped]
).returns(ActiveRecord::Relation)
end
def apply_split_conditions!(relation, conditions)
main_conditions, aux_conditions =
self.aux.partition_by_columns(conditions)
relation = relation.where(main_conditions) if main_conditions.any?
if aux_conditions.any?
relation = relation.where(aux_association_name => aux_conditions)
end
relation
end
sig do
params(conditions: T::Hash[String, T.untyped]).returns(
T::Hash[String, T.untyped]

View File

@@ -1,6 +1,68 @@
# typed: true
# frozen_string_literal: true
require "active_record/associations/association_scope"
class ActiveRecord::Associations::AssociationScope
def get_chain(reflection, association, tracker)
name = reflection.name
chain =
T.let(
[
ActiveRecord::Reflection::RuntimeReflection.new(
reflection,
association
)
],
T.untyped
)
reflection
.chain
.drop(1)
.each do |refl|
refl_klass = T.cast(refl.klass, T.class_of(ActiveRecord::Base))
if refl_klass.is_a?(HasAuxTable::ClassMethods) &&
(aux_config = refl_klass.aux_table_for(refl.foreign_key))
aliased_table = aux_config.aux.klass.arel_table
chain << ReflectionProxy.new(refl, aliased_table)
else
aliased_table =
tracker.aliased_table_for(refl.klass.arel_table) do
refl.alias_candidate(name)
end
chain << ReflectionProxy.new(refl, aliased_table)
end
end
chain
end
def scope(association)
klass = association.klass
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
scope = add_constraints(scope, owner, chain)
scope.limit!(1) unless reflection.collection?
chain.each do |refl|
klass = refl.klass
next unless klass.is_a?(HasAuxTable::ClassMethods)
next unless aux_config = klass.aux_table_for(refl.join_primary_key)
aux_table = aux_config.aux.klass.table_name
main_table = aux_config.main.klass.table_name
fkey = "'#{aux_table}'.'#{aux_config.foreign_key}'"
pkey = "'#{main_table}'.'#{aux_config.primary_key}'"
scope.joins!("INNER JOIN '#{main_table}' ON #{fkey} = #{pkey}")
end if association.is_a?(
ActiveRecord::Associations::HasManyThroughAssociation
)
scope
end
end
module HasAuxTable
module RelationExtensions
extend T::Sig
@@ -73,11 +135,7 @@ module HasAuxTable
:bind_attribute,
true
) do |original, name, value, &block|
if aux_config.aux.is_column?(name)
aux_config.aux_bind_attribute(name, value, &block)
else
original.call(name, value, &block)
end
aux_config.aux_bind_attribute(name, value, &block)
end
end
end

View File

@@ -461,3 +461,8 @@ class ActiveSupport::ErrorReporter
sig { params(error: T.any(Exception, String), severity: T.nilable(Symbol), context: T::Hash[Symbol, T.untyped], source: T.nilable(String)).void }
def unexpected(error, severity: T.unsafe(nil), context: T.unsafe(nil), source: T.unsafe(nil)); end
end
module ActiveSupport::Testing::Assertions
sig { type_parameters(:Block).params(block: T.proc.returns(T.type_parameter(:Block))).returns(T.type_parameter(:Block)) }
def assert_nothing_raised(&block); end
end

377
sorbet/rbi/gems/docile@1.4.1.rbi generated Normal file
View File

@@ -0,0 +1,377 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `docile` gem.
# Please instead update this file by running `bin/tapioca gem docile`.
# Docile keeps your Ruby DSLs tame and well-behaved.
#
# source://docile//lib/docile/version.rb#3
module Docile
extend ::Docile::Execution
private
# Execute a block in the context of an object whose methods represent the
# commands in a DSL.
#
# Use this method to execute an *imperative* DSL, which means that:
#
# 1. Each command mutates the state of the DSL context object
# 2. The return value of each command is ignored
# 3. The final return value is the original context object
#
# @example Use a String as a DSL
# Docile.dsl_eval("Hello, world!") do
# reverse!
# upcase!
# end
# #=> "!DLROW ,OLLEH"
# @example Use an Array as a DSL
# Docile.dsl_eval([]) do
# push 1
# push 2
# pop
# push 3
# end
# #=> [1, 3]
# @note Use with an *imperative* DSL (commands modify the context object)
# @param dsl [Object] context object whose methods make up the DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object
# @return [Object] the `dsl` context object after executing the block
#
# source://docile//lib/docile.rb#45
def dsl_eval(dsl, *args, **_arg2, &block); end
# Execute a block in the context of an immutable object whose methods,
# and the methods of their return values, represent the commands in a DSL.
#
# Use this method to execute a *functional* DSL, which means that:
#
# 1. The original DSL context object is never mutated
# 2. Each command returns the next DSL context object
# 3. The final return value is the value returned by the last command
#
# @example Use a frozen String as a DSL
# Docile.dsl_eval_immutable("I'm immutable!".freeze) do
# reverse
# upcase
# end
# #=> "!ELBATUMMI M'I"
# @example Use a Float as a DSL
# Docile.dsl_eval_immutable(84.5) do
# fdiv(2)
# floor
# end
# #=> 42
# @note Use with a *functional* DSL (commands return successor
# context objects)
# @param dsl [Object] immutable context object whose methods make up the
# initial DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object and successor return values
# @return [Object] the return value of the final command in the block
#
# source://docile//lib/docile.rb#128
def dsl_eval_immutable(dsl, *args, **_arg2, &block); end
# Execute a block in the context of an object whose methods represent the
# commands in a DSL, and return *the block's return value*.
#
# Use this method to execute an *imperative* DSL, which means that:
#
# 1. Each command mutates the state of the DSL context object
# 2. The return value of each command is ignored
# 3. The final return value is the original context object
#
# @example Use a String as a DSL
# Docile.dsl_eval_with_block_return("Hello, world!") do
# reverse!
# upcase!
# first
# end
# #=> "!"
# @example Use an Array as a DSL
# Docile.dsl_eval_with_block_return([]) do
# push "a"
# push "b"
# pop
# push "c"
# length
# end
# #=> 2
# @note Use with an *imperative* DSL (commands modify the context object)
# @param dsl [Object] context object whose methods make up the DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object
# @return [Object] the return value from executing the block
#
# source://docile//lib/docile.rb#87
def dsl_eval_with_block_return(dsl, *args, **_arg2, &block); end
class << self
# Execute a block in the context of an object whose methods represent the
# commands in a DSL.
#
# Use this method to execute an *imperative* DSL, which means that:
#
# 1. Each command mutates the state of the DSL context object
# 2. The return value of each command is ignored
# 3. The final return value is the original context object
#
# @example Use a String as a DSL
# Docile.dsl_eval("Hello, world!") do
# reverse!
# upcase!
# end
# #=> "!DLROW ,OLLEH"
# @example Use an Array as a DSL
# Docile.dsl_eval([]) do
# push 1
# push 2
# pop
# push 3
# end
# #=> [1, 3]
# @note Use with an *imperative* DSL (commands modify the context object)
# @param dsl [Object] context object whose methods make up the DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object
# @return [Object] the `dsl` context object after executing the block
#
# source://docile//lib/docile.rb#51
def dsl_eval(dsl, *args, **_arg2, &block); end
# Execute a block in the context of an immutable object whose methods,
# and the methods of their return values, represent the commands in a DSL.
#
# Use this method to execute a *functional* DSL, which means that:
#
# 1. The original DSL context object is never mutated
# 2. Each command returns the next DSL context object
# 3. The final return value is the value returned by the last command
#
# @example Use a frozen String as a DSL
# Docile.dsl_eval_immutable("I'm immutable!".freeze) do
# reverse
# upcase
# end
# #=> "!ELBATUMMI M'I"
# @example Use a Float as a DSL
# Docile.dsl_eval_immutable(84.5) do
# fdiv(2)
# floor
# end
# #=> 42
# @note Use with a *functional* DSL (commands return successor
# context objects)
# @param dsl [Object] immutable context object whose methods make up the
# initial DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object and successor return values
# @return [Object] the return value of the final command in the block
#
# source://docile//lib/docile.rb#133
def dsl_eval_immutable(dsl, *args, **_arg2, &block); end
# Execute a block in the context of an object whose methods represent the
# commands in a DSL, and return *the block's return value*.
#
# Use this method to execute an *imperative* DSL, which means that:
#
# 1. Each command mutates the state of the DSL context object
# 2. The return value of each command is ignored
# 3. The final return value is the original context object
#
# @example Use a String as a DSL
# Docile.dsl_eval_with_block_return("Hello, world!") do
# reverse!
# upcase!
# first
# end
# #=> "!"
# @example Use an Array as a DSL
# Docile.dsl_eval_with_block_return([]) do
# push "a"
# push "b"
# pop
# push "c"
# length
# end
# #=> 2
# @note Use with an *imperative* DSL (commands modify the context object)
# @param dsl [Object] context object whose methods make up the DSL
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed against the
# `dsl` context object
# @return [Object] the return value from executing the block
#
# source://docile//lib/docile.rb#94
def dsl_eval_with_block_return(dsl, *args, **_arg2, &block); end
end
end
# This is used to remove entries pointing to Docile's source files
# from {Exception#backtrace} and {Exception#backtrace_locations}.
#
# If {NoMethodError} is caught then the exception object will be extended
# by this module to add filter functionalities.
#
# @api private
#
# source://docile//lib/docile/backtrace_filter.rb#11
module Docile::BacktraceFilter
# @api private
#
# source://docile//lib/docile/backtrace_filter.rb#14
def backtrace; end
# @api private
#
# source://docile//lib/docile/backtrace_filter.rb#19
def backtrace_locations; end
end
# @api private
#
# source://docile//lib/docile/backtrace_filter.rb#12
Docile::BacktraceFilter::FILTER_PATTERN = T.let(T.unsafe(nil), Regexp)
# Operates in the same manner as {FallbackContextProxy}, but replacing
# the primary `receiver` object with the result of each proxied method.
#
# This is useful for implementing DSL evaluation for immutable context
# objects.
#
#
# @api private
# @see Docile.dsl_eval_immutable
#
# source://docile//lib/docile/chaining_fallback_context_proxy.rb#17
class Docile::ChainingFallbackContextProxy < ::Docile::FallbackContextProxy
# Proxy methods as in {FallbackContextProxy#method_missing}, replacing
# `receiver` with the returned value.
#
# @api private
#
# source://docile//lib/docile/chaining_fallback_context_proxy.rb#20
def method_missing(method, *args, **_arg2, &block); end
end
# A namespace for functions relating to the execution of a block against a
# proxy object.
#
# @api private
#
# source://docile//lib/docile/execution.rb#8
module Docile::Execution
private
# Execute a block in the context of an object whose methods represent the
# commands in a DSL, using a specific proxy class.
#
# @api private
# @param dsl [Object] context object whose methods make up the
# (initial) DSL
# @param proxy_type [FallbackContextProxy, ChainingFallbackContextProxy] which class to instantiate as proxy context
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed
# @return [Object] the return value of the block
#
# source://docile//lib/docile/execution.rb#19
def exec_in_proxy_context(dsl, proxy_type, *args, **_arg3, &block); end
class << self
# Execute a block in the context of an object whose methods represent the
# commands in a DSL, using a specific proxy class.
#
# @api private
# @param dsl [Object] context object whose methods make up the
# (initial) DSL
# @param proxy_type [FallbackContextProxy, ChainingFallbackContextProxy] which class to instantiate as proxy context
# @param args [Array] arguments to be passed to the block
# @param block [Proc] the block of DSL commands to be executed
# @return [Object] the return value of the block
#
# source://docile//lib/docile/execution.rb#51
def exec_in_proxy_context(dsl, proxy_type, *args, **_arg3, &block); end
end
end
# A proxy object with a primary receiver as well as a secondary
# fallback receiver.
#
# Will attempt to forward all method calls first to the primary receiver,
# and then to the fallback receiver if the primary does not handle that
# method.
#
# This is useful for implementing DSL evaluation in the context of an object.
#
#
# @api private
# @see Docile.dsl_eval
#
# source://docile//lib/docile/fallback_context_proxy.rb#20
class Docile::FallbackContextProxy
# @api private
# @param receiver [Object] the primary proxy target to which all methods
# initially will be forwarded
# @param fallback [Object] the fallback proxy target to which any methods
# not handled by `receiver` will be forwarded
# @return [FallbackContextProxy] a new instance of FallbackContextProxy
#
# source://docile//lib/docile/fallback_context_proxy.rb#46
def initialize(receiver, fallback); end
# @api private
# @return [Array<Symbol>] Instance variable names, excluding
# {NON_PROXIED_INSTANCE_VARIABLES}
#
# source://docile//lib/docile/fallback_context_proxy.rb#85
def instance_variables; end
# Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver`
# and then to `fallback` if not found.
#
# @api private
#
# source://docile//lib/docile/fallback_context_proxy.rb#91
def method_missing(method, *args, **_arg2, &block); end
end
# The set of methods which will **not** fallback from the block's context
# to the dsl object.
#
# @api private
#
# source://docile//lib/docile/fallback_context_proxy.rb#30
Docile::FallbackContextProxy::NON_FALLBACK_METHODS = T.let(T.unsafe(nil), Set)
# The set of instance variables which are local to this object and hidden.
# All other instance variables will be copied in and out of this object
# from the scope in which this proxy was created.
#
# @api private
#
# source://docile//lib/docile/fallback_context_proxy.rb#35
Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES = T.let(T.unsafe(nil), Set)
# The set of methods which will **not** be proxied, but instead answered
# by this object directly.
#
# @api private
#
# source://docile//lib/docile/fallback_context_proxy.rb#23
Docile::FallbackContextProxy::NON_PROXIED_METHODS = T.let(T.unsafe(nil), Set)
# The current version of this library
#
# source://docile//lib/docile/version.rb#5
Docile::VERSION = T.let(T.unsafe(nil), String)

View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `language_server-protocol` gem.
# Please instead update this file by running `bin/tapioca gem language_server-protocol`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

View File

@@ -0,0 +1,15 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `ruby-lsp-rspec` gem.
# Please instead update this file by running `bin/tapioca gem ruby-lsp-rspec`.
# source://ruby-lsp-rspec//lib/ruby_lsp_rspec/version.rb#4
module RubyLsp; end
# source://ruby-lsp-rspec//lib/ruby_lsp_rspec/version.rb#5
module RubyLsp::RSpec; end
# source://ruby-lsp-rspec//lib/ruby_lsp_rspec/version.rb#6
RubyLsp::RSpec::VERSION = T.let(T.unsafe(nil), String)

9
sorbet/rbi/gems/ruby-lsp@0.25.0.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `ruby-lsp` gem.
# Please instead update this file by running `bin/tapioca gem ruby-lsp`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

View File

@@ -0,0 +1,96 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `simplecov-html` gem.
# Please instead update this file by running `bin/tapioca gem simplecov-html`.
# source://simplecov-html//lib/simplecov-html.rb#15
module SimpleCov; end
# source://simplecov-html//lib/simplecov-html.rb#16
module SimpleCov::Formatter; end
# source://simplecov-html//lib/simplecov-html.rb#17
class SimpleCov::Formatter::HTMLFormatter
# @return [HTMLFormatter] a new instance of HTMLFormatter
#
# source://simplecov-html//lib/simplecov-html.rb#26
def initialize; end
# source://simplecov-html//lib/simplecov-html.rb#33
def format(result); end
private
# source://simplecov-html//lib/simplecov-html.rb#93
def asset_inline(name); end
# source://simplecov-html//lib/simplecov-html.rb#79
def asset_output_path; end
# source://simplecov-html//lib/simplecov-html.rb#87
def assets_path(name); end
# @return [Boolean]
#
# source://simplecov-html//lib/simplecov-html.rb#48
def branchable_result?; end
# source://simplecov-html//lib/simplecov-html.rb#124
def coverage_css_class(covered_percent); end
# source://simplecov-html//lib/simplecov-html.rb#120
def covered_percent(percent); end
# Returns a table containing the given source files
#
# source://simplecov-html//lib/simplecov-html.rb#111
def formatted_file_list(title, source_files); end
# Returns the html for the given source_file
#
# source://simplecov-html//lib/simplecov-html.rb#104
def formatted_source_file(source_file); end
# Return a (kind of) unique id for the source file given. Uses SHA1 on path for the id
#
# source://simplecov-html//lib/simplecov-html.rb#145
def id(source_file); end
# @return [Boolean]
#
# source://simplecov-html//lib/simplecov-html.rb#55
def line_status?(source_file, line); end
# source://simplecov-html//lib/simplecov-html.rb#157
def link_to_source_file(source_file); end
# source://simplecov-html//lib/simplecov-html.rb#63
def output_message(result); end
# source://simplecov-html//lib/simplecov-html.rb#75
def output_path; end
# source://simplecov-html//lib/simplecov-html.rb#153
def shortened_filename(source_file); end
# source://simplecov-html//lib/simplecov-html.rb#134
def strength_css_class(covered_strength); end
# Returns the an erb instance for the template of given name
#
# source://simplecov-html//lib/simplecov-html.rb#71
def template(name); end
# source://simplecov-html//lib/simplecov-html.rb#149
def timeago(time); end
end
# Only have a few content types, just hardcode them
#
# source://simplecov-html//lib/simplecov-html.rb#19
SimpleCov::Formatter::HTMLFormatter::CONTENT_TYPES = T.let(T.unsafe(nil), Hash)
# source://simplecov-html//lib/simplecov-html/version.rb#6
SimpleCov::Formatter::HTMLFormatter::VERSION = T.let(T.unsafe(nil), String)

127
sorbet/rbi/gems/simplecov-lcov@0.8.0.rbi generated Normal file
View File

@@ -0,0 +1,127 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `simplecov-lcov` gem.
# Please instead update this file by running `bin/tapioca gem simplecov-lcov`.
# source://simplecov-lcov//lib/simplecov-lcov.rb#7
module SimpleCov; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#8
module SimpleCov::Formatter; end
# Custom Formatter to generate lcov style coverage for simplecov
#
# source://simplecov-lcov//lib/simplecov-lcov.rb#10
class SimpleCov::Formatter::LcovFormatter
# generate lcov style coverage.
# ==== Args
# _result_ :: [SimpleCov::Result] abcoverage result instance.
#
# source://simplecov-lcov//lib/simplecov-lcov.rb#14
def format(result); end
private
# source://simplecov-lcov//lib/simplecov-lcov.rb#62
def create_output_directory!; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#121
def filtered_branches(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#117
def filtered_lines(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#125
def format_branch(branch, branch_idx); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#101
def format_branches(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#84
def format_file(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#130
def format_line(line); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#111
def format_lines(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#50
def lcov_results_path; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#46
def output_directory; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#79
def output_filename(filename); end
# @return [Boolean]
#
# source://simplecov-lcov//lib/simplecov-lcov.rb#54
def report_with_single_file?; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#58
def single_report_path; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#67
def write_lcov!(file); end
# source://simplecov-lcov//lib/simplecov-lcov.rb#73
def write_lcov_to_single_file!(files); end
class << self
# @yield [@config]
#
# source://simplecov-lcov//lib/simplecov-lcov.rb#27
def config; end
# source://simplecov-lcov//lib/simplecov-lcov.rb#33
def report_with_single_file=(value); end
end
end
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#1
module SimpleCovLcov; end
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#2
class SimpleCovLcov::Configuration
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#24
def lcov_file_name; end
# Sets the attribute lcov_file_name
#
# @param value the value to set the attribute lcov_file_name to.
#
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#5
def lcov_file_name=(_arg0); end
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#11
def output_directory; end
# Sets the attribute output_directory
#
# @param value the value to set the attribute output_directory to.
#
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#4
def output_directory=(_arg0); end
# Sets the attribute report_with_single_file
#
# @param value the value to set the attribute report_with_single_file to.
#
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#3
def report_with_single_file=(_arg0); end
# @return [Boolean]
#
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#7
def report_with_single_file?; end
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#20
def single_report_path; end
# source://simplecov-lcov//lib/simple_cov_lcov/configuration.rb#15
def single_report_path=(new_path); end
end

2149
sorbet/rbi/gems/simplecov@0.22.0.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `simplecov_json_formatter` gem.
# Please instead update this file by running `bin/tapioca gem simplecov_json_formatter`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

View File

@@ -0,0 +1,24 @@
# typed: strict
# frozen_string_literal: true
module SimpleCov
sig do
params(profile: T.untyped, block: T.proc.bind(SimpleCov).void).returns(
T.nilable(T.any(Symbol, Integer))
)
end
def self.start(profile = nil, &block)
end
sig { params(filter: T.untyped).void }
def add_filter(filter)
end
sig { params(level: T.nilable(T.any(Symbol, Integer))).void }
def enable_coverage(level = nil)
end
sig { params(formatter: T.untyped).void }
def formatter(formatter)
end
end

View File

@@ -2,8 +2,6 @@
# frozen_string_literal: true
RSpec.describe HasAuxTable do
before(:all) { SpecHelper.initialize_spec_schema! }
# Car class will be defined after schema setup
it "has a version number" do
@@ -23,6 +21,10 @@ RSpec.describe HasAuxTable do
end
describe "column reporting" do
it "reports columns of the base class" do
expect(Vehicle.inspect).to include("name")
end
it "reports the correct columns on the string repr of the class" do
expect(Car.inspect).to include("fuel_type")
end
@@ -108,7 +110,7 @@ RSpec.describe HasAuxTable do
end
it "can set association on aux record" do
driver = Driver.create!(name: "John Doe")
driver = Driver.create!(name: "John Doe", license_number: 12_345)
car = Car.create!(name: "Honda Civic")
driver.car = car
expect(driver.car).to eq(car)
@@ -144,6 +146,20 @@ RSpec.describe HasAuxTable do
expect(plane.engine_type).to eq("turboprop")
end
describe "validations" do
it "validates the main record" do
driver = Driver.create!(name: "John Doe", license_number: 12_345)
expect(driver.valid?).to be_truthy
driver.name = nil
expect(driver.valid?).to be_falsey
end
it "validates through an association" do
car = Car.create!(name: "Honda Civic")
car.drivers.create!(name: "John Doe", license_number: 12_345)
end
end
describe "#changed?" do
it "returns true if the main record changes" do
car = Car.create!(name: "Honda Civic")
@@ -363,10 +379,7 @@ RSpec.describe HasAuxTable do
end
it "works with chained where clauses" do
# Chain where clauses with auxiliary columns
efficient_cars = Car.where(fuel_type: "hybrid").where(engine_size: 1.8)
expect(efficient_cars.length).to eq(1)
expect(efficient_cars.first.name).to eq("Toyota Prius")
end
@@ -379,6 +392,12 @@ RSpec.describe HasAuxTable do
car_names = cars.map(&:name).sort
expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
end
it "works when sql is passed to where" do
cars = Car.where("fuel_type = 'hybrid'")
expect(cars.length).to eq(1)
expect(cars.first.name).to eq("Toyota Prius")
end
end
describe "query performance and optimization" do
@@ -691,7 +710,7 @@ RSpec.describe HasAuxTable do
describe "nested associations" do
it "can create a driver through the association" do
driver = @car.drivers.create!(name: "John Doe")
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect(driver.car).to eq(@car)
expect(driver.car_id).to eq(@car.id)
expect(driver.car.fuel_type).to eq("hybrid")
@@ -710,7 +729,8 @@ RSpec.describe HasAuxTable do
end
it "can create a driver directly" do
driver = Driver.create!(car: @car, name: "John Doe")
driver =
Driver.create!(car: @car, name: "John Doe", license_number: 123_456)
expect(driver.car).to eq(@car)
expect(driver.car_id).to eq(@car.id)
expect(driver.car.fuel_type).to eq("hybrid")
@@ -718,12 +738,12 @@ RSpec.describe HasAuxTable do
end
it "can be accessed through the association" do
driver = @car.drivers.create!(name: "John Doe")
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect(@car.drivers).to eq([driver])
end
it "can be destroyed through the association" do
driver = @car.drivers.create!(name: "John Doe")
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect { driver.destroy }.to change { @car.reload.drivers.count }.by(-1)
end
@@ -738,6 +758,15 @@ RSpec.describe HasAuxTable do
d = drivers.find_by(license_number: 123_456)
expect(d.id).to eq(driver.id)
end
# it "can create STI models through associations" do
# user =
# TwitterUser.create!(last_login_at: Time.now, twitter_handle: "a_user")
# post = user.posts.create!(title: "twitter post")
# expect(post.user).to eq(user)
# expect(post.user_id).to eq(user.id)
# expect(post.user.twitter_handle).to eq("a_user")
# end
end
describe "#reload" do
@@ -770,7 +799,7 @@ RSpec.describe HasAuxTable do
it "reloads associations" do
expect(@car.drivers.length).to eq(0)
Driver.create!(car: @car, name: "Billy Kid")
Driver.create!(car: @car, name: "Billy Kid", license_number: 123_456)
expect(@car.drivers.length).to eq(0)
expect(@car.drivers.count).to eq(1)
@@ -848,4 +877,38 @@ RSpec.describe HasAuxTable do
end
}.not_to raise_error
end
describe "counter cache" do
before do
@reader = Reader.create!(name: "John Doe", reading_speed: 100)
@book =
Book.create!(title: "The Great Gatsby", author: "F. Scott Fitzgerald")
end
it "updates counter caches that are on the aux model" do
@reader.read_books << @book
expect(@reader.read_book_joins_count).to eq(1)
expect(@reader.read_book_joins.count).to eq(1)
end
it "updates counter caches that are on the main table of an aux model" do
end
it "updates counter caches on a non-aux model" do
@reader.read_books << @book
expect(@book.read_book_joins_count).to eq(1)
expect(@book.read_book_joins.count).to eq(1)
end
end
describe "joins model with aux tables" do
it "can create a join record" do
doctor = Person.create!(name: "Dr. John Doe")
patient = Person.create!(name: "Jane Doe")
assoc = doctor.patients
assoc << patient
expect(doctor.patients.count).to eq(1)
expect(patient.doctors.count).to eq(1)
end
end
end

View File

@@ -1,10 +1,31 @@
# typed: strict
# frozen_string_literal: true
require "simplecov"
require "simplecov-lcov"
SimpleCov::Formatter::LcovFormatter.config do |c|
c.report_with_single_file = true
c.lcov_file_name = "lcov.info"
end
SimpleCov.start do
enable_coverage :branch
add_filter "spec"
formatter(
SimpleCov::Formatter::MultiFormatter.new(
[
SimpleCov::Formatter::LcovFormatter, # Add Lcov as an output when generating code coverage report
SimpleCov::Formatter::HTMLFormatter # Add other outputs for the code coverage report
]
)
)
end
require "pry"
require "active_record"
require "active_record/errors"
require "has_aux_table"
require "pry"
# Configure ActiveRecord to use in-memory SQLite database
ActiveRecord::Base.establish_connection(
@@ -12,6 +33,8 @@ ActiveRecord::Base.establish_connection(
database: ":memory:"
)
require_relative "spec_models"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
@@ -19,6 +42,8 @@ RSpec.configure do |config|
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.backtrace_inclusion_patterns = [/\bactiverecord\b/]
config.expect_with :rspec do |c|
c.syntax = :expect
end
@@ -58,82 +83,4 @@ module SpecHelper
query_count
end
sig { void }
def self.initialize_spec_schema!
# Set up the database schema for testing
ActiveRecord::Schema.define do
create_table :vehicle_lots do |t|
t.string :name
t.timestamps
end
create_base_table :vehicles do |t|
t.string :name
t.references :vehicle_lot, foreign_key: { to_table: :vehicle_lots }
t.timestamps
t.create_aux :car do |t|
t.string :fuel_type
t.decimal :engine_size, precision: 3, scale: 1
end
t.create_aux :boat do |t|
t.boolean :only_freshwater
end
t.create_aux :plane do |t|
t.integer :engine_type
end
end
create_base_table :people do |t|
t.string :name
t.timestamps
t.create_aux :driver do |t|
t.integer :license_number
t.references :car,
foreign_key: {
to_table: :vehicles_car_aux,
primary_key: :base_table_id
}
end
t.create_aux :captain do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
t.create_aux :passenger do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
end
create_base_table :utensils do |t|
t.string :name
t.string :material
t.timestamps
t.create_aux :fork do |t|
t.integer :num_tongs
end
t.create_aux :spoon do |t|
t.string :curvature
end
end
require_relative "spec_models"
end
end
end

View File

@@ -1,5 +1,121 @@
# typed: strict
# frozen_string_literal: true
extend T::Sig
# Set up the database schema for testing
ActiveRecord::Schema.define do
create_table :vehicle_lots do |t|
t.string :name
t.timestamps
end
create_base_table :vehicles do |t|
t.string :name
t.references :vehicle_lot, foreign_key: { to_table: :vehicle_lots }
t.timestamps
t.create_aux :car do |t|
t.string :fuel_type
t.decimal :engine_size, precision: 3, scale: 1
end
end
change_base_table :vehicles do |t|
t.create_aux :boat do |t|
t.boolean :only_freshwater
end
t.create_aux :plane do |t|
t.integer :engine_type
end
end
create_base_table :people do |t|
t.string :name
t.integer :friends_count
t.integer :lovers_count
t.timestamps
t.create_aux :driver do |t|
t.integer :license_number, index: true, null: false
t.references :car,
foreign_key: {
to_table: :vehicles_car_aux,
primary_key: :base_table_id
}
end
t.create_aux :captain do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
t.create_aux :passenger do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
end
create_base_table :relationship_joins do |t|
t.create_aux :doctor_patient do |t|
t.integer :num_exams
t.references :doctor, foreign_key: { to_table: :people }
t.references :patient, foreign_key: { to_table: :people }
end
t.create_aux :employer_employee do |t|
t.boolean :signed_nda
t.references :employer, foreign_key: { to_table: :people }
t.references :employee, foreign_key: { to_table: :people }
end
end
create_base_table :utensils do |t|
t.string :name
t.string :material
t.timestamps
t.create_aux :fork do |t|
t.integer :num_tongs
end
t.create_aux :spoon do |t|
t.string :curvature
end
end
create_aux_table :people, :reader do |t|
t.integer :reading_speed
t.integer :read_book_joins_count
end
create_table :books do |t|
t.string :title
t.string :author
t.integer :pages
t.integer :read_book_joins_count
t.timestamps
end
create_table :read_book_joins,
id: false,
primary_key: %i[book_id reader_id] do |t|
t.references :book, foreign_key: { to_table: :books }
t.references :reader,
foreign_key: {
to_table: :people_reader_aux,
primary_key: :base_table_id
}
end
end
class Vehicle < ActiveRecord::Base
include HasAuxTable
@@ -27,13 +143,82 @@ class Plane < Vehicle
end
class Person < ActiveRecord::Base
extend T::Sig
include HasAuxTable
self.table_name = "people"
validates :name, presence: true, uniqueness: true
sig do
params(
associations: [Symbol, Symbol],
table_name: Symbol,
model_class_name: T.any(Symbol, String, T.class_of(ActiveRecord::Base)),
join_class_name: T.any(Symbol, String, T.class_of(ActiveRecord::Base))
).void
end
def self.has_and_belongs_to_many_through(
associations,
table_name:,
model_class_name:,
join_class_name:
)
from_assoc_plural, to_assoc_plural = associations
from_assoc_class_name, to_assoc_class_name =
model_class_name,
model_class_name
from_assoc_singular = from_assoc_plural.to_s.singularize
from_join_assoc_name = :"#{from_assoc_singular}_#{table_name}"
to_assoc_singular = to_assoc_plural.to_s.singularize
to_join_assoc_name = :"#{to_assoc_singular}_#{table_name}"
has_many(
from_join_assoc_name,
primary_key: primary_key,
foreign_key: "#{from_assoc_singular}_id",
inverse_of: from_assoc_singular,
class_name: join_class_name
)
has_many(
to_assoc_plural,
through: from_join_assoc_name,
source: to_assoc_singular,
class_name: to_assoc_class_name
)
has_many(
to_join_assoc_name,
primary_key: primary_key,
foreign_key: "#{to_assoc_singular}_id",
inverse_of: to_assoc_singular,
class_name: join_class_name
)
has_many(
from_assoc_plural,
through: to_join_assoc_name,
source: from_assoc_singular,
class_name: from_assoc_class_name
)
end
has_and_belongs_to_many_through(
%i[doctors patients],
model_class_name: "Person",
join_class_name: "DoctorPatientJoin",
table_name: :doctor_patient_joins
)
has_and_belongs_to_many_through(
%i[employers employees],
model_class_name: "Person",
join_class_name: "EmployerEmployeeJoin",
table_name: :employer_employee_joins
)
end
class Driver < Person
aux_table :driver
belongs_to :car, optional: true
validates :license_number, presence: true, uniqueness: true
end
class Captain < Person
@@ -59,3 +244,71 @@ module Kitchen
aux_table :spoon
end
end
# Non-aux table model that has_and_belongs_to_many w/ counter cache
class Book < ActiveRecord::Base
has_many :read_book_joins, inverse_of: :book
has_many :readers, through: :read_book_joins
end
# Aux table model that has_and_belongs_to_many w/ counter cache
class Reader < Person
aux_table :reader
has_many :read_book_joins, inverse_of: :reader
has_many :read_books, through: :read_book_joins, source: :book
end
# The join table for the has_and_belongs_to_many association between Reader and Book
class ReadBookJoin < ActiveRecord::Base
belongs_to :book, counter_cache: true
belongs_to :reader, counter_cache: true
end
# Join table that has aux records
class RelationshipJoin < ActiveRecord::Base
include HasAuxTable
end
class DoctorPatientJoin < RelationshipJoin
aux_table :doctor_patient
belongs_to :doctor, class_name: "Person"
belongs_to :patient, class_name: "Person"
end
class EmployerEmployeeJoin < RelationshipJoin
aux_table :employer_employee
belongs_to :employer, class_name: "Person"
belongs_to :employee, class_name: "Person"
end
class User < ActiveRecord::Base
include HasAuxTable
has_many :posts, inverse_of: :user, class_name: "Post"
end
# class TwitterUser < User
# aux_table :twitter
# validates :twitter_handle, presence: true
# has_many :posts, inverse_of: :user, class_name: "TwitterPost"
# end
# class RedditUser < User
# aux_table :reddit
# validates :reddit_handle, presence: true
# has_many :posts, inverse_of: :user, class_name: "RedditPost"
# end
# class Post < ActiveRecord::Base
# include HasAuxTable
# belongs_to :user, inverse_of: :posts
# end
# class TwitterPost < Post
# aux_table :twitter
# belongs_to :user, inverse_of: :posts, class_name: "TwitterUser"
# end
# class RedditPost < Post
# aux_table :reddit
# belongs_to :user, inverse_of: :posts, class_name: "RedditUser"
# end