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..9ce71e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,92 +7,130 @@ PATH GEM remote: https://rubygems.org/ specs: - ast (2.4.2) + 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.3) backport (1.2.0) - benchmark (0.4.0) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) concurrent-ruby (1.3.5) - diff-lcs (1.6.0) - jaro_winkler (1.6.0) - json (2.10.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.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) - solargraph (0.52.0) + securerandom (0.4.1) + 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.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) 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,14 +142,17 @@ PLATFORMS ruby DEPENDENCIES + activerecord errgonomic! + minitest rake (~> 13.0) rspec (~> 3.0) rubocop rubocop-yard solargraph + sqlite3 yard (~> 0.9) 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 22b520a..c168b26 100644 --- a/gemset.nix +++ b/gemset.nix @@ -1,16 +1,49 @@ { + 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"]; + 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"]; @@ -19,18 +52,38 @@ }; version = "1.2.0"; }; + base64 = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0yx9yn47a8lkfcjmigk79fykxvr80r4m1i35q82sxzynpbm7lcr7"; + type = "gem"; + }; + 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 = "1p2szbr4jdvmwaaj2kxlbv1rp0m6ycbgfyp0kjkkkswmniv5y21r"; + type = "gem"; + }; + version = "3.2.2"; }; concurrent-ruby = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -39,15 +92,35 @@ }; version = "1.3.5"; }; + connection_pool = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0nrhsk7b3sjqbyl1cah6ibf1kvi3v93a7wf4637d355hp614mmyg"; + type = "gem"; + }; + version = "2.5.3"; + }; diff-lcs = { - groups = ["default"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0qlrj2qyysc9avzlr4zs1py3x684hqm61n4czrsk1pyllz5x5q4s"; + type = "gem"; + }; + version = "1.6.2"; + }; + drb = { + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0bnss89lcm3b1k3xcjd35grxqz5q040d12imd73qybwnfarggrx1"; + sha256 = "0wrkl7yiix268s2md1h6wh91311w95ikd8fy8m5gx589npyxc00b"; type = "gem"; }; - version = "1.6.0"; + version = "2.2.3"; }; errgonomic = { dependencies = ["concurrent-ruby"]; @@ -59,29 +132,40 @@ }; 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"]; + 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"]; @@ -92,7 +176,7 @@ }; kramdown-parser-gfm = { dependencies = ["kramdown"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -102,58 +186,68 @@ version = "1.1.0"; }; language_server-protocol = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; - sha256 = "0scnz2fvdczdgadvjn0j9d49118aqm3hj66qh8sd2kv6g1j65164"; + sha256 = "1k0311vah76kg5m6zr7wmkwyk5p2f9d9hyckjpn3xgr83ajkj7px"; type = "gem"; }; - version = "3.17.0.4"; + version = "3.17.0.5"; + }; + lint_roller = { + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "11yc0d84hsnlvx8cpk4cbj6a4dz9pk0r1k29p0n1fz9acddq831c"; + type = "gem"; + }; + 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"]; @@ -163,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"]; @@ -204,7 +308,7 @@ version = "1.8.1"; }; rainbow = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -214,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"]; @@ -246,7 +350,7 @@ }; reverse_markdown = { dependencies = ["nokogiri"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -256,7 +360,7 @@ version = "3.0.0"; }; rexml = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -267,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"]; @@ -362,19 +466,40 @@ }; 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"]; + 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"]; + groups = ["development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1xf9pm1pvrny4by0sy7s9hlpz2kwij5p9gys77fwd87zqpgpcqs4"; + type = "gem"; + }; + version = "2.7.2"; }; thor = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -384,18 +509,39 @@ version = "1.3.2"; }; tilt = { - groups = ["default"]; + groups = ["default" "development"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "0w27v04d7rnxjr3f65w1m7xyvr6ch6szjj2v5wv1wz6z5ax9pa9m"; + type = "gem"; + }; + version = "2.6.1"; + }; + 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 = "0szpapi229v3scrvw1pgy0vpjm7z3qlf58m1198kxn70cs278g96"; + sha256 = "16w2g84dzaf3z13gxyzlzbf748kylk5bdgg3n1ipvkvvqy685bwd"; type = "gem"; }; - version = "2.6.0"; + version = "2.0.6"; }; unicode-display_width = { dependencies = ["unicode-emoji"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -405,7 +551,7 @@ version = "3.1.4"; }; unicode-emoji = { - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; @@ -414,6 +560,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 = []; @@ -437,7 +593,7 @@ }; yard-solargraph = { dependencies = ["yard"]; - groups = ["default"]; + groups = ["default" "development"]; platforms = []; source = { remotes = ["https://rubygems.org"]; 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/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.rb b/lib/errgonomic/rails.rb new file mode 100644 index 0000000..26594de --- /dev/null +++ b/lib/errgonomic/rails.rb @@ -0,0 +1,48 @@ +require_relative 'option' +require_relative 'rails/active_record_optional' +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 + # 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 + Zeitwerk::Loader.eager_load_all + setupable = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base + setupable.descendants.each do |model| + next unless begin + model.table_exists? + rescue StandardError + false + end + + model.include Errgonomic::Rails::ActiveRecordOptional + end + end + end + + # Hook into Rails with a Railtie + class Railtie < ::Rails::Railtie + initializer 'errgonomic.setup_before' do + 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 + Errgonomic::Rails.setup_after + 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..75db1e7 --- /dev/null +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -0,0 +1,99 @@ +# 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 } + @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 + RUBY + end + end + + class_methods do + def errgonomic_optionals + @errgonomic_optionals + 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