From aa57618ad1a70180ab30343d540bd3e9fc71dd0d Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Mon, 6 Oct 2025 20:55:15 -0400 Subject: [PATCH 01/17] Modernize to support rails 7 & 8, drop support for rails 4/5/6. --- .github/workflows/ci.yml | 29 ++ .travis.yml | 25 -- Appraisals | 25 +- Gemfile.lock | 363 +++++++++++------- README.md | 18 +- gemfiles/rails_4.2.gemfile | 8 - .../{rails_6.0.gemfile => rails_7.0.gemfile} | 7 +- .../{rails_5.2.gemfile => rails_7.1.gemfile} | 5 +- gemfiles/rails_8.0.gemfile | 11 + lib/typical_situation/version.rb | 2 +- spec/spec_helper.rb | 5 - typical_situation.gemspec | 10 +- 12 files changed, 296 insertions(+), 212 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 gemfiles/rails_4.2.gemfile rename gemfiles/{rails_6.0.gemfile => rails_7.0.gemfile} (66%) rename gemfiles/{rails_5.2.gemfile => rails_7.1.gemfile} (55%) create mode 100644 gemfiles/rails_8.0.gemfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0976967 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Spec CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} + strategy: + matrix: + ruby: [3.0, 3.1, 3.2, 3.3] + rails: [rails_7.0, rails_7.1, rails_8.0] + exclude: + - ruby: 3.0 + rails: rails_8.0 # Rails 8 requires Ruby 3.1+ + env: + RUBY_VERSION: ${{ matrix.ruby }} + RAILS_VERSION: ${{ matrix.rails }} + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Install dependencies + run: bundle exec appraisal install + - name: Run specs + run: bundle exec appraisal ${{ matrix.rails }} rspec \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1b45396..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -os: linux -language: ruby -cache: bundler -before_install: - - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true - - gem install bundler -v '< 2' - - gem cleanup bundler -script: "bundle exec rspec" -rvm: - - 2.6 - - 2.5 - - 2.4 - - 2.3 -gemfile: - - gemfiles/rails_4.2.gemfile - - gemfiles/rails_5.2.gemfile - - gemfiles/rails_6.0.gemfile -jobs: - exclude: - - rvm: 2.3 - gemfile: gemfiles/rails_6.0.gemfile - - rvm: 2.4 - gemfile: gemfiles/rails_6.0.gemfile - - rvm: 2.6 - gemfile: gemfiles/rails_4.2.gemfile diff --git a/Appraisals b/Appraisals index 43335c8..d0be6a8 100644 --- a/Appraisals +++ b/Appraisals @@ -1,18 +1,25 @@ # frozen_string_literal: true -appraise 'rails-4.2' do - gem 'rails', '~> 4.2' - gem 'rails-forward_compatible_controller_tests', require: false +appraise 'rails_7.0' do + gem 'rails', '~> 7.0' + gem 'rspec', '~> 3.12' + gem 'rspec-rails', '~> 6.0' + gem 'rails-controller-testing' + gem 'sqlite3', '~> 1.4' end -appraise 'rails-5.2' do - gem 'rails', '~> 5.2' +appraise 'rails_7.1' do + gem 'rails', '~> 7.1' + gem 'rspec', '~> 3.12' + gem 'rspec-rails', '~> 6.1' gem 'rails-controller-testing' + gem 'sqlite3', '~> 1.4' end -appraise 'rails-6.0' do - gem 'rails', '~> 6.0' - gem 'sqlite3', '~> 1.4' +appraise 'rails_8.0' do + gem 'rails', '~> 8.0' + gem 'rspec', '~> 3.13' + gem 'rspec-rails', '~> 8.0' gem 'rails-controller-testing' - gem 'rspec-rails', '4.0.0.beta3' + gem 'sqlite3', '~> 2.1' end diff --git a/Gemfile.lock b/Gemfile.lock index b16a591..40af1f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,199 +1,268 @@ PATH remote: . specs: - typical_situation (0.11.1) - rails (>= 4.0.0) + typical_situation (1.0.0) + rails (>= 7.0.0) GEM remote: http://rubygems.org/ specs: - actioncable (6.0.1) - actionpack (= 6.0.1) + actioncable (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.1) - actionpack (= 6.0.1) - activejob (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) - mail (>= 2.7.1) - actionmailer (6.0.1) - actionpack (= 6.0.1) - actionview (= 6.0.1) - activejob (= 6.0.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.1) - actionview (= 6.0.1) - activesupport (= 6.0.1) - rack (~> 2.0) + zeitwerk (~> 2.6) + actionmailbox (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) + mail (>= 2.8.0) + actionmailer (8.0.3) + actionpack (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activesupport (= 8.0.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.3) + actionview (= 8.0.3) + activesupport (= 8.0.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.1) - actionpack (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.3) + actionpack (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.0.1) - activesupport (= 6.0.1) + actionview (8.0.3) + activesupport (= 8.0.3) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.1) - activesupport (= 6.0.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.3) + activesupport (= 8.0.3) globalid (>= 0.3.6) - activemodel (6.0.1) - activesupport (= 6.0.1) - activerecord (6.0.1) - activemodel (= 6.0.1) - activesupport (= 6.0.1) - activestorage (6.0.1) - actionpack (= 6.0.1) - activejob (= 6.0.1) - activerecord (= 6.0.1) - marcel (~> 0.3.1) - activesupport (6.0.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2) - appraisal (2.2.0) + activemodel (8.0.3) + activesupport (= 8.0.3) + activerecord (8.0.3) + activemodel (= 8.0.3) + activesupport (= 8.0.3) + timeout (>= 0.4.0) + activestorage (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activesupport (= 8.0.3) + marcel (~> 1.0) + activesupport (8.0.3) + 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) + appraisal (2.5.0) bundler rake thor (>= 0.14.0) - builder (3.2.3) - byebug (11.0.1) - combustion (1.1.2) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.3) + builder (3.3.0) + byebug (12.0.0) + combustion (1.5.0) activesupport (>= 3.0.0) railties (>= 3.0.0) thor (>= 0.14.6) - concurrent-ruby (1.1.5) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) coveralls (0.8.23) json (>= 1.8, < 3) simplecov (~> 0.16.1) term-ansicolor (~> 1.3) thor (>= 0.19.4, < 2.0) tins (~> 1.6) - crass (1.0.5) - diff-lcs (1.3) - docile (1.3.2) - erubi (1.9.0) - factory_bot (4.8.2) - activesupport (>= 3.0.0) - factory_bot_rails (4.8.2) - factory_bot (~> 4.8.2) - railties (>= 3.0.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.7.0) + crass (1.0.6) + date (3.4.1) + diff-lcs (1.6.2) + docile (1.4.1) + drb (2.2.3) + erb (5.0.3) + erubi (1.13.1) + factory_bot (6.5.5) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.2.0) - loofah (2.3.1) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.15.0) + logger (1.7.0) + loofah (2.24.1) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (0.9.2) - mimemagic (0.3.3) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.13.0) - nio4r (2.5.2) - nokogiri (1.10.5) - mini_portile2 (~> 2.4.0) - rack (2.0.7) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.1) - actioncable (= 6.0.1) - actionmailbox (= 6.0.1) - actionmailer (= 6.0.1) - actionpack (= 6.0.1) - actiontext (= 6.0.1) - actionview (= 6.0.1) - activejob (= 6.0.1) - activemodel (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) - bundler (>= 1.3.0) - railties (= 6.0.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + minitest (5.25.5) + mize (0.6.1) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + psych (5.2.6) + date + stringio + racc (1.8.1) + rack (3.2.1) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.3) + actioncable (= 8.0.3) + actionmailbox (= 8.0.3) + actionmailer (= 8.0.3) + actionpack (= 8.0.3) + actiontext (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activemodel (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) + bundler (>= 1.15.0) + railties (= 8.0.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.1) - actionpack (= 6.0.1) - activesupport (= 6.0.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.1) - rspec-core (3.9.0) - rspec-support (~> 3.9.0) - rspec-expectations (3.9.0) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.3.0) + rdoc (6.15.0) + erb + psych (>= 4.0.0) + tsort + reline (0.6.2) + io-console (~> 0.5) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (3.9.0) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + securerandom (0.4.1) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sprockets (4.0.0) + sqlite3 (1.7.3) + mini_portile2 (~> 2.8.0) + stringio (3.1.7) + sync (0.5.0) + term-ansicolor (1.11.3) + tins (~> 1) + thor (1.4.0) + timeout (0.4.3) + tins (1.44.1) + bigdecimal + mize (~> 0.6) + sync + tsort (0.2.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - sqlite3 (1.3.13) - term-ansicolor (1.7.1) - tins (~> 1.0) - thor (0.20.3) - thread_safe (0.3.6) - tins (1.22.2) - tzinfo (1.2.5) - thread_safe (~> 0.1) - websocket-driver (0.7.1) + uri (1.0.3) + useragent (0.16.11) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) - zeitwerk (2.2.1) + websocket-extensions (0.1.5) + zeitwerk (2.7.3) PLATFORMS + arm64-darwin-20 ruby DEPENDENCIES appraisal - bundler (~> 1.0) + bundler (>= 2.2.0) byebug combustion coveralls factory_bot_rails rake - rspec-rails - sqlite3 (~> 1.3.6) + rspec-rails (>= 6.0) + sqlite3 (>= 1.4) typical_situation! BUNDLED WITH - 1.17.1 + 2.7.2 diff --git a/README.md b/README.md index c9b1bc2..e9b6810 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Typical Situation [![Build Status](https://travis-ci.org/apsislabs/typical_situation.svg?branch=master)](https://travis-ci.org/apsislabs/typical_situation) +# Typical Situation [![Spec CI](https://github.com/apsislabs/typical_situation/workflows/Spec%20CI/badge.svg)](https://github.com/apsislabs/typical_situation/actions) The missing Ruby on Rails ActionController REST API mixin. @@ -8,22 +8,22 @@ A Ruby mixin (module) providing the seven standard resource actions & responses Tested in: -- Rails 4.2 -- Rails 5.2 -- Rails 6.0 +- Rails 7.0 +- Rails 7.1 +- Rails 8.0 Against Ruby versions: -- 2.2 -- 2.3 -- 2.4 -- 2.5 +- 3.0 +- 3.1 +- 3.2 +- 3.3 Add to your **Gemfile**: gem 'typical_situation' -**Rails 3.2**: For Rails 3.2 support, see https://github.com/mars/typical_situation +**Legacy Versions**: For Rails 4.x/5.x/6.x support, see older versions of this gem. Ruby 3.0+ is required. ## Usage diff --git a/gemfiles/rails_4.2.gemfile b/gemfiles/rails_4.2.gemfile deleted file mode 100644 index 0701be9..0000000 --- a/gemfiles/rails_4.2.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "http://rubygems.org" - -gem "rails", "~> 4.2" -gem "rails-forward_compatible_controller_tests", require: false - -gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_7.0.gemfile similarity index 66% rename from gemfiles/rails_6.0.gemfile rename to gemfiles/rails_7.0.gemfile index b49d5b7..8b23c5a 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_7.0.gemfile @@ -2,9 +2,10 @@ source "http://rubygems.org" -gem "rails", "~> 6.0" -gem "sqlite3", "~> 1.4" +gem "rails", "~> 7.0" +gem "rspec", "~> 3.12" +gem "rspec-rails", "~> 6.0" gem "rails-controller-testing" -gem "rspec-rails", "4.0.0.beta3" +gem "sqlite3", "~> 1.4" gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_7.1.gemfile similarity index 55% rename from gemfiles/rails_5.2.gemfile rename to gemfiles/rails_7.1.gemfile index 6a66f94..331319d 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_7.1.gemfile @@ -2,7 +2,10 @@ source "http://rubygems.org" -gem "rails", "~> 5.2" +gem "rails", "~> 7.1" +gem "rspec", "~> 3.12" +gem "rspec-rails", "~> 6.1" gem "rails-controller-testing" +gem "sqlite3", "~> 1.4" gemspec path: "../" diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile new file mode 100644 index 0000000..786aa5c --- /dev/null +++ b/gemfiles/rails_8.0.gemfile @@ -0,0 +1,11 @@ +# This file was generated by Appraisal + +source "http://rubygems.org" + +gem "rails", "~> 8.0" +gem "rspec", "~> 3.13" +gem "rspec-rails", "~> 8.0" +gem "rails-controller-testing" +gem "sqlite3", "~> 2.1" + +gemspec path: "../" diff --git a/lib/typical_situation/version.rb b/lib/typical_situation/version.rb index 287744e..258466a 100644 --- a/lib/typical_situation/version.rb +++ b/lib/typical_situation/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TypicalSituation - VERSION = '0.11.2' + VERSION = '1.0.0' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f326674..1cec1de 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,11 +14,6 @@ Dir[File.dirname(__FILE__) + '/factories/*.rb'].each { |f| require f } RSpec.configure do |config| - if Rails::VERSION::MAJOR < 5 - require 'rails/forward_compatible_controller_tests' - config.include Rails::ForwardCompatibleControllerTests, type: :controller - end - config.include FactoryBot::Syntax::Methods # Enable flags like --only-failures and --next-failure diff --git a/typical_situation.gemspec b/typical_situation.gemspec index 06a22a8..75b7d49 100644 --- a/typical_situation.gemspec +++ b/typical_situation.gemspec @@ -18,15 +18,17 @@ Gem::Specification.new do |s| s.files = Dir['{app,config,db,lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md'] s.test_files = Dir['test/**/*'] - s.add_runtime_dependency 'rails', '>= 4.0.0' + s.required_ruby_version = '>= 3.0.0' + + s.add_runtime_dependency 'rails', '>= 7.0.0' s.add_development_dependency 'appraisal' - s.add_development_dependency 'bundler', '~> 1.0' + s.add_development_dependency 'bundler', '>= 2.2.0' s.add_development_dependency 'byebug' s.add_development_dependency 'combustion' s.add_development_dependency 'coveralls' s.add_development_dependency 'factory_bot_rails' s.add_development_dependency 'rake' - s.add_development_dependency 'rspec-rails' - s.add_development_dependency 'sqlite3', '~> 1.3.6' + s.add_development_dependency 'rspec-rails', '>= 6.0' + s.add_development_dependency 'sqlite3', '>= 1.4' end From f71dfe946dff2d2ef3273b1a094d33c4c9356efb Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Mon, 6 Oct 2025 21:01:16 -0400 Subject: [PATCH 02/17] Add new syntax for declaring typical_situation --- Gemfile.lock | 2 +- README.md | 65 +++++++++- lib/typical_situation.rb | 21 ++++ lib/typical_situation/version.rb | 2 +- .../controllers/mock_apple_pies_controller.rb | 7 +- spec/internal/app/models/test_model.rb | 7 ++ spec/internal/db/schema.rb | 5 + spec/typical_situation_syntax_spec.rb | 118 ++++++++++++++++++ 8 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 spec/internal/app/models/test_model.rb create mode 100644 spec/typical_situation_syntax_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 40af1f4..17a74c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - typical_situation (1.0.0) + typical_situation (1.1.0) rails (>= 7.0.0) GEM diff --git a/README.md b/README.md index e9b6810..3643187 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,29 @@ Add to your **Gemfile**: ## Usage -### Define three methods +### Define your model and methods + +**Modern syntax (recommended):** + + class MockApplePiesController < ApplicationController + include TypicalSituation + + typical_situation :mock_apple_pie + + private + + # The collection of model instances. + def collection + current_user.mock_apple_pies + end + + # Find a model instance by ID. + def find_in_collection(id) + collection.find_by_id(id) + end + end + +**Legacy syntax (still supported):** class MockApplePiesController < ApplicationController include TypicalSituation @@ -37,6 +59,8 @@ Add to your **Gemfile**: :mock_apple_pie end + private + # The collection of model instances. def collection current_user.mock_apple_pies @@ -48,6 +72,14 @@ Add to your **Gemfile**: end end +### Syntax Options + +**`typical_situation` class method** - The recommended modern syntax that provides a clean, Rails-like declarative style. + +**`model_type` instance method** - The original syntax that's still fully supported for backward compatibility. + +Both syntaxes are functionally identical and can be used interchangeably. The `typical_situation` method is simply syntactic sugar that defines the `model_type` method under the hood. + ### Get a fully functional REST API The seven standard resourceful actions: @@ -158,6 +190,37 @@ Like `Blueprinter`, end end +##### Alba + +[Alba](https://github.com/okuramasafumi/alba) is a fast, modern JSON serializer. Like `Blueprinter` and `Fast JSON API`, it's best suited to being overridden at the controller level: + + class MockApplePieResource + include Alba::Resource + + attributes :id, :ingredients + + association :grandma, resource: GrandmaResource + end + + class MockApplePiesController < ApplicationController + include TypicalSituation + typical_situation :mock_apple_pie + + private + + def serializable_resource(resource) + MockApplePieResource.new(resource).serialize + end + + def collection + current_user.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + ## Legalese This project uses MIT-LICENSE. diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index d45f6ef..7f1efaa 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -13,6 +13,27 @@ module TypicalSituation def self.included(base) add_rescues(base) + base.extend(ClassMethods) + end + + module ClassMethods + # Syntactic sugar for defining model_type + # + # Example: + # class PostsController < ApplicationController + # include TypicalSituation + # typical_situation :post + # end + # + # This is equivalent to: + # def model_type + # :post + # end + def typical_situation(model_type_symbol) + define_method :model_type do + model_type_symbol + end + end end def self.add_rescues(action_controller) diff --git a/lib/typical_situation/version.rb b/lib/typical_situation/version.rb index 258466a..fb54104 100644 --- a/lib/typical_situation/version.rb +++ b/lib/typical_situation/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TypicalSituation - VERSION = '1.0.0' + VERSION = '1.1.0' end diff --git a/spec/internal/app/controllers/mock_apple_pies_controller.rb b/spec/internal/app/controllers/mock_apple_pies_controller.rb index c31bd72..1a046be 100644 --- a/spec/internal/app/controllers/mock_apple_pies_controller.rb +++ b/spec/internal/app/controllers/mock_apple_pies_controller.rb @@ -3,12 +3,11 @@ class MockApplePiesController < ApplicationController include TypicalSituation + typical_situation :mock_apple_pie + attr_accessor :current_grandma - # Symbolized, underscored version of the model (class) to use. - def model_type - :mock_apple_pie - end + private # The collection of model instances. def collection diff --git a/spec/internal/app/models/test_model.rb b/spec/internal/app/models/test_model.rb new file mode 100644 index 0000000..e2e2da4 --- /dev/null +++ b/spec/internal/app/models/test_model.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TestModel < ActiveRecord::Base + belongs_to :grandma + + validates :name, presence: true +end \ No newline at end of file diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb index ba04dd5..3b66326 100644 --- a/spec/internal/db/schema.rb +++ b/spec/internal/db/schema.rb @@ -11,4 +11,9 @@ t.integer 'grandma_id' t.string 'ingredients' end + + create_table 'test_models', force: true do |t| + t.integer 'grandma_id' + t.string 'name' + end end diff --git a/spec/typical_situation_syntax_spec.rb b/spec/typical_situation_syntax_spec.rb new file mode 100644 index 0000000..0c75f2f --- /dev/null +++ b/spec/typical_situation_syntax_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'TypicalSituation syntax sugar' do + describe 'typical_situation class method' do + let(:controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + typical_situation :test_model + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:controller) { controller_class.new } + let(:grandma) { create(:grandma) } + + before do + controller.current_grandma = grandma + end + + it 'defines model_type method' do + expect(controller.model_type).to eq(:test_model) + end + + it 'works with all typical situation functionality' do + expect(controller.model_class).to eq(TestModel) + end + + it 'works with plural_model_type' do + expect(controller.plural_model_type).to eq(:test_models) + end + + it 'works with model_params' do + # Mock params + allow(controller).to receive(:params) do + ActionController::Parameters.new( + test_model: { name: 'Test' } + ) + end + + expect(controller.model_params).to be_a(ActionController::Parameters) + expect(controller.model_params[:name]).to eq('Test') + end + end + + describe 'backward compatibility' do + let(:old_syntax_controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + + def model_type + :legacy_model + end + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:new_syntax_controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + typical_situation :legacy_model + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + it 'produces identical behavior between old and new syntax' do + old_controller = old_syntax_controller_class.new + new_controller = new_syntax_controller_class.new + + expect(old_controller.model_type).to eq(new_controller.model_type) + expect(old_controller.plural_model_type).to eq(new_controller.plural_model_type) + end + end + + describe 'class method availability' do + it 'adds typical_situation class method when module is included' do + controller_class = Class.new(ApplicationController) do + include TypicalSituation + end + + expect(controller_class).to respond_to(:typical_situation) + end + + it 'does not add class method to classes that do not include TypicalSituation' do + controller_class = Class.new(ApplicationController) + + expect(controller_class).not_to respond_to(:typical_situation) + end + end +end \ No newline at end of file From 559e890fa2e02a8d262049bb5be6bf779f123a03 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Mon, 6 Oct 2025 21:37:19 -0400 Subject: [PATCH 03/17] Extend functionality, format --- .github/workflows/publish.yml | 36 ++++ .standard.yml | 12 ++ Appraisals | 36 ++-- Gemfile | 2 +- Gemfile.lock | 58 ++++- README.md | 69 ++++++ Rakefile | 12 +- config.ru | 4 +- lib/typical_situation.rb | 10 +- lib/typical_situation/identity.rb | 6 +- lib/typical_situation/operations.rb | 41 +++- lib/typical_situation/responses.rb | 26 ++- lib/typical_situation/version.rb | 2 +- pkg/typical_situation-1.1.0.gem | Bin 0 -> 9216 bytes .../mock_apple_pies_controller_spec.rb | 200 +++++++++++------- spec/factories/grandma.rb | 2 +- spec/factories/mock_apple_pie.rb | 2 +- spec/spec_helper.rb | 12 +- spec/typical_situation_spec.rb | 4 +- spec/typical_situation_syntax_spec.rb | 32 +-- typical_situation.gemspec | 45 ++-- 21 files changed, 439 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .standard.yml create mode 100644 pkg/typical_situation-1.1.0.gem diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e61d7d3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +name: Publish Gem + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: true + - name: Release Gem + if: contains(github.ref, 'refs/tags/v') + env: + RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}} + TAG: ${{ github.event.release.tag_name }} + run: | + echo "Setting up gem credentials..." + mkdir -p ~/.gem + + cat << EOF > ~/.gem/credentials + --- + :rubygems_api_key: ${RUBYGEMS_API_KEY} + EOF + + chmod 0600 ~/.gem/credentials + + bundle exec rake build + + echo "Running gem release task..." + gem push pkg/typical_situation-${TAG#v}.gem \ No newline at end of file diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..fe232bc --- /dev/null +++ b/.standard.yml @@ -0,0 +1,12 @@ +# Standard Ruby configuration +# https://github.com/testdouble/standard + +ruby_version: 3.0 + +ignore: + - 'spec/internal/**/*' + - 'pkg/**/*' + - 'vendor/**/*' + - 'tmp/**/*' + +fix: true \ No newline at end of file diff --git a/Appraisals b/Appraisals index d0be6a8..c2be193 100644 --- a/Appraisals +++ b/Appraisals @@ -1,25 +1,25 @@ # frozen_string_literal: true -appraise 'rails_7.0' do - gem 'rails', '~> 7.0' - gem 'rspec', '~> 3.12' - gem 'rspec-rails', '~> 6.0' - gem 'rails-controller-testing' - gem 'sqlite3', '~> 1.4' +appraise "rails_7.0" do + gem "rails", "~> 7.0" + gem "rspec", "~> 3.12" + gem "rspec-rails", "~> 6.0" + gem "rails-controller-testing" + gem "sqlite3", "~> 1.4" end -appraise 'rails_7.1' do - gem 'rails', '~> 7.1' - gem 'rspec', '~> 3.12' - gem 'rspec-rails', '~> 6.1' - gem 'rails-controller-testing' - gem 'sqlite3', '~> 1.4' +appraise "rails_7.1" do + gem "rails", "~> 7.1" + gem "rspec", "~> 3.12" + gem "rspec-rails", "~> 6.1" + gem "rails-controller-testing" + gem "sqlite3", "~> 1.4" end -appraise 'rails_8.0' do - gem 'rails', '~> 8.0' - gem 'rspec', '~> 3.13' - gem 'rspec-rails', '~> 8.0' - gem 'rails-controller-testing' - gem 'sqlite3', '~> 2.1' +appraise "rails_8.0" do + gem "rails", "~> 8.0" + gem "rspec", "~> 3.13" + gem "rspec-rails", "~> 8.0" + gem "rails-controller-testing" + gem "sqlite3", "~> 2.1" end diff --git a/Gemfile b/Gemfile index 94b8df6..3d3754b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -source 'http://rubygems.org' +source "http://rubygems.org" # Declare your gem's dependencies in typical_situation.gemspec. # Bundler will treat runtime dependencies like base dependencies, and diff --git a/Gemfile.lock b/Gemfile.lock index 17a74c5..a23242a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,6 +82,7 @@ GEM bundler rake thor (>= 0.14.0) + ast (2.4.3) base64 (0.3.0) benchmark (0.4.1) bigdecimal (3.2.3) @@ -121,6 +122,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.15.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) @@ -132,7 +135,6 @@ GEM net-smtp marcel (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (5.25.5) mize (0.6.1) net-imap (0.5.12) @@ -145,14 +147,16 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.10) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc pp (0.6.3) prettyprint prettyprint (0.2.0) + prism (1.5.1) psych (5.2.6) date stringio @@ -179,6 +183,10 @@ GEM activesupport (= 8.0.3) bundler (>= 1.15.0) railties (= 8.0.3) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -195,11 +203,13 @@ GEM thor (~> 1.0, >= 1.2.2) tsort (>= 0.2) zeitwerk (~> 2.6) + rainbow (3.1.1) rake (13.3.0) rdoc (6.15.0) erb psych (>= 4.0.0) tsort + regexp_parser (2.11.3) reline (0.6.2) io-console (~> 0.5) rspec-core (3.13.5) @@ -219,14 +229,44 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.6) + rubocop (1.80.2) + json (~> 2.3) + 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.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (1.13.0) securerandom (0.4.1) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sqlite3 (1.7.3) - mini_portile2 (~> 2.8.0) + sqlite3 (2.7.4-arm64-darwin) + standard (1.51.1) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.80.2) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.8.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.25.0) stringio (3.1.7) sync (0.5.0) term-ansicolor (1.11.3) @@ -240,6 +280,9 @@ GEM tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) uri (1.0.3) useragent (0.16.11) websocket-driver (0.8.0) @@ -250,7 +293,6 @@ GEM PLATFORMS arm64-darwin-20 - ruby DEPENDENCIES appraisal @@ -259,9 +301,11 @@ DEPENDENCIES combustion coveralls factory_bot_rails + rails-controller-testing rake rspec-rails (>= 6.0) sqlite3 (>= 1.4) + standard typical_situation! BUNDLED WITH diff --git a/README.md b/README.md index 3643187..d088dd8 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,75 @@ The library is split into modules: - [operations](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/operations.rb) - loading, changing, & persisting the model - [responses](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/responses.rb) - HTTP responses & redirects +#### Common Customization Hooks + +**Scoped Collections** - Filter the collection based on user permissions or other criteria: + +```ruby +def scoped_resource + if current_user.admin? + collection + else + collection.where(published: true) + end +end +``` + +**Custom Lookup** - Use different attributes for finding resources: + +```ruby +def find_resource(param) + collection.find_by!(slug: param) +end +``` + +**Custom Redirects** - Control where users go after actions: + +```ruby +def after_resource_created_path(resource) + { action: :index } +end + +def after_resource_updated_path(resource) + edit_resource_path(resource) +end + +def after_resource_destroyed_path(resource) + { action: :index } +end +``` + +**Sorting** - Set default sorting for index pages: + +```ruby +def default_sorting_attribute + :created_at +end + +def default_sorting_direction + :desc +end +``` + +**Pagination** - Bring your own pagination solution: + +```ruby +# Kaminari +def paginate_resources(resources) + resources.page(params[:page]).per(params[:per_page] || 25) +end + +# will_paginate +def paginate_resources(resources) + resources.paginate(page: params[:page], per_page: params[:per_page] || 25) +end + +# Custom pagination +def paginate_resources(resources) + resources.limit(20).offset((params[:page].to_i - 1) * 20) +end +``` + #### Serialization Under the hood `TypicalSituation` calls `to_json` on your `ActiveRecord` models. This isn't always the optimal way to serialize resources, though, and so `TypicalSituation` offers a simple means of overriding the base Serialization --- either on an individual controller, or for your entire application. diff --git a/Rakefile b/Rakefile index 82bb534..faea475 100755 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,14 @@ # frozen_string_literal: true -require 'bundler/gem_tasks' -require 'rspec/core/rake_task' +require "bundler/gem_tasks" +require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) -task default: :spec +begin + require "standard/rake" +rescue LoadError + # Standard not available +end + +task default: [:standard, :spec] diff --git a/config.ru b/config.ru index e3007f5..1265db9 100644 --- a/config.ru +++ b/config.ru @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rubygems' -require 'bundler' +require "rubygems" +require "bundler" Bundler.require :default, :development diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index 7f1efaa..119d54c 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'typical_situation/identity' -require 'typical_situation/actions' -require 'typical_situation/operations' -require 'typical_situation/responses' +require "typical_situation/identity" +require "typical_situation/actions" +require "typical_situation/operations" +require "typical_situation/responses" module TypicalSituation include Identity @@ -18,7 +18,7 @@ def self.included(base) module ClassMethods # Syntactic sugar for defining model_type - # + # # Example: # class PostsController < ApplicationController # include TypicalSituation diff --git a/lib/typical_situation/identity.rb b/lib/typical_situation/identity.rb index 803aacc..115c867 100644 --- a/lib/typical_situation/identity.rb +++ b/lib/typical_situation/identity.rb @@ -5,7 +5,7 @@ module TypicalSituation module Identity # Symbolized, underscored version of the model (class) to use. def model_type - raise(NotImplementedError, '#model_type must be defined in the TypicalSituation implementation.') + raise(NotImplementedError, "#model_type must be defined in the TypicalSituation implementation.") end def model_params @@ -38,12 +38,12 @@ def permitted_update_params # The collection of model instances. def collection - raise(NotImplementedError, '#collection must be defined in the TypicalSituation implementation.') + raise(NotImplementedError, "#collection must be defined in the TypicalSituation implementation.") end # Find a model instance by ID. def find_in_collection(_id) - raise(NotImplementedError, '#find_in_collection must be defined in the TypicalSituation implementation.') + raise(NotImplementedError, "#find_in_collection must be defined in the TypicalSituation implementation.") end def include_root? diff --git a/lib/typical_situation/operations.rb b/lib/typical_situation/operations.rb index 448bb66..41601af 100644 --- a/lib/typical_situation/operations.rb +++ b/lib/typical_situation/operations.rb @@ -4,8 +4,32 @@ module TypicalSituation # Model operations. # Assume that we're working w/ an ActiveRecord association collection. module Operations + def scoped_resource + collection + end + + def find_resource(param) + find_in_collection(param) + end + + def default_sorting_attribute + nil + end + + def default_sorting_direction + :asc + end + + def paginate_resources(resources) + resources + end + + def pagination_params + params.permit(:page, :per_page) + end + def get_resource - if (@resource = find_in_collection(params[:id])) + if (@resource = find_resource(params[:id])) set_single_instance @resource else @@ -18,7 +42,7 @@ def has_errors? end def get_resources - @resources = collection + @resources = paginate_resources(apply_sorting(scoped_resource)) set_collection_instance @resources end @@ -59,7 +83,7 @@ def serialize_resource(resource, options = {}) def serialize_resources(resources) if include_root? - return { plural_model_type => serializable_resource(resources) } + return {plural_model_type => serializable_resource(resources)} end serializable_resource(resources).to_json(root: false) @@ -72,13 +96,20 @@ def serializable_resource(resource) # Set the singular instance variable named after the model. Modules are delimited with "_". # Example: a MockApplePie resource is set to ivar @mock_apple_pie. def set_single_instance - instance_variable_set(:"@#{model_type.to_s.gsub('/', '__')}", @resource) + instance_variable_set(:"@#{model_type.to_s.gsub("/", "__")}", @resource) end # Set the plural instance variable named after the model. Modules are delimited with "_". # Example: a MockApplePie resource collection is set to ivar @mock_apple_pies. def set_collection_instance - instance_variable_set(:"@#{model_type.to_s.gsub('/', '__').pluralize}", @resources) + instance_variable_set(:"@#{model_type.to_s.gsub("/", "__").pluralize}", @resources) + end + + private + + def apply_sorting(resources) + return resources unless default_sorting_attribute + resources.order(default_sorting_attribute => default_sorting_direction) end end end diff --git a/lib/typical_situation/responses.rb b/lib/typical_situation/responses.rb index e866ccf..8003151 100644 --- a/lib/typical_situation/responses.rb +++ b/lib/typical_situation/responses.rb @@ -68,8 +68,8 @@ def respond_as_created end format.json do render json: serialize_resource(@resource), - location: location_url, - status: :created + location: location_url, + status: :created end end end @@ -82,11 +82,11 @@ def respond_as_error format.html do set_single_instance render action: (@resource.new_record? ? :new : :edit), - status: :unprocessable_entity + status: :unprocessable_entity end format.json do render json: serialize_resource(@resource, methods: [:errors]), - status: :unprocessable_entity + status: :unprocessable_entity end end end @@ -114,7 +114,7 @@ def respond_as_not_found yield(format) if block_given? format.html do - raise ActionController::RoutingError, 'Not Found' + raise ActionController::RoutingError, "Not Found" end format.json do head :not_found @@ -122,15 +122,27 @@ def respond_as_not_found end end + def after_resource_created_path(resource) + {action: :show, id: resource.id} + end + + def after_resource_updated_path(resource) + {action: :show, id: resource.id} + end + + def after_resource_destroyed_path(resource) + {action: :index} + end + # HTML response when @resource saved or updated. def changed_so_redirect - redirect_to action: :show, id: @resource.to_param + redirect_to after_resource_updated_path(@resource) true # return true when redirecting end # HTML response when @resource deleted. def gone_so_redirect - redirect_to action: :index + redirect_to after_resource_destroyed_path(@resource) true # return true when redirecting end end diff --git a/lib/typical_situation/version.rb b/lib/typical_situation/version.rb index fb54104..1fe4a24 100644 --- a/lib/typical_situation/version.rb +++ b/lib/typical_situation/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TypicalSituation - VERSION = '1.1.0' + VERSION = "1.1.0" end diff --git a/pkg/typical_situation-1.1.0.gem b/pkg/typical_situation-1.1.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..00d7980fc9a038171fee2ad2bc0d0539b8d43c22 GIT binary patch literal 9216 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 literal 0 HcmV?d00001 diff --git a/spec/controllers/mock_apple_pies_controller_spec.rb b/spec/controllers/mock_apple_pies_controller_spec.rb index 532d59e..4035580 100644 --- a/spec/controllers/mock_apple_pies_controller_spec.rb +++ b/spec/controllers/mock_apple_pies_controller_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" PIES_COUNT = 5 @@ -12,9 +12,9 @@ let(:pie) { @grandma.mock_apple_pies.first } - describe 'GET #index' do - context 'html' do - it 'renders the index template' do + describe "GET #index" do + context "html" do + it "renders the index template" do get :index expect(response).to have_http_status :ok @@ -24,29 +24,29 @@ end end - context 'json' do - it 'renders index JSON' do + context "json" do + it "renders index JSON" do get :index, format: :json response_body = JSON.parse(response.body) expect(response).to have_http_status :ok expect(response_body).to be_a Hash - expect(response_body['mock_apple_pies']).to be_a Array - expect(response_body['mock_apple_pies'].size).to eq PIES_COUNT + expect(response_body["mock_apple_pies"]).to be_a Array + expect(response_body["mock_apple_pies"].size).to eq PIES_COUNT - response_body['mock_apple_pies'].each do |pie| - expect(@grandma.id).to eq pie['grandma_id'] - expect(pie['ingredients']).not_to be nil + response_body["mock_apple_pies"].each do |pie| + expect(@grandma.id).to eq pie["grandma_id"] + expect(pie["ingredients"]).not_to be nil end end end end - describe 'GET #show' do - context 'html' do - it 'renders the show template' do - get :show, params: { id: pie.to_param } + describe "GET #show" do + context "html" do + it "renders the show template" do + get :show, params: {id: pie.to_param} expect(response).to have_http_status :ok expect(response).to render_template(:show) @@ -55,32 +55,32 @@ expect(assigns(:mock_apple_pie)).to be_a MockApplePie end - it 'renders not_found' do - expect { get :show, params: { id: 555 } }.to raise_error(ActionController::RoutingError) + it "renders not_found" do + expect { get :show, params: {id: 555} }.to raise_error(ActionController::RoutingError) end end - context 'json' do - it 'renders show JSON' do - get :show, params: { id: pie.to_param }, format: :json + context "json" do + it "renders show JSON" do + get :show, params: {id: pie.to_param}, format: :json response_body = JSON.parse(response.body) expect(response).to have_http_status :ok expect(response_body).to be_a Hash - expect(response_body['mock_apple_pie']).to be_a Hash - expect(response_body['mock_apple_pie']['grandma_id']).to eq @grandma.id + expect(response_body["mock_apple_pie"]).to be_a Hash + expect(response_body["mock_apple_pie"]["grandma_id"]).to eq @grandma.id end - it 'renders not_found' do - get :show, params: { id: 555 }, format: :json + it "renders not_found" do + get :show, params: {id: 555}, format: :json expect(response).to have_http_status :not_found end end end - describe 'GET #new' do - it 'renders the new template' do + describe "GET #new" do + it "renders the new template" do get :new expect(response).to have_http_status :ok @@ -91,46 +91,46 @@ end end - describe 'POST #create' do - let(:new_attrs) { { mock_apple_pie: { ingredients: 'love', grandma_id: @grandma.id } } } - let(:bad_attrs) { { mock_apple_pie: { ingredients: '', grandma_id: @grandma.id } } } + describe "POST #create" do + let(:new_attrs) { {mock_apple_pie: {ingredients: "love", grandma_id: @grandma.id}} } + let(:bad_attrs) { {mock_apple_pie: {ingredients: "", grandma_id: @grandma.id}} } - context 'html' do - it 'redirects to show' do + context "html" do + it "redirects to show" do post :create, params: new_attrs pie = MockApplePie.all.last expect(response).to have_http_status :redirect expect(response).to redirect_to(action: :show, id: pie.id) end - it 'renders 422 for invalid args' do + it "renders 422 for invalid args" do post :create, params: bad_attrs expect(response).to have_http_status :unprocessable_entity end end - context 'json' do - it 'creates successfully' do + context "json" do + it "creates successfully" do post :create, params: new_attrs.merge(format: :json) response_body = JSON.parse(response.body) expect(response).to have_http_status :created expect(response_body).to be_a Hash - expect(response_body['mock_apple_pie']).to be_a Hash - expect(response_body['mock_apple_pie']['grandma_id']).to eq @grandma.id - expect(response_body['mock_apple_pie']['ingredients']).to eq new_attrs[:mock_apple_pie][:ingredients] + expect(response_body["mock_apple_pie"]).to be_a Hash + expect(response_body["mock_apple_pie"]["grandma_id"]).to eq @grandma.id + expect(response_body["mock_apple_pie"]["ingredients"]).to eq new_attrs[:mock_apple_pie][:ingredients] end - it 'renders 422 for invalid args' do + it "renders 422 for invalid args" do post :create, params: bad_attrs.merge(format: :json) expect(response).to have_http_status :unprocessable_entity end end end - describe 'GET #edit' do - it 'renders the new template' do - get :edit, params: { id: pie.to_param } + describe "GET #edit" do + it "renders the new template" do + get :edit, params: {id: pie.to_param} expect(response).to have_http_status :ok expect(response).to render_template(:edit) @@ -139,93 +139,147 @@ expect(assigns(:mock_apple_pie)).to be_a MockApplePie end - it 'renders not_found' do - expect { get :edit, params: { id: 555 } }.to raise_error(ActionController::RoutingError) + it "renders not_found" do + expect { get :edit, params: {id: 555} }.to raise_error(ActionController::RoutingError) end end - describe 'PUT #update' do - let(:update_attrs) { { mock_apple_pie: { ingredients: 'lots of love' } } } - let(:bad_attrs) { { mock_apple_pie: { ingredients: '' } } } + describe "PUT #update" do + let(:update_attrs) { {mock_apple_pie: {ingredients: "lots of love"}} } + let(:bad_attrs) { {mock_apple_pie: {ingredients: ""}} } - context 'html' do - it 'redirects to show' do + context "html" do + it "redirects to show" do put :update, params: update_attrs.merge(id: pie.to_param) expect(response).to have_http_status :redirect expect(response).to redirect_to(action: :show, id: pie.to_param) end - it 'renders not_found' do + it "renders not_found" do expect { put :update, params: update_attrs.merge(id: 555) }.to raise_error(ActionController::RoutingError) end - it 'renders unprocessable_entity' do + it "renders unprocessable_entity" do put :update, params: bad_attrs.merge(id: pie.to_param) expect(response).to have_http_status :unprocessable_entity end end - context 'json' do - it 'updates successfully' do + context "json" do + it "updates successfully" do put :update, params: update_attrs.merge(id: pie.to_param, format: :json) response_body = JSON.parse(response.body) expect(response).to have_http_status :ok expect(response_body).to be_a Hash - expect(response_body['mock_apple_pie']).to be_a Hash - expect(response_body['mock_apple_pie']['ingredients']).to eq update_attrs[:mock_apple_pie][:ingredients] + expect(response_body["mock_apple_pie"]).to be_a Hash + expect(response_body["mock_apple_pie"]["ingredients"]).to eq update_attrs[:mock_apple_pie][:ingredients] end - it 'renders not_found' do + it "renders not_found" do put :update, params: update_attrs.merge(id: 555, format: :json) expect(response).to have_http_status :not_found end - it 'renders unprocessable_entity' do + it "renders unprocessable_entity" do put :update, params: bad_attrs.merge(id: pie.to_param, format: :json) expect(response).to have_http_status :unprocessable_entity end end end - describe 'DELETE #destroy' do - context 'html' do - it 'redirects to index' do - delete :destroy, params: { id: pie.to_param } + describe "DELETE #destroy" do + context "html" do + it "redirects to index" do + delete :destroy, params: {id: pie.to_param} expect(response).to have_http_status :redirect expect(response).to redirect_to(action: :index) end - it 'renders not_found' do - expect { delete :destroy, params: { id: 555 } }.to raise_error(ActionController::RoutingError) + it "renders not_found" do + expect { delete :destroy, params: {id: 555} }.to raise_error(ActionController::RoutingError) end - it 'renders unprocessable_entity' do - pie.update_attribute(:ingredients, 'real apples') + it "renders unprocessable_entity" do + pie.update_attribute(:ingredients, "real apples") - delete :destroy, params: { id: pie.to_param } + delete :destroy, params: {id: pie.to_param} expect(response).to have_http_status :unprocessable_entity end end - context 'json' do - it 'deletes successfully' do - delete :destroy, params: { id: pie.to_param }, format: :json + context "json" do + it "deletes successfully" do + delete :destroy, params: {id: pie.to_param}, format: :json expect(response).to have_http_status :no_content expect(response.body).to be_empty end - it 'renders not_found' do - delete :destroy, params: { id: 555 }, format: :json + it "renders not_found" do + delete :destroy, params: {id: 555}, format: :json expect(response).to have_http_status :not_found end - it 'renders unprocessable_entity' do - pie.update_attribute(:ingredients, 'real apples') + it "renders unprocessable_entity" do + pie.update_attribute(:ingredients, "real apples") - delete :destroy, params: { id: pie.to_param }, format: :json + delete :destroy, params: {id: pie.to_param}, format: :json expect(response).to have_http_status :unprocessable_entity end end end + + describe "customization hooks" do + describe "default behavior" do + it "scoped_resource returns collection" do + expect(controller.scoped_resource).to eq(@grandma.mock_apple_pies) + end + + it "find_resource calls find_in_collection" do + result = controller.find_resource(pie.id) + expect(result).to eq(pie) + end + + it "default_sorting_attribute returns nil" do + expect(controller.default_sorting_attribute).to be_nil + end + + it "default_sorting_direction returns :asc" do + expect(controller.default_sorting_direction).to eq(:asc) + end + + it "paginate_resources returns unchanged resources" do + resources = @grandma.mock_apple_pies + expect(controller.paginate_resources(resources)).to eq(resources) + end + + it "after_resource_created_path returns show path" do + path = controller.after_resource_created_path(pie) + expect(path).to eq({action: :show, id: pie.id}) + end + + it "after_resource_updated_path returns show path" do + path = controller.after_resource_updated_path(pie) + expect(path).to eq({action: :show, id: pie.id}) + end + + it "after_resource_destroyed_path returns index path" do + path = controller.after_resource_destroyed_path(pie) + expect(path).to eq({action: :index}) + end + end + + describe "pagination_params" do + it "permits page and per_page params" do + allow(controller).to receive(:params).and_return( + ActionController::Parameters.new(page: "2", per_page: "10", other: "ignored") + ) + + permitted = controller.pagination_params + expect(permitted[:page]).to eq("2") + expect(permitted[:per_page]).to eq("10") + expect(permitted[:other]).to be_nil + end + end + end end diff --git a/spec/factories/grandma.rb b/spec/factories/grandma.rb index 9a7eb54..a2ee463 100644 --- a/spec/factories/grandma.rb +++ b/spec/factories/grandma.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :grandma do - name { 'Annie' } + name { "Annie" } transient do pies_count { 5 } diff --git a/spec/factories/mock_apple_pie.rb b/spec/factories/mock_apple_pie.rb index b0d21a4..cc29997 100644 --- a/spec/factories/mock_apple_pie.rb +++ b/spec/factories/mock_apple_pie.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :mock_apple_pie do - ingredients { 'flour, sugar, water, butter, eggs, milk, Ritz crackers, lemon, vanilla, cinnamon' } + ingredients { "flour, sugar, water, butter, eggs, milk, Ritz crackers, lemon, vanilla, cinnamon" } grandma end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1cec1de..ad9f40a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true -require 'bundler' +require "bundler" Bundler.require :default, :development # If you're using all parts of Rails: Combustion.initialize! :all -require 'rspec/rails' -require 'typical_situation' -require 'factory_bot_rails' +require "rspec/rails" +require "typical_situation" +require "factory_bot_rails" -Dir[File.dirname(__FILE__) + '/factories/*.rb'].each { |f| require f } +Dir[File.dirname(__FILE__) + "/factories/*.rb"].each { |f| require f } RSpec.configure do |config| config.include FactoryBot::Syntax::Methods # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' + config.example_status_persistence_file_path = ".rspec_status" # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! diff --git a/spec/typical_situation_spec.rb b/spec/typical_situation_spec.rb index 9d0af36..67d766c 100644 --- a/spec/typical_situation_spec.rb +++ b/spec/typical_situation_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" RSpec.describe TypicalSituation do - it 'has a version number' do + it "has a version number" do expect(TypicalSituation::VERSION).not_to be nil end end diff --git a/spec/typical_situation_syntax_spec.rb b/spec/typical_situation_syntax_spec.rb index 0c75f2f..16d562b 100644 --- a/spec/typical_situation_syntax_spec.rb +++ b/spec/typical_situation_syntax_spec.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" -RSpec.describe 'TypicalSituation syntax sugar' do - describe 'typical_situation class method' do +RSpec.describe "TypicalSituation syntax sugar" do + describe "typical_situation class method" do let(:controller_class) do Class.new(ApplicationController) do include TypicalSituation + typical_situation :test_model attr_accessor :current_grandma @@ -28,32 +29,32 @@ def find_in_collection(id) controller.current_grandma = grandma end - it 'defines model_type method' do + it "defines model_type method" do expect(controller.model_type).to eq(:test_model) end - it 'works with all typical situation functionality' do + it "works with all typical situation functionality" do expect(controller.model_class).to eq(TestModel) end - it 'works with plural_model_type' do + it "works with plural_model_type" do expect(controller.plural_model_type).to eq(:test_models) end - it 'works with model_params' do + it "works with model_params" do # Mock params allow(controller).to receive(:params) do ActionController::Parameters.new( - test_model: { name: 'Test' } + test_model: {name: "Test"} ) end expect(controller.model_params).to be_a(ActionController::Parameters) - expect(controller.model_params[:name]).to eq('Test') + expect(controller.model_params[:name]).to eq("Test") end end - describe 'backward compatibility' do + describe "backward compatibility" do let(:old_syntax_controller_class) do Class.new(ApplicationController) do include TypicalSituation @@ -77,6 +78,7 @@ def find_in_collection(id) let(:new_syntax_controller_class) do Class.new(ApplicationController) do include TypicalSituation + typical_situation :legacy_model attr_accessor :current_grandma @@ -91,7 +93,7 @@ def find_in_collection(id) end end - it 'produces identical behavior between old and new syntax' do + it "produces identical behavior between old and new syntax" do old_controller = old_syntax_controller_class.new new_controller = new_syntax_controller_class.new @@ -100,8 +102,8 @@ def find_in_collection(id) end end - describe 'class method availability' do - it 'adds typical_situation class method when module is included' do + describe "class method availability" do + it "adds typical_situation class method when module is included" do controller_class = Class.new(ApplicationController) do include TypicalSituation end @@ -109,10 +111,10 @@ def find_in_collection(id) expect(controller_class).to respond_to(:typical_situation) end - it 'does not add class method to classes that do not include TypicalSituation' do + it "does not add class method to classes that do not include TypicalSituation" do controller_class = Class.new(ApplicationController) expect(controller_class).not_to respond_to(:typical_situation) end end -end \ No newline at end of file +end diff --git a/typical_situation.gemspec b/typical_situation.gemspec index 75b7d49..9ccf90f 100644 --- a/typical_situation.gemspec +++ b/typical_situation.gemspec @@ -1,34 +1,35 @@ # frozen_string_literal: true -$LOAD_PATH.push File.expand_path('lib', __dir__) +$LOAD_PATH.push File.expand_path("lib", __dir__) # Maintain your gem's version: -require 'typical_situation/version' +require "typical_situation/version" # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = 'typical_situation' - s.version = TypicalSituation::VERSION - s.authors = ['Mars Hall', 'Wyatt Kirby'] - s.email = ['m@marsorange.com', 'wyatt@apsis.io'] - s.homepage = 'https://github.com/mars/typical_situation' - s.summary = 'The missing Rails ActionController REST API mixin.' - s.description = 'A module providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection.' + s.name = "typical_situation" + s.version = TypicalSituation::VERSION + s.authors = ["Mars Hall", "Wyatt Kirby"] + s.email = ["m@marsorange.com", "wyatt@apsis.io"] + s.homepage = "https://github.com/mars/typical_situation" + s.summary = "The missing Rails ActionController REST API mixin." + s.description = "A module providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection." - s.files = Dir['{app,config,db,lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md'] - s.test_files = Dir['test/**/*'] + s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] - s.required_ruby_version = '>= 3.0.0' + s.required_ruby_version = ">= 3.0.0" - s.add_runtime_dependency 'rails', '>= 7.0.0' + s.add_runtime_dependency "rails", ">= 7.0.0" - s.add_development_dependency 'appraisal' - s.add_development_dependency 'bundler', '>= 2.2.0' - s.add_development_dependency 'byebug' - s.add_development_dependency 'combustion' - s.add_development_dependency 'coveralls' - s.add_development_dependency 'factory_bot_rails' - s.add_development_dependency 'rake' - s.add_development_dependency 'rspec-rails', '>= 6.0' - s.add_development_dependency 'sqlite3', '>= 1.4' + s.add_development_dependency "appraisal" + s.add_development_dependency "bundler", ">= 2.2.0" + s.add_development_dependency "byebug" + s.add_development_dependency "combustion" + s.add_development_dependency "coveralls" + s.add_development_dependency "factory_bot_rails" + s.add_development_dependency "rails-controller-testing" + s.add_development_dependency "rake" + s.add_development_dependency "rspec-rails", ">= 6.0" + s.add_development_dependency "sqlite3", ">= 1.4" + s.add_development_dependency "standard" end From 929d66a4b3a772b92354dd9132d2364ceb4861e8 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Mon, 6 Oct 2025 21:40:05 -0400 Subject: [PATCH 04/17] Add lint job --- .github/workflows/ci.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0976967..dc5e019 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,21 @@ -name: Spec CI +name: CI on: [push, pull_request] jobs: + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true + - name: Run Standard RB + run: bundle exec standardrb + build: runs-on: ubuntu-latest name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} From 6c4d913017d658e4fc77a44daa1a1763a31ec3f4 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 08:17:01 -0400 Subject: [PATCH 05/17] Add permission hooks and documentation --- README.md | 36 +++++++++ lib/typical_situation.rb | 2 + lib/typical_situation/actions.rb | 7 ++ lib/typical_situation/permissions.rb | 16 ++++ pkg/typical_situation-1.1.0.gem | Bin 9216 -> 0 bytes spec/typical_situation_permissions_spec.rb | 90 +++++++++++++++++++++ 6 files changed, 151 insertions(+) create mode 100644 lib/typical_situation/permissions.rb delete mode 100644 pkg/typical_situation-1.1.0.gem create mode 100644 spec/typical_situation_permissions_spec.rb diff --git a/README.md b/README.md index d088dd8..3b30344 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,42 @@ def paginate_resources(resources) end ``` +#### Authorization + +Control access to resources by overriding the `authorized?` method: + +```ruby +class PostsController < ApplicationController + include TypicalSituation + typical_situation :post + + private + + def authorized?(action, resource = nil) + case action + when :destroy, :update, :edit + resource&.user == current_user || current_user&.admin? + when :show + resource&.published? || resource&.user == current_user + else + true + end + end +end +``` + +**CanCanCan**: `can?(action, resource || model_class)` + +**Pundit**: `policy(resource || model_class).public_send("#{action}?")` + +**Custom responses**: + +```ruby +def respond_as_forbidden + redirect_to login_path, alert: "Access denied" +end +``` + #### Serialization Under the hood `TypicalSituation` calls `to_json` on your `ActiveRecord` models. This isn't always the optimal way to serialize resources, though, and so `TypicalSituation` offers a simple means of overriding the base Serialization --- either on an individual controller, or for your entire application. diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index 119d54c..95d0e89 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true require "typical_situation/identity" +require "typical_situation/permissions" require "typical_situation/actions" require "typical_situation/operations" require "typical_situation/responses" module TypicalSituation include Identity + include Permissions include Actions include Operations include Responses diff --git a/lib/typical_situation/actions.rb b/lib/typical_situation/actions.rb index 0108b65..23f5c5a 100644 --- a/lib/typical_situation/actions.rb +++ b/lib/typical_situation/actions.rb @@ -4,38 +4,45 @@ module TypicalSituation # Standard REST/CRUD actions. module Actions def index + respond_as_forbidden unless authorized?(:index) get_resources respond_with_resources end def show get_resource + respond_as_forbidden unless authorized?(:show, @resource) respond_with_resource end def edit get_resource + respond_as_forbidden unless authorized?(:edit, @resource) respond_with_resource end def new + respond_as_forbidden unless authorized?(:new) new_resource respond_with_resource end def update get_resource + respond_as_forbidden unless authorized?(:update, @resource) update_resource(@resource, update_params) respond_as_changed end def destroy get_resource + respond_as_forbidden unless authorized?(:destroy, @resource) destroy_resource(@resource) respond_as_gone end def create + respond_as_forbidden unless authorized?(:create) @resource = create_resource(create_params) respond_as_created end diff --git a/lib/typical_situation/permissions.rb b/lib/typical_situation/permissions.rb new file mode 100644 index 0000000..a28a5c1 --- /dev/null +++ b/lib/typical_situation/permissions.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module TypicalSituation + module Permissions + def authorized?(_action, _resource = nil) + true + end + + def respond_as_forbidden + respond_to do |format| + format.html { render plain: "Forbidden", status: :forbidden } + format.json { head :forbidden } + end + end + end +end diff --git a/pkg/typical_situation-1.1.0.gem b/pkg/typical_situation-1.1.0.gem deleted file mode 100644 index 00d7980fc9a038171fee2ad2bc0d0539b8d43c22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9216 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 diff --git a/spec/typical_situation_permissions_spec.rb b/spec/typical_situation_permissions_spec.rb new file mode 100644 index 0000000..b7cf9e3 --- /dev/null +++ b/spec/typical_situation_permissions_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "spec_helper" + +PIES_COUNT = 5 + +RSpec.describe MockApplePiesController, type: :controller do + before(:each) do + @grandma = create(:grandma, pies_count: PIES_COUNT) + controller.current_grandma = @grandma + end + + let(:pie) { @grandma.mock_apple_pies.first } + + describe "authorization" do + describe "default behavior" do + it "authorized? returns true by default" do + expect(controller.authorized?(:show, pie)).to be true + expect(controller.authorized?(:destroy, pie)).to be true + end + end + + describe "custom authorization" do + let(:controller_class) do + Class.new(MockApplePiesController) do + def authorized?(action, resource = nil) + case action + when :destroy + resource&.ingredients != "forbidden_ingredient" + when :show + resource&.ingredients != "secret_ingredient" + else + true + end + end + end + end + + let(:custom_controller) { controller_class.new } + + before do + custom_controller.current_grandma = @grandma + end + + it "allows destroy when authorized" do + pie = create(:mock_apple_pie, grandma: @grandma, ingredients: "allowed") + expect(custom_controller.authorized?(:destroy, pie)).to be true + end + + it "blocks destroy when unauthorized" do + pie = create(:mock_apple_pie, grandma: @grandma, ingredients: "forbidden_ingredient") + expect(custom_controller.authorized?(:destroy, pie)).to be false + end + + it "allows show when authorized" do + pie = create(:mock_apple_pie, grandma: @grandma, ingredients: "allowed") + expect(custom_controller.authorized?(:show, pie)).to be true + end + + it "blocks show when unauthorized" do + pie = create(:mock_apple_pie, grandma: @grandma, ingredients: "secret_ingredient") + expect(custom_controller.authorized?(:show, pie)).to be false + end + end + + describe "custom forbidden responses" do + let(:custom_controller_class) do + Class.new(MockApplePiesController) do + def authorized?(_action, _resource = nil) + false # Always unauthorized + end + + def respond_as_forbidden + redirect_to "/custom_forbidden" + end + end + end + + it "uses custom forbidden response" do + custom_controller = custom_controller_class.new + custom_controller.current_grandma = @grandma + @grandma.mock_apple_pies.first + + # Test that respond_as_forbidden is called (we can't easily test the full response in this setup) + expect(custom_controller).to receive(:redirect_to).with("/custom_forbidden") + custom_controller.respond_as_forbidden + end + end + end +end From 69695c9b66700bf65837d0ce9117ee3cf082e248 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 11:42:51 -0400 Subject: [PATCH 06/17] Update docs and specs with details about create/update params --- README.md | 29 +++++++- lib/typical_situation/responses.rb | 4 +- lib/typical_situation/version.rb | 2 +- .../mock_apple_pies_controller_spec.rb | 72 +++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3b30344..c5559f3 100644 --- a/README.md +++ b/README.md @@ -180,12 +180,35 @@ def paginate_resources(resources) resources.paginate(page: params[:page], per_page: params[:per_page] || 25) end -# Custom pagination -def paginate_resources(resources) - resources.limit(20).offset((params[:page].to_i - 1) * 20) + # Custom pagination + def paginate_resources(resources) + resources.limit(20).offset((params[:page].to_i - 1) * 20) + end + ``` + +**Strong Parameters** - Control which parameters are allowed for create and update operations: + +```ruby +class PostsController < ApplicationController + include TypicalSituation + typical_situation :post + + private + + # Only allow title and content for new posts + def permitted_create_params + [:title, :content] + end + + # Allow title, content, and published for updates + def permitted_update_params + [:title, :content, :published] + end end ``` +By default, `TypicalSituation` permits all parameters (`permit!`) when these methods return `nil` or an empty array. Override them to restrict parameters for security. + #### Authorization Control access to resources by overriding the `authorized?` method: diff --git a/lib/typical_situation/responses.rb b/lib/typical_situation/responses.rb index 8003151..1c62256 100644 --- a/lib/typical_situation/responses.rb +++ b/lib/typical_situation/responses.rb @@ -137,13 +137,13 @@ def after_resource_destroyed_path(resource) # HTML response when @resource saved or updated. def changed_so_redirect redirect_to after_resource_updated_path(@resource) - true # return true when redirecting + true end # HTML response when @resource deleted. def gone_so_redirect redirect_to after_resource_destroyed_path(@resource) - true # return true when redirecting + true end end end diff --git a/lib/typical_situation/version.rb b/lib/typical_situation/version.rb index 1fe4a24..c5fb55b 100644 --- a/lib/typical_situation/version.rb +++ b/lib/typical_situation/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TypicalSituation - VERSION = "1.1.0" + VERSION = "1.0.0" end diff --git a/spec/controllers/mock_apple_pies_controller_spec.rb b/spec/controllers/mock_apple_pies_controller_spec.rb index 4035580..e9cf0b8 100644 --- a/spec/controllers/mock_apple_pies_controller_spec.rb +++ b/spec/controllers/mock_apple_pies_controller_spec.rb @@ -281,5 +281,77 @@ expect(permitted[:other]).to be_nil end end + + describe "strong params" do + let(:full_params) { ActionController::Parameters.new(mock_apple_pie: {ingredients: "love", grandma_id: 1, secret_field: "hidden"}) } + + before do + allow(controller).to receive(:params).and_return(full_params) + end + + describe "#permitted_create_params" do + it "returns nil by default" do + expect(controller.permitted_create_params).to be_nil + end + end + + describe "#permitted_update_params" do + it "returns nil by default" do + expect(controller.permitted_update_params).to be_nil + end + end + + describe "#create_params" do + it "permits all params when permitted_create_params is nil" do + allow(controller).to receive(:permitted_create_params).and_return(nil) + result = controller.create_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to eq(1) + expect(result[:secret_field]).to eq("hidden") + end + + it "permits all params when permitted_create_params is empty" do + allow(controller).to receive(:permitted_create_params).and_return([]) + result = controller.create_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to eq(1) + expect(result[:secret_field]).to eq("hidden") + end + + it "filters params when permitted_create_params is specified" do + allow(controller).to receive(:permitted_create_params).and_return([:ingredients]) + result = controller.create_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to be_nil + expect(result[:secret_field]).to be_nil + end + end + + describe "#update_params" do + it "permits all params when permitted_update_params is nil" do + allow(controller).to receive(:permitted_update_params).and_return(nil) + result = controller.update_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to eq(1) + expect(result[:secret_field]).to eq("hidden") + end + + it "permits all params when permitted_update_params is empty" do + allow(controller).to receive(:permitted_update_params).and_return([]) + result = controller.update_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to eq(1) + expect(result[:secret_field]).to eq("hidden") + end + + it "filters params when permitted_update_params is specified" do + allow(controller).to receive(:permitted_update_params).and_return([:ingredients]) + result = controller.update_params + expect(result[:ingredients]).to eq("love") + expect(result[:grandma_id]).to be_nil + expect(result[:secret_field]).to be_nil + end + end + end end end From 49f30bcbb01a5b9f52a176f1aa5373920ccf0d78 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 11:51:47 -0400 Subject: [PATCH 07/17] Add support fo helper rest and crud shortcuts --- Gemfile.lock | 2 +- lib/typical_situation.rb | 21 +- spec/typical_situation_syntax_spec.rb | 264 +++++++++++++++++++++++++- 3 files changed, 283 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a23242a..6e840b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - typical_situation (1.1.0) + typical_situation (1.0.0) rails (>= 7.0.0) GEM diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index 95d0e89..085c253 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -9,7 +9,6 @@ module TypicalSituation include Identity include Permissions - include Actions include Operations include Responses @@ -31,10 +30,28 @@ module ClassMethods # def model_type # :post # end - def typical_situation(model_type_symbol) + def typical_situation(model_type_symbol, only: nil) define_method :model_type do model_type_symbol end + + if only + only.each do |action| + if TypicalSituation::Actions.method_defined?(action) + define_method(action, TypicalSituation::Actions.instance_method(action)) + end + end + else + include TypicalSituation::Actions + end + end + + def typical_rest(model_type_symbol) + typical_situation(model_type_symbol, only: nil) + end + + def typical_crud(model_type_symbol) + typical_situation(model_type_symbol, only: %i[create show update destroy]) end end diff --git a/spec/typical_situation_syntax_spec.rb b/spec/typical_situation_syntax_spec.rb index 16d562b..a5c249c 100644 --- a/spec/typical_situation_syntax_spec.rb +++ b/spec/typical_situation_syntax_spec.rb @@ -54,6 +54,250 @@ def find_in_collection(id) end end + describe "with only parameter" do + let(:limited_controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + + typical_situation :test_model, only: %i[index show] + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:limited_controller) { limited_controller_class.new } + let(:grandma) { create(:grandma) } + + before do + limited_controller.current_grandma = grandma + end + + it "only defines specified actions" do + expect(limited_controller).to respond_to(:index) + expect(limited_controller).to respond_to(:show) + expect(limited_controller).not_to respond_to(:create) + expect(limited_controller).not_to respond_to(:update) + expect(limited_controller).not_to respond_to(:destroy) + end + + it "still works with model_type functionality" do + expect(limited_controller.model_type).to eq(:test_model) + end + end + + describe "typical_rest class method" do + let(:rest_controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + + typical_rest :test_model + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:rest_controller) { rest_controller_class.new } + let(:grandma) { create(:grandma) } + + before do + rest_controller.current_grandma = grandma + end + + it "defines model_type method" do + expect(rest_controller.model_type).to eq(:test_model) + end + + it "includes all REST actions" do + expect(rest_controller).to respond_to(:index) + expect(rest_controller).to respond_to(:show) + expect(rest_controller).to respond_to(:new) + expect(rest_controller).to respond_to(:create) + expect(rest_controller).to respond_to(:edit) + expect(rest_controller).to respond_to(:update) + expect(rest_controller).to respond_to(:destroy) + end + + it "is equivalent to typical_situation without only parameter" do + equivalent_class = Class.new(ApplicationController) do + include TypicalSituation + + typical_situation :test_model + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + + equivalent_controller = equivalent_class.new + equivalent_controller.current_grandma = grandma + + expect(rest_controller.model_type).to eq(equivalent_controller.model_type) + + %i[index show new create edit update destroy].each do |action| + expect(rest_controller.respond_to?(action)).to eq(equivalent_controller.respond_to?(action)) + end + end + end + + describe "typical_crud class method" do + let(:crud_controller_class) do + Class.new(ApplicationController) do + include TypicalSituation + + typical_crud :test_model + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:crud_controller) { crud_controller_class.new } + let(:grandma) { create(:grandma) } + + before do + crud_controller.current_grandma = grandma + end + + it "defines model_type method" do + expect(crud_controller.model_type).to eq(:test_model) + end + + it "includes only CRUD actions" do + expect(crud_controller).to respond_to(:create) + expect(crud_controller).to respond_to(:show) + expect(crud_controller).to respond_to(:update) + expect(crud_controller).to respond_to(:destroy) + end + + it "does not include non-CRUD actions" do + expect(crud_controller).not_to respond_to(:index) + expect(crud_controller).not_to respond_to(:new) + expect(crud_controller).not_to respond_to(:edit) + end + + it "is equivalent to typical_situation with CRUD only actions" do + equivalent_class = Class.new(ApplicationController) do + include TypicalSituation + + typical_situation :test_model, only: %i[create show update destroy] + + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + + equivalent_controller = equivalent_class.new + equivalent_controller.current_grandma = grandma + + expect(crud_controller.model_type).to eq(equivalent_controller.model_type) + + %i[index show new create edit update destroy].each do |action| + expect(crud_controller.respond_to?(action)).to eq(equivalent_controller.respond_to?(action)) + end + end + end + + describe "helper method differences" do + let(:grandma) { create(:grandma) } + + let(:rest_class) do + Class.new(ApplicationController) do + include TypicalSituation + + typical_rest :test_model + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:crud_class) do + Class.new(ApplicationController) do + include TypicalSituation + + typical_crud :test_model + attr_accessor :current_grandma + + def collection + current_grandma.mock_apple_pies + end + + def find_in_collection(id) + collection.find_by_id(id) + end + end + end + + let(:rest_controller) { rest_class.new.tap { |c| c.current_grandma = grandma } } + let(:crud_controller) { crud_class.new.tap { |c| c.current_grandma = grandma } } + + it "typical_rest includes more actions than typical_crud" do + rest_actions = %i[index show new create edit update destroy].select do |action| + rest_controller.respond_to?(action) + end + + crud_actions = %i[index show new create edit update destroy].select do |action| + crud_controller.respond_to?(action) + end + + expect(rest_actions.count).to be > crud_actions.count + end + + it "typical_crud excludes form-related actions" do + expect(crud_controller).not_to respond_to(:new) + expect(crud_controller).not_to respond_to(:edit) + expect(crud_controller).not_to respond_to(:index) + end + + it "both helpers work with model_type functionality" do + expect(rest_controller.model_type).to eq(:test_model) + expect(crud_controller.model_type).to eq(:test_model) + expect(rest_controller.model_class).to eq(TestModel) + expect(crud_controller.model_class).to eq(TestModel) + end + end + describe "backward compatibility" do let(:old_syntax_controller_class) do Class.new(ApplicationController) do @@ -111,10 +355,28 @@ def find_in_collection(id) expect(controller_class).to respond_to(:typical_situation) end - it "does not add class method to classes that do not include TypicalSituation" do + it "adds typical_rest class method when module is included" do + controller_class = Class.new(ApplicationController) do + include TypicalSituation + end + + expect(controller_class).to respond_to(:typical_rest) + end + + it "adds typical_crud class method when module is included" do + controller_class = Class.new(ApplicationController) do + include TypicalSituation + end + + expect(controller_class).to respond_to(:typical_crud) + end + + it "does not add class methods to classes that do not include TypicalSituation" do controller_class = Class.new(ApplicationController) expect(controller_class).not_to respond_to(:typical_situation) + expect(controller_class).not_to respond_to(:typical_rest) + expect(controller_class).not_to respond_to(:typical_crud) end end end From 0ffa9236819522b72a21915e4458380c2368f3b0 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 11:56:38 -0400 Subject: [PATCH 08/17] Refactor how forbidden is handled --- lib/typical_situation.rb | 14 +++++++++----- lib/typical_situation/actions.rb | 21 ++++++++++++++------- lib/typical_situation/permissions.rb | 7 ------- lib/typical_situation/responses.rb | 25 ++++++++++++++++--------- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index 085c253..637dcb3 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true -require "typical_situation/identity" -require "typical_situation/permissions" -require "typical_situation/actions" -require "typical_situation/operations" -require "typical_situation/responses" +require 'typical_situation/identity' +require 'typical_situation/permissions' +require 'typical_situation/actions' +require 'typical_situation/operations' +require 'typical_situation/responses' module TypicalSituation + class Error < StandardError; end + class ActionForbidden < Error; end + include Identity include Permissions include Operations @@ -58,6 +61,7 @@ def typical_crud(model_type_symbol) def self.add_rescues(action_controller) action_controller.class_eval do rescue_from ActiveRecord::RecordNotFound, with: :respond_as_not_found + rescue_from TypicalSituation::ActionForbidden, with: :respond_as_forbidden end end end diff --git a/lib/typical_situation/actions.rb b/lib/typical_situation/actions.rb index 23f5c5a..8b641e0 100644 --- a/lib/typical_situation/actions.rb +++ b/lib/typical_situation/actions.rb @@ -4,45 +4,52 @@ module TypicalSituation # Standard REST/CRUD actions. module Actions def index - respond_as_forbidden unless authorized?(:index) + raise TypicalSituation::ActionForbidden unless authorized?(:index) + get_resources respond_with_resources end def show get_resource - respond_as_forbidden unless authorized?(:show, @resource) + raise TypicalSituation::ActionForbidden unless authorized?(:show, @resource) + respond_with_resource end def edit get_resource - respond_as_forbidden unless authorized?(:edit, @resource) + raise TypicalSituation::ActionForbidden unless authorized?(:edit, @resource) + respond_with_resource end def new - respond_as_forbidden unless authorized?(:new) + raise TypicalSituation::ActionForbidden unless authorized?(:new) + new_resource respond_with_resource end def update get_resource - respond_as_forbidden unless authorized?(:update, @resource) + raise TypicalSituation::ActionForbidden unless authorized?(:update, @resource) + update_resource(@resource, update_params) respond_as_changed end def destroy get_resource - respond_as_forbidden unless authorized?(:destroy, @resource) + raise TypicalSituation::ActionForbidden unless authorized?(:destroy, @resource) + destroy_resource(@resource) respond_as_gone end def create - respond_as_forbidden unless authorized?(:create) + raise TypicalSituation::ActionForbidden unless authorized?(:create) + @resource = create_resource(create_params) respond_as_created end diff --git a/lib/typical_situation/permissions.rb b/lib/typical_situation/permissions.rb index a28a5c1..367236b 100644 --- a/lib/typical_situation/permissions.rb +++ b/lib/typical_situation/permissions.rb @@ -5,12 +5,5 @@ module Permissions def authorized?(_action, _resource = nil) true end - - def respond_as_forbidden - respond_to do |format| - format.html { render plain: "Forbidden", status: :forbidden } - format.json { head :forbidden } - end - end end end diff --git a/lib/typical_situation/responses.rb b/lib/typical_situation/responses.rb index 1c62256..0824e59 100644 --- a/lib/typical_situation/responses.rb +++ b/lib/typical_situation/responses.rb @@ -68,8 +68,8 @@ def respond_as_created end format.json do render json: serialize_resource(@resource), - location: location_url, - status: :created + location: location_url, + status: :created end end end @@ -82,11 +82,11 @@ def respond_as_error format.html do set_single_instance render action: (@resource.new_record? ? :new : :edit), - status: :unprocessable_entity + status: :unprocessable_entity end format.json do render json: serialize_resource(@resource, methods: [:errors]), - status: :unprocessable_entity + status: :unprocessable_entity end end end @@ -114,7 +114,7 @@ def respond_as_not_found yield(format) if block_given? format.html do - raise ActionController::RoutingError, "Not Found" + raise ActionController::RoutingError, 'Not Found' end format.json do head :not_found @@ -122,16 +122,23 @@ def respond_as_not_found end end + def respond_as_forbidden + respond_to do |format| + format.html { render plain: 'Forbidden', status: :forbidden } + format.json { head :forbidden } + end + end + def after_resource_created_path(resource) - {action: :show, id: resource.id} + { action: :show, id: resource.id } end def after_resource_updated_path(resource) - {action: :show, id: resource.id} + { action: :show, id: resource.id } end - def after_resource_destroyed_path(resource) - {action: :index} + def after_resource_destroyed_path(_resource) + { action: :index } end # HTML response when @resource saved or updated. From 69fdd0a2f316f167c49caa4ff2b588ce65f8ea7a Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:09:39 -0400 Subject: [PATCH 09/17] Update README --- .gitignore | 4 +- README.md | 330 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 195 insertions(+), 139 deletions(-) diff --git a/.gitignore b/.gitignore index 3b88694..96542b7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ pickle-email-*.html .byebug_history *.sqlite3 *.sqlite -*.log \ No newline at end of file +*.log + +pkg/**/* diff --git a/README.md b/README.md index c5559f3..1d400b8 100644 --- a/README.md +++ b/README.md @@ -29,82 +29,71 @@ Add to your **Gemfile**: ### Define your model and methods -**Modern syntax (recommended):** +Basic usage is to declare the `typical_situation`, and then two required helper methods. Everything else is handled automatically. - class MockApplePiesController < ApplicationController - include TypicalSituation - - typical_situation :mock_apple_pie +```rb +class PostsController < ApplicationController + include TypicalSituation - private + typical_situation :post # => maps to the Post model - # The collection of model instances. - def collection - current_user.mock_apple_pies - end + private - # Find a model instance by ID. - def find_in_collection(id) - collection.find_by_id(id) - end - end + # The collection of model instances. + def collection + current_user.posts + end -**Legacy syntax (still supported):** + # Find a model instance by ID. + def find_in_collection(id) + collection.find_by_id(id) + end +end +``` - class MockApplePiesController < ApplicationController - include TypicalSituation +There are two alternative helper methods: - # Symbolized, underscored version of the model (class) to use as the resource. - def model_type - :mock_apple_pie - end +#### Typical REST - private +The typical REST helper is an alias for `typical_situation`, and defines the 7 standard REST endpoints: `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`. - # The collection of model instances. - def collection - current_user.mock_apple_pies - end +```rb +class PostsController < ApplicationController + include TypicalSituation - # Find a model instance by ID. - def find_in_collection(id) - collection.find_by_id(id) - end - end + typical_rest :post -### Syntax Options + ... +end +``` -**`typical_situation` class method** - The recommended modern syntax that provides a clean, Rails-like declarative style. +#### Typical CRUD -**`model_type` instance method** - The original syntax that's still fully supported for backward compatibility. +Sometimes you don't need all seven endpoints, and just need standard CRUD. The typical CRUD helper defines the 4 standard CRUD endpoints: `create`, `show`, `update`, `destroy`. -Both syntaxes are functionally identical and can be used interchangeably. The `typical_situation` method is simply syntactic sugar that defines the `model_type` method under the hood. +```rb +class PostsController < ApplicationController + include TypicalSituation -### Get a fully functional REST API + typical_crud :post -The seven standard resourceful actions: + ... +end +``` -1. **index** -2. **show** -3. **new** -4. **create** -5. **edit** -6. **update** -7. **delete** +#### Customizing defined endpoints -For the content types: +You can also define only the endpoints you want by passing an `only` flag to `typical_situation`: -- **HTML** -- **JSON** +```rb +class PostsController < ApplicationController + include TypicalSituation -With response handling for: + typical_situation :post, only: [:index, :show] -- the collection -- a single instance -- not found -- validation errors (using ActiveModel::Errors format) -- changed -- deleted/gone + ... +end +``` ### Customize by overriding highly composable methods @@ -115,6 +104,7 @@ The library is split into modules: - [identity](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/identity.rb) - **required definitions** of the model & how to find it - [actions](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/actions.rb) - high-level controller actions - [operations](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/operations.rb) - loading, changing, & persisting the model +- [permissions](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/permissions.rb) - handling authorization to records and actions - [responses](https://github.com/mars/typical_situation/blob/master/lib/typical_situation/responses.rb) - HTTP responses & redirects #### Common Customization Hooks @@ -183,8 +173,8 @@ end # Custom pagination def paginate_resources(resources) resources.limit(20).offset((params[:page].to_i - 1) * 20) - end - ``` +end +``` **Strong Parameters** - Control which parameters are allowed for create and update operations: @@ -213,7 +203,7 @@ By default, `TypicalSituation` permits all parameters (`permit!`) when these met Control access to resources by overriding the `authorized?` method: -```ruby +```rb class PostsController < ApplicationController include TypicalSituation typical_situation :post @@ -233,122 +223,186 @@ class PostsController < ApplicationController end ``` -**CanCanCan**: `can?(action, resource || model_class)` - -**Pundit**: `policy(resource || model_class).public_send("#{action}?")` +You can also customize the response when authorization is denied: -**Custom responses**: - -```ruby +```rb def respond_as_forbidden redirect_to login_path, alert: "Access denied" end ``` +##### CanCanCan + +```rb +def authorized?(action, resource = nil) + can?(action, resource || model_class) +end +``` + +##### Pundit + +```rb +def authorized?(action, resource = nil) + policy(resource || model_class).public_send("#{action}?") +end +``` + #### Serialization Under the hood `TypicalSituation` calls `to_json` on your `ActiveRecord` models. This isn't always the optimal way to serialize resources, though, and so `TypicalSituation` offers a simple means of overriding the base Serialization --- either on an individual controller, or for your entire application. -##### ActiveModelSerializers +##### Alba -To use `ActiveModelSerializers`, add an file an initializer called `typical_situation.rb` and override the `Operations` module: +```rb +class MockApplePieResource + include Alba::Resource - module TypicalSituation - module Operations - def serializable_resource(resource) - ActiveModelSerializers::SerializableResource.new(resource) - end - end - end + attributes :id, :ingredients + + association :grandma, resource: GrandmaResource +end -If you'd like to use different serializers per method, you can check `action_name` to determine your current controller endpoint. +class MockApplePiesController < ApplicationController + include TypicalSituation + typical_situation :mock_apple_pie - class MockApplePieIndexSerializer < ActiveModel::Serializer - attributes :id, :ingredients - end + private - module TypicalSituation - module Operations - def serializable_resource(resource) - if action_name == "index" - ActiveModelSerializers::SerializableResource.new( - resource, - each_serializer: MockApplePieIndexSerializer - ) - else - ActiveModelSerializers::SerializableResource.new(resource) - end - end - end - end + def serializable_resource(resource) + MockApplePieResource.new(resource).serialize + end -##### Blueprinter + def collection + current_user.mock_apple_pies + end -`Blueprinter` relies on calling a specific blueprint, it is better suited to being overriden at the controller level. To do so, in your controller file, simply override the `serializable_resource` method as below: + def find_in_collection(id) + collection.find_by_id(id) + end +end +``` - class MockApplePieBlueprint < Blueprinter::Base - identifier :id - fields :ingredients - association :grandma, blueprint: GrandmaBlueprint - end +##### ActiveModelSerializers - class MockApplePiesController < ApplicationController - include TypicalSituation +```rb +class MockApplePieIndexSerializer < ActiveModel::Serializer + attributes :id, :ingredients +end - def serializable_resource(resource) - MockApplePieBlueprint.render(resource) +module TypicalSituation + module Operations + def serializable_resource(resource) + if action_name == "index" + ActiveModelSerializers::SerializableResource.new( + resource, + each_serializer: MockApplePieIndexSerializer + ) + else + ActiveModelSerializers::SerializableResource.new(resource) end end + end +end +``` ###### Fast JSON API -Like `Blueprinter`, +```rb +class MockApplePieSerializer + include FastJsonapi::ObjectSerializer + attributes :ingredients + belongs_to :grandma +end - class MockApplePieSerializer - include FastJsonapi::ObjectSerializer - attributes :ingredients - belongs_to :grandma - end +class MockApplePiesController < ApplicationController + include TypicalSituation + + def serializable_resource(resource) + MockApplePieSerializer.new(resource).serializable_hash + end +end +``` - class MockApplePiesController < ApplicationController - include TypicalSituation +## Development - def serializable_resource(resource) - MockApplePieSerializer.new(resource).serializable_hash - end - end +After checking out the repo, run `bin/setup` to install dependencies. -##### Alba +### Local Setup -[Alba](https://github.com/okuramasafumi/alba) is a fast, modern JSON serializer. Like `Blueprinter` and `Fast JSON API`, it's best suited to being overridden at the controller level: +1. Clone the repository +2. Install dependencies: + ```bash + bundle install + ``` +3. Install appraisal gemfiles for testing across Rails versions: + ```bash + bundle exec appraisal install + ``` - class MockApplePieResource - include Alba::Resource +### Running Tests - attributes :id, :ingredients - - association :grandma, resource: GrandmaResource - end +Tests are written using [RSpec](https://rspec.info/) and are setup to use [Appraisal](https://github.com/thoughtbot/appraisal) to run tests over multiple Rails versions. - class MockApplePiesController < ApplicationController - include TypicalSituation - typical_situation :mock_apple_pie +Run all tests across all supported Rails versions: +```bash +bundle exec appraisal rspec +``` - private +Run tests for a specific Rails version: +```bash +bundle exec appraisal rails_7.0 rspec +bundle exec appraisal rails_7.1 rspec +bundle exec appraisal rails_8.0 rspec +``` - def serializable_resource(resource) - MockApplePieResource.new(resource).serialize - end +Run specific test files: +```bash +bundle exec rspec spec/path/to/spec.rb +bundle exec appraisal rails_7.0 rspec spec/path/to/spec.rb +``` - def collection - current_user.mock_apple_pies - end +### Linting and Formatting - def find_in_collection(id) - collection.find_by_id(id) - end - end +This project uses [Standard Ruby](https://github.com/testdouble/standard) for code formatting and linting. + +Check for style violations: +```bash +bundle exec standardrb +``` + +Automatically fix style violations: +```bash +bundle exec standardrb --fix +``` + +Run both linting and tests (the default rake task): +```bash +bundle exec rake +``` + +### Console + +Start an interactive console to experiment with the gem: +```bash +bundle exec irb -r typical_situation +``` + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_situation. + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). + +## Legal Disclaimer + +Apsis Labs, LLP is not a law firm and does not provide legal advice. The information in this repo and software does not constitute legal advice, nor does usage of this software create an attorney-client relationship. + +--- + +# Built by Apsis -## Legalese +[![apsis](https://s3-us-west-2.amazonaws.com/apsiscdn/apsis.png)](https://www.apsis.io) -This project uses MIT-LICENSE. +`typical_situation` was built by Apsis Labs. We love sharing what we build! Check out our [other libraries on Github](https://github.com/apsislabs), and if you like our work you can [hire us](https://www.apsis.io) to build your vision. From 26e161f081af99749fd6240863375f491ca74bc5 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:22:41 -0400 Subject: [PATCH 10/17] Fix bundler compatability for github actions --- .github/workflows/ci.yml | 6 +++--- Gemfile.lock | 10 +++++----- README.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc5e019..d507100 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,11 @@ jobs: name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} strategy: matrix: - ruby: [3.0, 3.1, 3.2, 3.3] + ruby: [3.1, 3.2, 3.3, 3.4] rails: [rails_7.0, rails_7.1, rails_8.0] exclude: - - ruby: 3.0 - rails: rails_8.0 # Rails 8 requires Ruby 3.1+ + - ruby: 3.1 + rails: rails_8.0 # Rails 8 requires Ruby 3.2+ env: RUBY_VERSION: ${{ matrix.ruby }} RAILS_VERSION: ${{ matrix.rails }} diff --git a/Gemfile.lock b/Gemfile.lock index 6e840b8..51582da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,7 @@ GEM ast (2.4.3) base64 (0.3.0) benchmark (0.4.1) - bigdecimal (3.2.3) + bigdecimal (3.3.0) builder (3.3.0) byebug (12.0.0) combustion (1.5.0) @@ -121,7 +121,7 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.15.0) + json (2.15.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -161,7 +161,7 @@ GEM date stringio racc (1.8.1) - rack (3.2.1) + rack (3.2.2) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -283,7 +283,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.0.3) + uri (1.0.4) useragent (0.16.11) websocket-driver (0.8.0) base64 @@ -309,4 +309,4 @@ DEPENDENCIES typical_situation! BUNDLED WITH - 2.7.2 + 2.4.22 diff --git a/README.md b/README.md index 1d400b8..7cc3631 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ Tested in: Against Ruby versions: -- 3.0 - 3.1 - 3.2 - 3.3 +- 3.4 Add to your **Gemfile**: From 8051d6df65e67aad342bad4ce03f3e8588c4083c Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:24:09 -0400 Subject: [PATCH 11/17] Update README with clearer comment on typical situation syntax --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7cc3631..e238dda 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Basic usage is to declare the `typical_situation`, and then two required helper class PostsController < ApplicationController include TypicalSituation + # Symbolized, underscored version of the model to use as the resource. typical_situation :post # => maps to the Post model private From 26d0934c15dfb14cfb6c671850cc17dcbd939ed0 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:26:14 -0400 Subject: [PATCH 12/17] Update homepage in gemspec --- typical_situation.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typical_situation.gemspec b/typical_situation.gemspec index 9ccf90f..5ca6f48 100644 --- a/typical_situation.gemspec +++ b/typical_situation.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.version = TypicalSituation::VERSION s.authors = ["Mars Hall", "Wyatt Kirby"] s.email = ["m@marsorange.com", "wyatt@apsis.io"] - s.homepage = "https://github.com/mars/typical_situation" + s.homepage = "https://github.com/apsislabs/typical_situation" s.summary = "The missing Rails ActionController REST API mixin." s.description = "A module providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection." From e8d763a092de7144c8ba30900f7bc5dee47594ff Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:27:02 -0400 Subject: [PATCH 13/17] Add x86 linux to gemfile for running specs on github --- Gemfile.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 51582da..5df8430 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,8 @@ GEM nio4r (2.7.4) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) @@ -255,6 +257,7 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) sqlite3 (2.7.4-arm64-darwin) + sqlite3 (2.7.4-x86_64-linux-gnu) standard (1.51.1) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -293,6 +296,7 @@ GEM PLATFORMS arm64-darwin-20 + x86_64-linux DEPENDENCIES appraisal From 303ef33799326b7a68e50efb9d721f371f570e4d Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:42:13 -0400 Subject: [PATCH 14/17] Remove Gemfile.lock from git --- Gemfile.lock | 316 --------------------------------------------------- 1 file changed, 316 deletions(-) delete mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 5df8430..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,316 +0,0 @@ -PATH - remote: . - specs: - typical_situation (1.0.0) - rails (>= 7.0.0) - -GEM - remote: http://rubygems.org/ - specs: - actioncable (8.0.3) - actionpack (= 8.0.3) - activesupport (= 8.0.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (8.0.3) - actionpack (= 8.0.3) - activejob (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) - mail (>= 2.8.0) - actionmailer (8.0.3) - actionpack (= 8.0.3) - actionview (= 8.0.3) - activejob (= 8.0.3) - activesupport (= 8.0.3) - mail (>= 2.8.0) - rails-dom-testing (~> 2.2) - actionpack (8.0.3) - actionview (= 8.0.3) - activesupport (= 8.0.3) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - useragent (~> 0.16) - actiontext (8.0.3) - actionpack (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (8.0.3) - activesupport (= 8.0.3) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (8.0.3) - activesupport (= 8.0.3) - globalid (>= 0.3.6) - activemodel (8.0.3) - activesupport (= 8.0.3) - activerecord (8.0.3) - activemodel (= 8.0.3) - activesupport (= 8.0.3) - timeout (>= 0.4.0) - activestorage (8.0.3) - actionpack (= 8.0.3) - activejob (= 8.0.3) - activerecord (= 8.0.3) - activesupport (= 8.0.3) - marcel (~> 1.0) - activesupport (8.0.3) - 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) - appraisal (2.5.0) - bundler - rake - thor (>= 0.14.0) - ast (2.4.3) - base64 (0.3.0) - benchmark (0.4.1) - bigdecimal (3.3.0) - builder (3.3.0) - byebug (12.0.0) - combustion (1.5.0) - activesupport (>= 3.0.0) - railties (>= 3.0.0) - thor (>= 0.14.6) - concurrent-ruby (1.3.5) - connection_pool (2.5.4) - coveralls (0.8.23) - json (>= 1.8, < 3) - simplecov (~> 0.16.1) - term-ansicolor (~> 1.3) - thor (>= 0.19.4, < 2.0) - tins (~> 1.6) - crass (1.0.6) - date (3.4.1) - diff-lcs (1.6.2) - docile (1.4.1) - drb (2.2.3) - erb (5.0.3) - erubi (1.13.1) - factory_bot (6.5.5) - activesupport (>= 6.1.0) - factory_bot_rails (6.5.1) - factory_bot (~> 6.5) - railties (>= 6.1.0) - globalid (1.3.0) - activesupport (>= 6.1) - i18n (1.14.7) - concurrent-ruby (~> 1.0) - io-console (0.8.1) - irb (1.15.2) - pp (>= 0.6.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - json (2.15.1) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - loofah (2.24.1) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.1.0) - mini_mime (1.1.5) - minitest (5.25.5) - mize (0.6.1) - net-imap (0.5.12) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.1) - net-protocol - nio4r (2.7.4) - nokogiri (1.18.10-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.10-x86_64-linux-gnu) - racc (~> 1.4) - parallel (1.27.0) - parser (3.3.9.0) - ast (~> 2.4.1) - racc - pp (0.6.3) - prettyprint - prettyprint (0.2.0) - prism (1.5.1) - psych (5.2.6) - date - stringio - racc (1.8.1) - rack (3.2.2) - rack-session (2.1.1) - base64 (>= 0.1.0) - rack (>= 3.0.0) - rack-test (2.2.0) - rack (>= 1.3) - rackup (2.2.1) - rack (>= 3) - rails (8.0.3) - actioncable (= 8.0.3) - actionmailbox (= 8.0.3) - actionmailer (= 8.0.3) - actionpack (= 8.0.3) - actiontext (= 8.0.3) - actionview (= 8.0.3) - activejob (= 8.0.3) - activemodel (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) - bundler (>= 1.15.0) - railties (= 8.0.3) - rails-controller-testing (1.0.5) - actionpack (>= 5.0.1.rc1) - actionview (>= 5.0.1.rc1) - activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.3.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.0.3) - actionpack (= 8.0.3) - activesupport (= 8.0.3) - irb (~> 1.13) - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - tsort (>= 0.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.3.0) - rdoc (6.15.0) - erb - psych (>= 4.0.0) - tsort - regexp_parser (2.11.3) - reline (0.6.2) - io-console (~> 0.5) - rspec-core (3.13.5) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-rails (8.0.2) - actionpack (>= 7.2) - activesupport (>= 7.2) - railties (>= 7.2) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) - rspec-support (3.13.6) - rubocop (1.80.2) - json (~> 2.3) - 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.46.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.47.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-performance (1.25.0) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - ruby-progressbar (1.13.0) - securerandom (0.4.1) - simplecov (0.16.1) - docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) - sqlite3 (2.7.4-arm64-darwin) - sqlite3 (2.7.4-x86_64-linux-gnu) - standard (1.51.1) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.80.2) - standard-custom (~> 1.0.0) - standard-performance (~> 1.8) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.8.0) - lint_roller (~> 1.1) - rubocop-performance (~> 1.25.0) - stringio (3.1.7) - sync (0.5.0) - term-ansicolor (1.11.3) - tins (~> 1) - thor (1.4.0) - timeout (0.4.3) - tins (1.44.1) - bigdecimal - mize (~> 0.6) - sync - tsort (0.2.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) - uri (1.0.4) - useragent (0.16.11) - websocket-driver (0.8.0) - base64 - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.7.3) - -PLATFORMS - arm64-darwin-20 - x86_64-linux - -DEPENDENCIES - appraisal - bundler (>= 2.2.0) - byebug - combustion - coveralls - factory_bot_rails - rails-controller-testing - rake - rspec-rails (>= 6.0) - sqlite3 (>= 1.4) - standard - typical_situation! - -BUNDLED WITH - 2.4.22 From 188c30170abbd3d2dd36075b3adf9e3d7b38edab Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 12:47:20 -0400 Subject: [PATCH 15/17] Remove support for 3.1 --- .github/workflows/ci.yml | 7 ++----- README.md | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d507100..cb888f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,8 @@ jobs: name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} strategy: matrix: - ruby: [3.1, 3.2, 3.3, 3.4] + ruby: [3.2, 3.3, 3.4] rails: [rails_7.0, rails_7.1, rails_8.0] - exclude: - - ruby: 3.1 - rails: rails_8.0 # Rails 8 requires Ruby 3.2+ env: RUBY_VERSION: ${{ matrix.ruby }} RAILS_VERSION: ${{ matrix.rails }} @@ -39,4 +36,4 @@ jobs: - name: Install dependencies run: bundle exec appraisal install - name: Run specs - run: bundle exec appraisal ${{ matrix.rails }} rspec \ No newline at end of file + run: bundle exec appraisal ${{ matrix.rails }} rspec diff --git a/README.md b/README.md index e238dda..a939229 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Tested in: Against Ruby versions: -- 3.1 - 3.2 - 3.3 - 3.4 From efd5b8fc15ffdee72842ddf8383438839b61eadd Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 13:18:45 -0400 Subject: [PATCH 16/17] Update ruby versions to match across standard and workflows --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish.yml | 4 ++-- .standard.yml | 4 ++-- README.md | 3 --- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb888f0..a33009f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest name: Lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.3 + ruby-version: 3.4 bundler-cache: true - name: Run Standard RB run: bundle exec standardrb @@ -27,7 +27,7 @@ jobs: RUBY_VERSION: ${{ matrix.ruby }} RAILS_VERSION: ${{ matrix.rails }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e61d7d3..3a2e34d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: '3.1' + ruby-version: '3.4' bundler-cache: true - name: Release Gem if: contains(github.ref, 'refs/tags/v') @@ -33,4 +33,4 @@ jobs: bundle exec rake build echo "Running gem release task..." - gem push pkg/typical_situation-${TAG#v}.gem \ No newline at end of file + gem push pkg/typical_situation-${TAG#v}.gem diff --git a/.standard.yml b/.standard.yml index fe232bc..eeb43ab 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,7 +1,7 @@ # Standard Ruby configuration # https://github.com/testdouble/standard -ruby_version: 3.0 +ruby_version: 3.4 ignore: - 'spec/internal/**/*' @@ -9,4 +9,4 @@ ignore: - 'vendor/**/*' - 'tmp/**/*' -fix: true \ No newline at end of file +fix: true diff --git a/README.md b/README.md index a939229..6c13d36 100644 --- a/README.md +++ b/README.md @@ -395,9 +395,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/apsisl The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). -## Legal Disclaimer - -Apsis Labs, LLP is not a law firm and does not provide legal advice. The information in this repo and software does not constitute legal advice, nor does usage of this software create an attorney-client relationship. --- From ddd881281f3ca2ca451ffc97c627b13ca778dc53 Mon Sep 17 00:00:00 2001 From: Wyatt Kirby Date: Tue, 7 Oct 2025 13:18:59 -0400 Subject: [PATCH 17/17] Run standard --- lib/typical_situation.rb | 10 +++++----- lib/typical_situation/responses.rb | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/typical_situation.rb b/lib/typical_situation.rb index 637dcb3..4d5a1a6 100644 --- a/lib/typical_situation.rb +++ b/lib/typical_situation.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'typical_situation/identity' -require 'typical_situation/permissions' -require 'typical_situation/actions' -require 'typical_situation/operations' -require 'typical_situation/responses' +require "typical_situation/identity" +require "typical_situation/permissions" +require "typical_situation/actions" +require "typical_situation/operations" +require "typical_situation/responses" module TypicalSituation class Error < StandardError; end diff --git a/lib/typical_situation/responses.rb b/lib/typical_situation/responses.rb index 0824e59..4cd9fbe 100644 --- a/lib/typical_situation/responses.rb +++ b/lib/typical_situation/responses.rb @@ -68,8 +68,8 @@ def respond_as_created end format.json do render json: serialize_resource(@resource), - location: location_url, - status: :created + location: location_url, + status: :created end end end @@ -82,11 +82,11 @@ def respond_as_error format.html do set_single_instance render action: (@resource.new_record? ? :new : :edit), - status: :unprocessable_entity + status: :unprocessable_entity end format.json do render json: serialize_resource(@resource, methods: [:errors]), - status: :unprocessable_entity + status: :unprocessable_entity end end end @@ -114,7 +114,7 @@ def respond_as_not_found yield(format) if block_given? format.html do - raise ActionController::RoutingError, 'Not Found' + raise ActionController::RoutingError, "Not Found" end format.json do head :not_found @@ -124,21 +124,21 @@ def respond_as_not_found def respond_as_forbidden respond_to do |format| - format.html { render plain: 'Forbidden', status: :forbidden } + format.html { render plain: "Forbidden", status: :forbidden } format.json { head :forbidden } end end def after_resource_created_path(resource) - { action: :show, id: resource.id } + {action: :show, id: resource.id} end def after_resource_updated_path(resource) - { action: :show, id: resource.id } + {action: :show, id: resource.id} end def after_resource_destroyed_path(_resource) - { action: :index } + {action: :index} end # HTML response when @resource saved or updated.