From 33a3fb96a32cf10f7107a627e3e094d5d8c931cb Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Thu, 17 Apr 2025 11:03:38 -0500 Subject: [PATCH 01/12] start integrating Option into ActiveRecord --- Gemfile | 17 ++- Gemfile.lock | 35 +++++ gemset.nix | 136 ++++++++++++++++++ lib/errgonomic.rb | 3 + lib/errgonomic/rails.rb | 40 ++++++ .../rails/active_record_delegate_optional.rb | 22 +++ .../rails/active_record_optional.rb | 92 ++++++++++++ test/rails_test.rb | 79 ++++++++++ 8 files changed, 418 insertions(+), 6 deletions(-) create mode 100644 lib/errgonomic/rails.rb create mode 100644 lib/errgonomic/rails/active_record_delegate_optional.rb create mode 100644 lib/errgonomic/rails/active_record_optional.rb create mode 100644 test/rails_test.rb diff --git a/Gemfile b/Gemfile index 965df16..e66b026 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,15 @@ source 'https://rubygems.org' # Specify your gem's dependencies in errgonomic.gemspec gemspec -gem 'rake', '~> 13.0', group: :development -gem 'rspec', '~> 3.0', group: :development -gem 'rubocop', group: :development -gem 'rubocop-yard', group: :development -gem 'solargraph', group: :development +group :development do + gem 'rake', '~> 13.0' + gem 'rspec', '~> 3.0' + gem 'rubocop' + gem 'rubocop-yard' + gem 'solargraph' + gem 'activerecord' + gem 'sqlite3' + gem 'minitest' +end -# gem "standard", "~> 1.3", group: :development +# gem "standard", "~> 1.3" diff --git a/Gemfile.lock b/Gemfile.lock index ca280a2..2ce428d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,11 +7,36 @@ PATH GEM remote: https://rubygems.org/ specs: + activemodel (8.0.2) + activesupport (= 8.0.2) + activerecord (8.0.2) + activemodel (= 8.0.2) + activesupport (= 8.0.2) + timeout (>= 0.4.0) + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) ast (2.4.2) backport (1.2.0) + base64 (0.2.0) benchmark (0.4.0) + bigdecimal (3.1.9) concurrent-ruby (1.3.5) + connection_pool (2.5.1) diff-lcs (1.6.0) + drb (2.2.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) jaro_winkler (1.6.0) json (2.10.1) kramdown (2.5.1) @@ -69,6 +94,7 @@ GEM rubocop (~> 1.21) yard ruby-progressbar (1.13.0) + securerandom (0.4.1) solargraph (0.52.0) backport (~> 1.2) benchmark @@ -88,11 +114,17 @@ GEM tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) yard-solargraph (~> 0.1) + sqlite3 (2.6.0) + mini_portile2 (~> 2.8.0) thor (1.3.2) tilt (2.6.0) + timeout (0.4.3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + uri (1.0.3) yard (0.9.37) yard-doctest (0.1.17) minitest @@ -104,12 +136,15 @@ PLATFORMS ruby DEPENDENCIES + activerecord errgonomic! + minitest rake (~> 13.0) rspec (~> 3.0) rubocop rubocop-yard solargraph + sqlite3 yard (~> 0.9) yard-doctest (~> 0.1) diff --git a/gemset.nix b/gemset.nix index 22b520a..fdb1e5f 100644 --- a/gemset.nix +++ b/gemset.nix @@ -1,4 +1,37 @@ { + activemodel = { + dependencies = ["activesupport"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0v35y2jzqlfy1wnrzlzj2cxylhnz09vykaa1l2dnkq7sl5zzpq8a"; + type = "gem"; + }; + version = "8.0.2"; + }; + activerecord = { + dependencies = ["activemodel" "activesupport" "timeout"]; + groups = ["development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "02nrya34qviawdkssyahb3mg08kqdc461b320a6ikr245jwp0d3r"; + type = "gem"; + }; + version = "8.0.2"; + }; + activesupport = { + dependencies = ["base64" "benchmark" "bigdecimal" "concurrent-ruby" "connection_pool" "drb" "i18n" "logger" "minitest" "securerandom" "tzinfo" "uri"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0pm40y64wfc50a9sj87kxvil2102rmpdcbv82zf0r40vlgdwsrc5"; + type = "gem"; + }; + version = "8.0.2"; + }; ast = { groups = ["default"]; platforms = []; @@ -19,6 +52,16 @@ }; version = "1.2.0"; }; + base64 = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "01qml0yilb9basf7is2614skjp8384h2pycfx86cr8023arfj98g"; + type = "gem"; + }; + version = "0.2.0"; + }; benchmark = { groups = ["default"]; platforms = []; @@ -29,6 +72,16 @@ }; version = "0.4.0"; }; + bigdecimal = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1k6qzammv9r6b2cw3siasaik18i6wjc5m0gw5nfdc6jj64h79z1g"; + type = "gem"; + }; + version = "3.1.9"; + }; concurrent-ruby = { groups = ["default"]; platforms = []; @@ -39,6 +92,16 @@ }; version = "1.3.5"; }; + connection_pool = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "171yzazg2wccbc545hwsx85sba91k7k1imir3c88385mlj82m05f"; + type = "gem"; + }; + version = "2.5.1"; + }; diff-lcs = { groups = ["default"]; platforms = []; @@ -49,6 +112,16 @@ }; version = "1.6.0"; }; + drb = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0h5kbj9hvg5hb3c7l425zpds0vb42phvln2knab8nmazg2zp5m79"; + type = "gem"; + }; + version = "2.2.1"; + }; errgonomic = { dependencies = ["concurrent-ruby"]; groups = ["default"]; @@ -59,6 +132,17 @@ }; version = "0.2.0"; }; + i18n = { + dependencies = ["concurrent-ruby"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "03sx3ahz1v5kbqjwxj48msw3maplpp2iyzs22l4jrzrqh4zmgfnf"; + type = "gem"; + }; + version = "1.14.7"; + }; jaro_winkler = { groups = ["default"]; platforms = []; @@ -362,6 +446,16 @@ }; version = "1.13.0"; }; + securerandom = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1cd0iriqfsf1z91qg271sm88xjnfd92b832z49p1nd542ka96lfc"; + type = "gem"; + }; + version = "0.4.1"; + }; solargraph = { dependencies = ["backport" "benchmark" "diff-lcs" "jaro_winkler" "kramdown" "kramdown-parser-gfm" "logger" "observer" "ostruct" "parser" "rbs" "reverse_markdown" "rubocop" "thor" "tilt" "yard" "yard-solargraph"]; groups = ["default"]; @@ -373,6 +467,17 @@ }; version = "0.52.0"; }; + sqlite3 = { + dependencies = ["mini_portile2"]; + groups = ["development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0573vgz5ck0hqr8h132ln0hczx53m21h4w42n1p75rj837qjbim1"; + type = "gem"; + }; + version = "2.6.0"; + }; thor = { groups = ["default"]; platforms = []; @@ -393,6 +498,27 @@ }; version = "2.6.0"; }; + timeout = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "03p31w5ghqfsbz5mcjzvwgkw3h9lbvbknqvrdliy8pxmn9wz02cm"; + type = "gem"; + }; + version = "0.4.3"; + }; + tzinfo = { + dependencies = ["concurrent-ruby"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "16w2g84dzaf3z13gxyzlzbf748kylk5bdgg3n1ipvkvvqy685bwd"; + type = "gem"; + }; + version = "2.0.6"; + }; unicode-display_width = { dependencies = ["unicode-emoji"]; groups = ["default"]; @@ -414,6 +540,16 @@ }; version = "4.0.4"; }; + uri = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "04bhfvc25b07jaiaf62yrach7khhr5jlr5bx6nygg8pf11329wp9"; + type = "gem"; + }; + version = "1.0.3"; + }; yard = { groups = ["development"]; platforms = []; diff --git a/lib/errgonomic.rb b/lib/errgonomic.rb index 348e7df..f9de496 100644 --- a/lib/errgonomic.rb +++ b/lib/errgonomic.rb @@ -9,6 +9,9 @@ require_relative 'errgonomic/option' require_relative 'errgonomic/result' +# Rails fu +require_relative 'errgonomic/rails' if defined?(Rails::Railtie) + # Errgonomic adds opinionated abstractions to handle errors in a way that blends # Rust and Ruby ergonomics. This library leans on Rails conventions for some # presence-related methods; when in doubt, make those feel like Rails. It also diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb new file mode 100644 index 0000000..494cf39 --- /dev/null +++ b/lib/errgonomic/rails.rb @@ -0,0 +1,40 @@ +require_relative 'option' +require_relative 'rails/active_record_optional' +require_relative 'rails/active_record_delegate_optional' + +module Errgonomic + # Rails specific functionality to integrate Errgonomic with minimum fuss. + module Rails + # We provide helper class methods, like `delegate_optional`, + # which need to be included into ActiveRecord::Base before any models are + # evaluated. + def self.setup_before + ActiveRecord::Base.include(Errgonomic::Rails::ActiveRecordDelegateOptional) + end + + # Wrapping optional associations requires that we include the module after + # the class is first evaluated, so that it can define its associations for + # later reflection. + def self.setup_after + ActiveRecord::Base.descendants.each do |model| + model.include Errgonomic::Rails::ActiveRecordOptional if model.table_name + end + end + end +end + +# TODO: Implement a Railtie to hook in the setup_before and setup_after at the +# appropriate times for the Rails application lifecycle, in dev and prod. +# +# if defined?(Rails::Railtie) +# module Errgonomic::Rails +# class Railtie < Rails::Railtie +# initializer 'errgonomic.rails.setup_before' do +# Errgonomic::Rails.setup_before +# end +# initializer 'errgonomic.rails.setup_after' do +# Errgonomic::Rails.setup_after +# end +# end +# end +# end diff --git a/lib/errgonomic/rails/active_record_delegate_optional.rb b/lib/errgonomic/rails/active_record_delegate_optional.rb new file mode 100644 index 0000000..5a384ac --- /dev/null +++ b/lib/errgonomic/rails/active_record_delegate_optional.rb @@ -0,0 +1,22 @@ +module Errgonomic + module Rails + module ActiveRecordDelegateOptional + extend ActiveSupport::Concern + + class_methods do + def delegate_optional(*methods, to: nil, prefix: nil, private: nil) + return if to.nil? + + methods.each do |method_name| + prefixed_method_name = prefix == true ? "#{to}_#{method_name}" : method_name + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{prefixed_method_name} + #{to}.map { |obj| obj.send(:#{method_name}) } + end + RUBY + end + end + end + end + end +end diff --git a/lib/errgonomic/rails/active_record_optional.rb b/lib/errgonomic/rails/active_record_optional.rb new file mode 100644 index 0000000..7bc2792 --- /dev/null +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Errgonomic + module Rails + # Concern to make ActiveRecord optional attributes and associations return an Option. + # + module ActiveRecordOptional + extend ActiveSupport::Concern + included do + # ::Rails.logger.debug('ActiveRecordOptional') + optional_associations = reflect_on_all_associations(:belongs_to) + .select { |r| r.options[:optional] } + .map(&:name) + optional_attributes = column_names + .select { |n| column_for_attribute(n).null } + (optional_attributes + optional_associations).each do |name| + # Rails.logger.debug("#{self.name}: #{name}: optional") + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name} + raise "stack too deep" if caller.length > 1024 + val = super + val.nil? ? Errgonomic::Option::None.new : Errgonomic::Option::Some.new(val) + end + RUBY + end + end + end + end +end + +# do we need this since we alias present below? +class SomeValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors.add(attribute, 'is invalid') unless value.some? + end +end + +module Errgonomic + module Option + class Any + alias some? present? + alias none? blank? + end + + class Some + delegate :marked_for_destruction?, to: :value + delegate :persisted?, to: :value + delegate :touch_later, to: :value + + def to_s + raise "Attempted to convert Some to String, please use Option API to safely work with internal value -- #{value}" + end + end + + class None + def nil? + true + end + + def to_s + raise 'Cannot convert None to String - please use Option API to safely work with internal value' + end + end + end +end + +module ActiveRecordOptionShim + def type_cast(value) + case value + when Errgonomic::Option::Some + super(value.unwrap!) + when Errgonomic::Option::None + super(nil) + else + super + end + end +end + +ActiveRecord::ConnectionAdapters::Quoting.prepend(ActiveRecordOptionShim) + +class NilClass + def to_option + None() + end +end + +class Object + def to_option + Some(self) + end +end diff --git a/test/rails_test.rb b/test/rails_test.rb new file mode 100644 index 0000000..041c671 --- /dev/null +++ b/test/rails_test.rb @@ -0,0 +1,79 @@ +require 'active_record' +require 'minitest/autorun' +require 'logger' + +require_relative '../lib/errgonomic/rails' + +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') +ActiveRecord::Base.logger = Logger.new(File::NULL) + +# Book reviews with various optional attributes and associations +ActiveRecord::Schema.define do + create_table 'authors', force: :cascade do |t| + t.string :name, null: false + t.text :bio + t.timestamps + end + + create_table 'books', force: :cascade do |t| + t.string :title, null: false + t.string :isbn + t.date :published_at + t.references :author + t.references :genre + t.timestamps + end + + create_table 'genres', force: :cascade do |t| + t.string :name, null: false + t.references :parent, foreign_key: { to_table: :genres } + t.timestamps + end +end + +# Before classes are loaded we need to define helper methods like `delegate_optional` +Errgonomic::Rails.setup_before + +class Author < ActiveRecord::Base + has_many :books +end + +class Book < ActiveRecord::Base + has_many :reviews + has_many :reviewers, through: :reviews, source: :user + belongs_to :author, optional: true + + delegate_optional :name, to: :author, prefix: true +end + +class Genre < ActiveRecord::Base + has_many :books + belongs_to :parent, class_name: 'Genre', optional: true +end + +# Optional associations have to be defined after the model is evaluated so we +# can reflect on those associations. +Errgonomic::Rails.setup_after + +class BugTest < Minitest::Test + def test_optional_attributes + author = Author.create!(name: 'Cixin Liu') + assert author.name.present? + assert author.bio.none? + book = author.books.create!(title: 'The Three-Body Problem') + assert book.isbn.none? + end + + def test_optional_associations + author = Author.create!(name: 'Cixin Liu') + book = author.books.create!(title: 'The Dark Forest') + assert book.author.some? + end + + def test_delegate_optional + author = Author.create!(name: 'Cixin Liu') + book = author.books.create!(title: 'Death\'s End') + assert book.author_name.some? + assert_equal author.name, book.author_name.unwrap! + end +end From d47c8fedcb7d31abf5036c382f5fd671b11b22a4 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:03:12 -0600 Subject: [PATCH 02/12] railtie and some gem updates --- Gemfile.lock | 84 ++++++++------- flake.lock | 8 +- flake.nix | 2 +- gemset.nix | 226 ++++++++++++++++++++++------------------ lib/errgonomic/rails.rb | 27 ++--- 5 files changed, 183 insertions(+), 164 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2ce428d..9ce71e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,98 +26,104 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - ast (2.4.2) + ast (2.4.3) backport (1.2.0) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) concurrent-ruby (1.3.5) - connection_pool (2.5.1) - diff-lcs (1.6.0) - drb (2.2.1) + connection_pool (2.5.3) + diff-lcs (1.6.2) + drb (2.2.3) i18n (1.14.7) concurrent-ruby (~> 1.0) - jaro_winkler (1.6.0) - json (2.10.1) + jaro_winkler (1.6.1) + json (2.12.2) kramdown (2.5.1) rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - language_server-protocol (3.17.0.4) - logger (1.6.6) - mini_portile2 (2.8.8) - minitest (5.25.4) - nokogiri (1.18.3) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + mini_portile2 (2.8.9) + minitest (5.25.5) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) observer (0.1.2) - ostruct (0.6.1) - parallel (1.26.3) - parser (3.3.7.1) + ostruct (0.6.2) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc + prism (1.4.0) racc (1.8.1) rainbow (3.1.1) - rake (13.2.1) - rbs (3.8.1) + rake (13.3.0) + rbs (3.9.4) logger regexp_parser (2.10.0) reverse_markdown (3.0.0) nokogiri rexml (3.4.1) - rspec (3.13.0) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) + rspec-core (3.13.5) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.2) - rubocop (1.71.2) + rspec-support (3.13.4) + rubocop (1.78.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.1) - parser (>= 3.3.1.0) - rubocop-yard (0.10.0) - rubocop (~> 1.21) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-yard (1.0.0) + lint_roller + rubocop (~> 1.72) yard ruby-progressbar (1.13.0) securerandom (0.4.1) - solargraph (0.52.0) + solargraph (0.56.0) backport (~> 1.2) - benchmark + benchmark (~> 0.4) bundler (~> 2.0) diff-lcs (~> 1.4) - jaro_winkler (~> 1.6) + jaro_winkler (~> 1.6, >= 1.6.1) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) logger (~> 1.6) observer (~> 0.1) ostruct (~> 0.6) parser (~> 3.0) - rbs (~> 3.0) - reverse_markdown (>= 2.0, < 4) + prism (~> 1.4) + rbs (~> 3.3) + reverse_markdown (~> 3.0) rubocop (~> 1.38) thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) yard-solargraph (~> 0.1) - sqlite3 (2.6.0) + sqlite3 (2.7.2) mini_portile2 (~> 2.8.0) thor (1.3.2) - tilt (2.6.0) + tilt (2.6.1) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -149,4 +155,4 @@ DEPENDENCIES yard-doctest (~> 0.1) BUNDLED WITH - 2.5.22 + 2.6.7 diff --git a/flake.lock b/flake.lock index 9108411..6eabc6c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1740463929, - "narHash": "sha256-4Xhu/3aUdCKeLfdteEHMegx5ooKQvwPHNkOgNCXQrvc=", + "lastModified": 1751741127, + "narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5d7db4668d7a0c6cc5fc8cf6ef33b008b2b1ed8b", + "rev": "29e290002bfff26af1db6f64d070698019460302", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-24.11", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 7b270fa..5e52811 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Errgonomic"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-24.11"; + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.05"; }; outputs = diff --git a/gemset.nix b/gemset.nix index fdb1e5f..c168b26 100644 --- a/gemset.nix +++ b/gemset.nix @@ -33,17 +33,17 @@ version = "8.0.2"; }; ast = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "04nc8x27hlzlrr5c2gn7mar4vdr0apw5xg22wp6m8dx3wqr04a0y"; + sha256 = "10yknjyn0728gjn6b5syynvrvrwm66bhssbxq8mkhshxghaiailm"; type = "gem"; }; - version = "2.4.2"; + version = "2.4.3"; }; backport = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -57,33 +57,33 @@ platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "01qml0yilb9basf7is2614skjp8384h2pycfx86cr8023arfj98g"; + sha256 = "0yx9yn47a8lkfcjmigk79fykxvr80r4m1i35q82sxzynpbm7lcr7"; type = "gem"; }; - version = "0.2.0"; + version = "0.3.0"; }; benchmark = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0jl71qcgamm96dzyqk695j24qszhcc7liw74qc83fpjljp2gh4hg"; + sha256 = "1kicilpma5l0lwayqjb5577bm0hbjndj2gh150xz09xsgc1l1vyl"; type = "gem"; }; - version = "0.4.0"; + version = "0.4.1"; }; bigdecimal = { groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1k6qzammv9r6b2cw3siasaik18i6wjc5m0gw5nfdc6jj64h79z1g"; + sha256 = "1p2szbr4jdvmwaaj2kxlbv1rp0m6ycbgfyp0kjkkkswmniv5y21r"; type = "gem"; }; - version = "3.1.9"; + version = "3.2.2"; }; concurrent-ruby = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -97,30 +97,30 @@ platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "171yzazg2wccbc545hwsx85sba91k7k1imir3c88385mlj82m05f"; + sha256 = "0nrhsk7b3sjqbyl1cah6ibf1kvi3v93a7wf4637d355hp614mmyg"; type = "gem"; }; - version = "2.5.1"; + version = "2.5.3"; }; diff-lcs = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0bnss89lcm3b1k3xcjd35grxqz5q040d12imd73qybwnfarggrx1"; + sha256 = "0qlrj2qyysc9avzlr4zs1py3x684hqm61n4czrsk1pyllz5x5q4s"; type = "gem"; }; - version = "1.6.0"; + version = "1.6.2"; }; drb = { groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0h5kbj9hvg5hb3c7l425zpds0vb42phvln2knab8nmazg2zp5m79"; + sha256 = "0wrkl7yiix268s2md1h6wh91311w95ikd8fy8m5gx589npyxc00b"; type = "gem"; }; - version = "2.2.1"; + version = "2.2.3"; }; errgonomic = { dependencies = ["concurrent-ruby"]; @@ -144,28 +144,28 @@ version = "1.14.7"; }; jaro_winkler = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "09645h5an19zc1i7wlmixszj8xxqb2zc8qlf8dmx39bxpas1l24b"; + sha256 = "14xkw4lb6wwvbcwqkf6ds116sridk9c8yz6y3caw07vzpwdvcmn0"; type = "gem"; }; - version = "1.6.0"; + version = "1.6.1"; }; json = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1p4l5ycdxfsr8b51gnvlvhq6s21vmx9z4x617003zbqv3bcqmj6x"; + sha256 = "1x5b8ipv6g0z44wgc45039k04smsyf95h2m5m67mqq35sa5a955s"; type = "gem"; }; - version = "2.10.1"; + version = "2.12.2"; }; kramdown = { dependencies = ["rexml"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -176,7 +176,7 @@ }; kramdown-parser-gfm = { dependencies = ["kramdown"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -186,58 +186,68 @@ version = "1.1.0"; }; language_server-protocol = { - groups = ["default"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1k0311vah76kg5m6zr7wmkwyk5p2f9d9hyckjpn3xgr83ajkj7px"; + type = "gem"; + }; + version = "3.17.0.5"; + }; + lint_roller = { + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0scnz2fvdczdgadvjn0j9d49118aqm3hj66qh8sd2kv6g1j65164"; + sha256 = "11yc0d84hsnlvx8cpk4cbj6a4dz9pk0r1k29p0n1fz9acddq831c"; type = "gem"; }; - version = "3.17.0.4"; + version = "1.1.0"; }; logger = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "05s008w9vy7is3njblmavrbdzyrwwc1fsziffdr58w9pwqj8sqfx"; + sha256 = "00q2zznygpbls8asz5knjvvj2brr3ghmqxgr83xnrdj4rk3xwvhr"; type = "gem"; }; - version = "1.6.6"; + version = "1.7.0"; }; mini_portile2 = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0x8asxl83msn815lwmb2d7q5p29p7drhjv5va0byhk60v9n16iwf"; + sha256 = "12f2830x7pq3kj0v8nz0zjvaw02sv01bqs1zwdrc04704kwcgmqc"; type = "gem"; }; - version = "2.8.8"; + version = "2.8.9"; }; minitest = { - groups = ["default" "development"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0izrg03wn2yj3gd76ck7ifbm9h2kgy8kpg4fk06ckpy4bbicmwlw"; + sha256 = "0mn7q9yzrwinvfvkyjiz548a4rmcwbmz2fn9nyzh4j1snin6q6rr"; type = "gem"; }; - version = "5.25.4"; + version = "5.25.5"; }; nokogiri = { dependencies = ["mini_portile2" "racc"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0npx535cs8qc33n0lpbbwl0p9fi3a5bczn6ayqhxvknh9yqw77vb"; + sha256 = "0rb306hbky6cxfyc8vrwpvl40fdapjvhsk62h08gg9wwbn3n8x4c"; type = "gem"; }; - version = "1.18.3"; + version = "1.18.8"; }; observer = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -247,38 +257,48 @@ version = "0.1.2"; }; ostruct = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "05xqijcf80sza5pnlp1c8whdaay8x5dc13214ngh790zrizgp8q9"; + sha256 = "1h6gazp5837xbz1aqvq9x0a5ffpw32nhvknn931a4074k6i04wvd"; type = "gem"; }; - version = "0.6.1"; + version = "0.6.2"; }; parallel = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1vy7sjs2pgz4i96v5yk9b7aafbffnvq7nn419fgvw55qlavsnsyq"; + sha256 = "0c719bfgcszqvk9z47w2p8j2wkz5y35k48ywwas5yxbbh3hm3haa"; type = "gem"; }; - version = "1.26.3"; + version = "1.27.0"; }; parser = { dependencies = ["ast" "racc"]; - groups = ["default"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0i9w8msil4snx5w11ix9b0wf52vjc3r49khy3ddgl1xk890kcxi4"; + type = "gem"; + }; + version = "3.3.8.0"; + }; + prism = { + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "18dcwrcnddvi8gl3hmbsb2cj1l7afxk2lh3jmhj90l95h1hn3gkx"; + sha256 = "0gkhpdjib9zi9i27vd9djrxiwjia03cijmd6q8yj2q1ix403w3nw"; type = "gem"; }; - version = "3.3.7.1"; + version = "1.4.0"; }; racc = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -288,7 +308,7 @@ version = "1.8.1"; }; rainbow = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -298,28 +318,28 @@ version = "3.1.1"; }; rake = { - groups = ["default"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "17850wcwkgi30p7yqh60960ypn7yibacjjha0av78zaxwvd3ijs6"; + sha256 = "14s4jdcs1a4saam9qmzbsa2bsh85rj9zfxny5z315x3gg0nhkxcn"; type = "gem"; }; - version = "13.2.1"; + version = "13.3.0"; }; rbs = { dependencies = ["logger"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "07cwjkx7b3ssy8ccqq1s34sc5snwvgxan2ikmp9y2rz2a9wy6v1b"; + sha256 = "1mx533jn2nv29xc5faw9g5xj9qbdaiwl9wv2byv98bgw6gqwhhlf"; type = "gem"; }; - version = "3.8.1"; + version = "3.9.4"; }; regexp_parser = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -330,7 +350,7 @@ }; reverse_markdown = { dependencies = ["nokogiri"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -340,7 +360,7 @@ version = "3.0.0"; }; rexml = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -351,93 +371,93 @@ }; rspec = { dependencies = ["rspec-core" "rspec-expectations" "rspec-mocks"]; - groups = ["default"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "14xrp8vq6i9zx37vh0yp4h9m0anx9paw200l1r5ad9fmq559346l"; + sha256 = "0h11wynaki22a40rfq3ahcs4r36jdpz9acbb3m5dkf0mm67sbydr"; type = "gem"; }; - version = "3.13.0"; + version = "3.13.1"; }; rspec-core = { dependencies = ["rspec-support"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1r6zbis0hhbik1ck8kh58qb37d1qwij1x1d2fy4jxkzryh3na4r5"; + sha256 = "18sgga9zjrd5579m9rpb78l7yn9a0bjzwz51z5kiq4y6jwl6hgxb"; type = "gem"; }; - version = "3.13.3"; + version = "3.13.5"; }; rspec-expectations = { dependencies = ["diff-lcs" "rspec-support"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0n3cyrhsa75x5wwvskrrqk56jbjgdi2q1zx0irllf0chkgsmlsqf"; + sha256 = "0dl8npj0jfpy31bxi6syc7jymyd861q277sfr6jawq2hv6hx791k"; type = "gem"; }; - version = "3.13.3"; + version = "3.13.5"; }; rspec-mocks = { dependencies = ["diff-lcs" "rspec-support"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1vxxkb2sf2b36d8ca2nq84kjf85fz4x7wqcvb8r6a5hfxxfk69r3"; + sha256 = "10gajm8iscl7gb8q926hyna83bw3fx2zb4sqdzjrznjs51pqlcz4"; type = "gem"; }; - version = "3.13.2"; + version = "3.13.5"; }; rspec-support = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1v6v6xvxcpkrrsrv7v1xgf7sl0d71vcfz1cnrjflpf6r7x3a58yf"; + sha256 = "1xx3f4mgr84jz07fifd3r68hm6giqy91hqyzawmi0s59yqa1hjqq"; type = "gem"; }; - version = "3.13.2"; + version = "3.13.4"; }; rubocop = { - dependencies = ["json" "language_server-protocol" "parallel" "parser" "rainbow" "regexp_parser" "rubocop-ast" "ruby-progressbar" "unicode-display_width"]; - groups = ["default"]; + dependencies = ["json" "language_server-protocol" "lint_roller" "parallel" "parser" "rainbow" "regexp_parser" "rubocop-ast" "ruby-progressbar" "unicode-display_width"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0zzrsdz23jmjc8iwilzhw5cifn4flzmsbwkzxlwa6qf6m80payws"; + sha256 = "1h3b1pl0wawm9w6jad2w333xijjxykvzflc8hzkd6kzb2bwscx4b"; type = "gem"; }; - version = "1.71.2"; + version = "1.78.0"; }; rubocop-ast = { - dependencies = ["parser"]; - groups = ["default"]; + dependencies = ["parser" "prism"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "1zjpv3kw4ciwk0dh43zj17ws318vnirby1clmcy6j9mvr4mbxv40"; + sha256 = "0gis8w51k5dsmzzlppvwwznqyfd73fa3zcrpl1xihzy1mm4jw14l"; type = "gem"; }; - version = "1.38.1"; + version = "1.45.1"; }; rubocop-yard = { - dependencies = ["rubocop" "yard"]; + dependencies = ["lint_roller" "rubocop" "yard"]; groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "03s8lwah6apkr1g25whhd9y2zrqq9dy56g5kwn0bxp0slakrpisz"; + sha256 = "0k1pcbc7hahfchbivf8gn38rxxxv8p30vvnzihlr0sf3rdrzz4ai"; type = "gem"; }; - version = "0.10.0"; + version = "1.0.0"; }; ruby-progressbar = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -457,15 +477,15 @@ version = "0.4.1"; }; solargraph = { - dependencies = ["backport" "benchmark" "diff-lcs" "jaro_winkler" "kramdown" "kramdown-parser-gfm" "logger" "observer" "ostruct" "parser" "rbs" "reverse_markdown" "rubocop" "thor" "tilt" "yard" "yard-solargraph"]; - groups = ["default"]; + dependencies = ["backport" "benchmark" "diff-lcs" "jaro_winkler" "kramdown" "kramdown-parser-gfm" "logger" "observer" "ostruct" "parser" "prism" "rbs" "reverse_markdown" "rubocop" "thor" "tilt" "yard" "yard-solargraph"]; + groups = ["development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0fqa486hfn6kdbqp3ppy3jvl9xyj8jz41a2dzgkhc6ny2pj31w92"; + sha256 = "1jf8d19j32f3h99acmx9h4g5qaykxzb25fqp028g2bvqvl8k7r2r"; type = "gem"; }; - version = "0.52.0"; + version = "0.56.0"; }; sqlite3 = { dependencies = ["mini_portile2"]; @@ -473,13 +493,13 @@ platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0573vgz5ck0hqr8h132ln0hczx53m21h4w42n1p75rj837qjbim1"; + sha256 = "1xf9pm1pvrny4by0sy7s9hlpz2kwij5p9gys77fwd87zqpgpcqs4"; type = "gem"; }; - version = "2.6.0"; + version = "2.7.2"; }; thor = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -489,14 +509,14 @@ version = "1.3.2"; }; tilt = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0szpapi229v3scrvw1pgy0vpjm7z3qlf58m1198kxn70cs278g96"; + sha256 = "0w27v04d7rnxjr3f65w1m7xyvr6ch6szjj2v5wv1wz6z5ax9pa9m"; type = "gem"; }; - version = "2.6.0"; + version = "2.6.1"; }; timeout = { groups = ["default" "development"]; @@ -521,7 +541,7 @@ }; unicode-display_width = { dependencies = ["unicode-emoji"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -531,7 +551,7 @@ version = "3.1.4"; }; unicode-emoji = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -573,7 +593,7 @@ }; yard-solargraph = { dependencies = ["yard"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 494cf39..a31ecf3 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -3,7 +3,6 @@ require_relative 'rails/active_record_delegate_optional' module Errgonomic - # Rails specific functionality to integrate Errgonomic with minimum fuss. module Rails # We provide helper class methods, like `delegate_optional`, # which need to be included into ActiveRecord::Base before any models are @@ -21,20 +20,14 @@ def self.setup_after end end end -end -# TODO: Implement a Railtie to hook in the setup_before and setup_after at the -# appropriate times for the Rails application lifecycle, in dev and prod. -# -# if defined?(Rails::Railtie) -# module Errgonomic::Rails -# class Railtie < Rails::Railtie -# initializer 'errgonomic.rails.setup_before' do -# Errgonomic::Rails.setup_before -# end -# initializer 'errgonomic.rails.setup_after' do -# Errgonomic::Rails.setup_after -# end -# end -# end -# end + # Hook into Rails with a Railtie + class Railtie < ::Rails::Railtie + initializer 'errgonomic.setup_before' do + Errgonomic::Rails.setup_before + end + config.to_prepare do + Errgonomic::Rails.setup_after + end + end +end From 6ee315c4f37046d7c7032f939e45c00530b021f5 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:12:53 -0600 Subject: [PATCH 03/12] iterate --- lib/errgonomic/rails.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index a31ecf3..16e70eb 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -24,9 +24,11 @@ def self.setup_after # Hook into Rails with a Railtie class Railtie < ::Rails::Railtie initializer 'errgonomic.setup_before' do + puts "errgonomic railtie initializer to_prepare setup_before" Errgonomic::Rails.setup_before end config.to_prepare do + puts "errgonomic railtie config.to_prepare setup_after" Errgonomic::Rails.setup_after end end From ee576bed5d0b65f21e15015619e2840e03dd725f Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:15:43 -0600 Subject: [PATCH 04/12] wip --- lib/errgonomic/rails.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 16e70eb..527ad2d 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -16,6 +16,7 @@ def self.setup_before # later reflection. def self.setup_after ActiveRecord::Base.descendants.each do |model| + puts "errgonomic making #{model.name} optional" model.include Errgonomic::Rails::ActiveRecordOptional if model.table_name end end From 152cbc708ab107801219f5a68f79fb65650df253 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:23:03 -0600 Subject: [PATCH 05/12] wip --- lib/errgonomic/rails.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 527ad2d..a7178a8 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -15,9 +15,11 @@ def self.setup_before # the class is first evaluated, so that it can define its associations for # later reflection. def self.setup_after + Rails.application.eager_load! unless Rails.configuration.cache_classes ActiveRecord::Base.descendants.each do |model| + next unless model.table_exists? rescue false puts "errgonomic making #{model.name} optional" - model.include Errgonomic::Rails::ActiveRecordOptional if model.table_name + model.include Errgonomic::Rails::ActiveRecordOptional end end end @@ -28,6 +30,11 @@ class Railtie < ::Rails::Railtie puts "errgonomic railtie initializer to_prepare setup_before" Errgonomic::Rails.setup_before end + config.after_initialize do + ActiveSupport.on_load(:after_initialize) do + Errgonomic::Rails.setup_after + end + end config.to_prepare do puts "errgonomic railtie config.to_prepare setup_after" Errgonomic::Rails.setup_after From 7aa6344100e91fdbc9c2fd65d84a54d84c7e97d3 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:25:11 -0600 Subject: [PATCH 06/12] wip --- lib/errgonomic/rails.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index a7178a8..5fc45f7 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -15,7 +15,6 @@ def self.setup_before # the class is first evaluated, so that it can define its associations for # later reflection. def self.setup_after - Rails.application.eager_load! unless Rails.configuration.cache_classes ActiveRecord::Base.descendants.each do |model| next unless model.table_exists? rescue false puts "errgonomic making #{model.name} optional" From 332d32b1330c6f6886db297a6793c301353ad01c Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Tue, 8 Jul 2025 16:31:49 -0600 Subject: [PATCH 07/12] wip --- lib/errgonomic/rails.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 5fc45f7..9e8bf95 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -15,6 +15,7 @@ def self.setup_before # the class is first evaluated, so that it can define its associations for # later reflection. def self.setup_after + Zeitwerk::Loader.eager_load_all ActiveRecord::Base.descendants.each do |model| next unless model.table_exists? rescue false puts "errgonomic making #{model.name} optional" From 4a9430a0edd1b68b9397c5beffa86fe8011e6bdd Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Wed, 9 Jul 2025 10:07:56 -0600 Subject: [PATCH 08/12] wip --- lib/errgonomic/rails.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index 9e8bf95..df944ee 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -3,6 +3,8 @@ require_relative 'rails/active_record_delegate_optional' module Errgonomic + # Slightly more convenient access to the setup functions: + # Errgonomic::Rails.setup_before and Errgonomic::Rails.setup_after module Rails # We provide helper class methods, like `delegate_optional`, # which need to be included into ActiveRecord::Base before any models are @@ -17,8 +19,12 @@ def self.setup_before def self.setup_after Zeitwerk::Loader.eager_load_all ActiveRecord::Base.descendants.each do |model| - next unless model.table_exists? rescue false - puts "errgonomic making #{model.name} optional" + next unless begin + model.table_exists? + rescue StandardError + false + end + model.include Errgonomic::Rails::ActiveRecordOptional end end @@ -27,7 +33,6 @@ def self.setup_after # Hook into Rails with a Railtie class Railtie < ::Rails::Railtie initializer 'errgonomic.setup_before' do - puts "errgonomic railtie initializer to_prepare setup_before" Errgonomic::Rails.setup_before end config.after_initialize do @@ -36,7 +41,6 @@ class Railtie < ::Rails::Railtie end end config.to_prepare do - puts "errgonomic railtie config.to_prepare setup_after" Errgonomic::Rails.setup_after end end From c94201307493054c09b3976e1f9ef7240ed00789 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Wed, 9 Jul 2025 11:01:25 -0600 Subject: [PATCH 09/12] keep track of columns that are given the optional treatment --- lib/errgonomic/option.rb | 19 ++++++++++++++++--- .../rails/active_record_optional.rb | 3 ++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/errgonomic/option.rb b/lib/errgonomic/option.rb index 15f0d7f..0dbff41 100644 --- a/lib/errgonomic/option.rb +++ b/lib/errgonomic/option.rb @@ -5,6 +5,10 @@ module Option # The base class for all options. Some and None are subclasses. # class Any + # def method_missing(_name, *_args) + # raise 'do it right noob' + # end + # An option of the same type with an equal inner value is equal. # # Because we're going to monkey patch this into other libraries Rails, we @@ -98,7 +102,7 @@ def unwrap! # message # @example # Some(1).expect!("msg") # => 1 - # None().expect!("msg") # => raise Errgonomic::ExpectError, "msg" + # None().expect!("here's why this failed") # => raise Errgonomic::ExpectError, "here's why this failed" def expect!(msg) raise Errgonomic::ExpectError, msg if none? @@ -115,6 +119,16 @@ def unwrap_or(default) value end + # # returns the inner value if present, else returns the default value + # # @example + # # Some(1).unwrap_or(2) # => 1 + # # None().unwrap_or(2) # => 2 + # def unwrap_or_default + # self.class.respond_to?(:default) or raise + # return self.class.default if none? + # value + # end + # returns the inner value if present, else returns the result of the # provided block # @example @@ -302,6 +316,7 @@ def zip(other) # Some(2).zip_with(Some(3)) { |a, b| a + b } # => Some(5) def zip_with(other, &block) return None() unless some? && other.some? + other = block.call(value, other.value) Some(other) end @@ -314,8 +329,6 @@ def zip_with(other, &block) # take # take_if # replace - # zip - # zip_with end # Represent a value diff --git a/lib/errgonomic/rails/active_record_optional.rb b/lib/errgonomic/rails/active_record_optional.rb index 7bc2792..48c3cda 100644 --- a/lib/errgonomic/rails/active_record_optional.rb +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -13,7 +13,8 @@ module ActiveRecordOptional .map(&:name) optional_attributes = column_names .select { |n| column_for_attribute(n).null } - (optional_attributes + optional_associations).each do |name| + @optionals = (optional_attributes + optional_associations) + @optionals.each do |name| # Rails.logger.debug("#{self.name}: #{name}: optional") class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} From 53f75b28c8a25da1ea48c1ba9f2997eb7cf552e6 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Wed, 9 Jul 2025 11:06:06 -0600 Subject: [PATCH 10/12] we want to only mess with the app, not the framework --- lib/errgonomic/rails.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/errgonomic/rails.rb b/lib/errgonomic/rails.rb index df944ee..26594de 100644 --- a/lib/errgonomic/rails.rb +++ b/lib/errgonomic/rails.rb @@ -18,7 +18,8 @@ def self.setup_before # later reflection. def self.setup_after Zeitwerk::Loader.eager_load_all - ActiveRecord::Base.descendants.each do |model| + setupable = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base + setupable.descendants.each do |model| next unless begin model.table_exists? rescue StandardError From d4e89948e7d6e86b1af3e849f773045dfc9744db Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Wed, 9 Jul 2025 11:08:40 -0600 Subject: [PATCH 11/12] class accessor for the optionals --- .../rails/active_record_optional.rb | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/errgonomic/rails/active_record_optional.rb b/lib/errgonomic/rails/active_record_optional.rb index 48c3cda..b50c8b7 100644 --- a/lib/errgonomic/rails/active_record_optional.rb +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -6,6 +6,7 @@ module Rails # module ActiveRecordOptional extend ActiveSupport::Concern + included do # ::Rails.logger.debug('ActiveRecordOptional') optional_associations = reflect_on_all_associations(:belongs_to) @@ -13,19 +14,24 @@ module ActiveRecordOptional .map(&:name) optional_attributes = column_names .select { |n| column_for_attribute(n).null } - @optionals = (optional_attributes + optional_associations) - @optionals.each do |name| - # Rails.logger.debug("#{self.name}: #{name}: optional") + @errgonomic_optionals = (optional_attributes + optional_associations) + @errgonomic_optionals.each do |name| class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{name} - raise "stack too deep" if caller.length > 1024 - val = super - val.nil? ? Errgonomic::Option::None.new : Errgonomic::Option::Some.new(val) - end + def #{name} + raise "stack too deep" if caller.length > 1024 + val = super + val.nil? ? Errgonomic::Option::None.new : Errgonomic::Option::Some.new(val) + end RUBY end end end + + class_methods do + def errgonomic_optionals + @errgonomic_optionals + end + end end end From 162943f5b9cb7ce14e2810d27f1a0da3cdf97687 Mon Sep 17 00:00:00 2001 From: Nick Zadrozny Date: Wed, 9 Jul 2025 11:10:26 -0600 Subject: [PATCH 12/12] concern stuff goes in the concern --- lib/errgonomic/rails/active_record_optional.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/errgonomic/rails/active_record_optional.rb b/lib/errgonomic/rails/active_record_optional.rb index b50c8b7..75db1e7 100644 --- a/lib/errgonomic/rails/active_record_optional.rb +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -25,11 +25,11 @@ def #{name} RUBY end end - end - class_methods do - def errgonomic_optionals - @errgonomic_optionals + class_methods do + def errgonomic_optionals + @errgonomic_optionals + end end end end