Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
name: Lint
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand All @@ -21,19 +21,25 @@ jobs:
name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }}
strategy:
matrix:
ruby: [3.2, 3.3, 3.4]
rails: [rails_7.0, rails_7.1, rails_8.0]
ruby: [3.1, 3.2, 3.3, 3.4, 4.0]
rails: [rails_7.2, rails_8.0, rails_8.1]
exclude:
- ruby: 3.1
rails: rails_8.0
- ruby: 3.1
rails: rails_8.1
env:
RUBY_VERSION: ${{ matrix.ruby }}
RAILS_VERSION: ${{ matrix.rails }}
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- 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
run: bundle install
- name: Run specs
run: bundle exec appraisal ${{ matrix.rails }} rspec
run: bundle exec rspec
75 changes: 56 additions & 19 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
name: Publish Gem

on:
release:
types: [published]
push:
tags:
- "v*"

permissions:
contents: read

jobs:
build:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- name: Checkout
uses: actions/checkout@v6

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
ruby-version: "3.4"
bundler-cache: true
- name: Release Gem
if: contains(github.ref, 'refs/tags/v')

- name: Build gem
run: |
set -euo pipefail
gemspec="$(ls *.gemspec | head -n 1)"
gem_name="${gemspec%.gemspec}"
version="${GITHUB_REF_NAME#v}"
gem_file="pkg/${gem_name}-${version}.gem"

echo "Building ${gem_name} ${version}..."
mkdir -p pkg
bundle exec gem build "$gemspec" --output "$gem_file"
test -f "$gem_file"

- name: Publish to RubyGems
env:
RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
TAG: ${{ github.event.release.tag_name }}
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
run: |
echo "Setting up gem credentials..."
mkdir -p ~/.gem
set -euo pipefail

if [[ -z "${RUBYGEMS_API_KEY:-}" ]]; then
echo "::error::Missing RUBYGEMS_API_KEY secret."
exit 1
fi

cat << EOF > ~/.gem/credentials
---
:rubygems_api_key: ${RUBYGEMS_API_KEY}
EOF
gem_file="$(ls pkg/*.gem | head -n 1)"
echo "Publishing ${gem_file}..."

mkdir -p ~/.gem
printf -- "---\n:rubygems_api_key: %s\n" "$RUBYGEMS_API_KEY" > ~/.gem/credentials
chmod 0600 ~/.gem/credentials

bundle exec rake build
gem push "$gem_file"
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

prerelease_flag=()
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
prerelease_flag=(--prerelease)
fi

echo "Running gem release task..."
gem push pkg/typical_situation-${TAG#v}.gem
echo "Creating GitHub release ${GITHUB_REF_NAME}..."
gh release create "$GITHUB_REF_NAME" pkg/*.gem \
--verify-tag \
--title "$GITHUB_REF_NAME" \
--notes "Published to RubyGems." \
"${prerelease_flag[@]}"
24 changes: 12 additions & 12 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# frozen_string_literal: true

appraise "rails_7.0" do
gem "rails", "~> 7.0"
gem "rspec", "~> 3.12"
gem "rspec-rails", "~> 6.0"
appraise "rails_7.2" do
gem "rails", "~> 7.2"
gem "rspec", "~> 3.13"
gem "rspec-rails", "~> 7.0"
gem "rails-controller-testing"
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 2.1"
end

appraise "rails_7.1" do
gem "rails", "~> 7.1"
gem "rspec", "~> 3.12"
gem "rspec-rails", "~> 6.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", "~> 1.4"
gem "sqlite3", "~> 2.1"
end

appraise "rails_8.0" do
gem "rails", "~> 8.0"
appraise "rails_8.1" do
gem "rails", "~> 8.1"
gem "rspec", "~> 3.13"
gem "rspec-rails", "~> 8.0"
gem "rails-controller-testing"
Expand Down
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ A Ruby mixin (module) providing the seven standard resource actions & responses

Tested in:

- Rails 7.0
- Rails 7.1
- Rails 7.2
- Rails 8.0
- Rails 8.1

Against Ruby versions:

- 3.1
- 3.2
- 3.3
- 3.4
- 4.0

Add to your **Gemfile**:

Expand Down Expand Up @@ -109,7 +111,17 @@ The library is split into modules:

#### Common Customization Hooks

**Scoped Collections** - Filter the collection based on user permissions or other criteria:
Most customization hooks are intended to be overridden as `private` or `protected` controller methods so they do not become public controller actions.

For index actions, resources move through this pipeline:

1. `collection` - base relation from the host controller
2. `scoped_resource` - visibility/tenant/security scoping
3. `prepare_resources` - search or filter transforms
4. `apply_sorting` - default sorting from `default_sorting_attribute`
5. `paginate_resources` - pagination adapter hook

**Scoped Collections** - Restrict the base collection based on user permissions, tenancy, or other security boundaries:

```ruby
def scoped_resource
Expand All @@ -121,6 +133,16 @@ def scoped_resource
end
```

**Search and Filtering** - Apply request-driven filters after scoping and before sorting/pagination:

```ruby
def prepare_resources(resources)
resources = resources.where(status: params[:status]) if params[:status].present?
resources = resources.where("title ILIKE ?", "%#{params[:q]}%") if params[:q].present?
resources
end
```

**Custom Lookup** - Use different attributes for finding resources:

```ruby
Expand Down Expand Up @@ -439,6 +461,21 @@ Start an interactive console to experiment with the gem:
bundle exec irb -r typical_situation
```

## Releases

Releases are driven by git tags. The version lives in `lib/typical_situation/version.rb`, and the gemspec reads `TypicalSituation::VERSION`.

Release locally from the branch you want to publish:

```bash
bundle install
bin/release patch # or: minor, major
```

`bin/release` uses `bump`, commits the version file, creates a `vX.Y.Z` tag, pushes the branch, and pushes the tag.

GitHub Actions publishes only when a `v*` tag is pushed. The publish workflow builds the gem and pushes it to RubyGems with `RUBYGEMS_API_KEY`.

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_situation.
Expand Down
84 changes: 84 additions & 0 deletions bin/release
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
echo "Usage: bin/release patch|minor|major|pre"
echo " bin/release rc X.Y.Z"
}

level="${1:-}"
target_version="${2:-}"
case "$level" in
patch|minor|major|pre)
if [[ $# -ne 1 ]]; then
usage
exit 1
fi
;;
rc)
if [[ $# -ne 2 || ! "$target_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
usage
exit 1
fi
;;
*)
usage
exit 1
;;
esac

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: bin/release must be run inside a git repository."
exit 1
fi

branch="$(git symbolic-ref --quiet --short HEAD)" || {
echo "Error: releases must be run from a branch, not a detached HEAD."
exit 1
}

version_file="$(bundle exec bump file | awk '{print $NF}')"
current_version="$(bundle exec bump current --value-only)"
if [[ "$level" == "rc" ]]; then
next_version="${target_version}-rc"
else
next_version="$(bundle exec bump show-next "$level" --value-only)"
fi
tag="v${next_version}"

if [[ -z "$version_file" || -z "$current_version" || -z "$next_version" ]]; then
echo "Error: could not determine gem version information."
exit 1
fi

if ! git diff --quiet -- "$version_file" || ! git diff --cached --quiet -- "$version_file"; then
echo "Error: $version_file has uncommitted changes."
exit 1
fi

if git rev-parse "$tag" >/dev/null 2>&1; then
echo "Error: tag $tag already exists."
exit 1
fi

echo "Releasing ${tag} (${level})"
echo "Version: ${current_version} -> ${next_version}"
echo "Branch: ${branch}"

if [[ "$level" == "rc" ]]; then
bundle exec bump set "$next_version" --no-commit --no-bundle
else
bundle exec bump "$level" --no-commit --no-bundle
fi

git add "$version_file"
git commit -m "$tag"
git tag "$tag"

echo "Pushing ${branch}..."
git push origin "$branch"

echo "Pushing ${tag}..."
git push origin "$tag"

echo "Released ${tag}."
8 changes: 4 additions & 4 deletions gemfiles/rails_7.0.gemfile → gemfiles/rails_7.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

source "http://rubygems.org"

gem "rails", "~> 7.0"
gem "rspec", "~> 3.12"
gem "rspec-rails", "~> 6.0"
gem "rails", "~> 7.2"
gem "rspec", "~> 3.13"
gem "rspec-rails", "~> 7.0"
gem "rails-controller-testing"
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 2.1"

gemspec path: "../"
8 changes: 4 additions & 4 deletions gemfiles/rails_7.1.gemfile → gemfiles/rails_8.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

source "http://rubygems.org"

gem "rails", "~> 7.1"
gem "rspec", "~> 3.12"
gem "rspec-rails", "~> 6.1"
gem "rails", "~> 8.1"
gem "rspec", "~> 3.13"
gem "rspec-rails", "~> 8.0"
gem "rails-controller-testing"
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 2.1"

gemspec path: "../"
1 change: 1 addition & 0 deletions lib/typical_situation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def typical_situation(model_type_symbol, only: nil)
define_method :model_type do
model_type_symbol
end
protected :model_type

if only
only.each do |action|
Expand Down
2 changes: 2 additions & 0 deletions lib/typical_situation/identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module TypicalSituation
# These Identity methods must be defined for each implementation.
module Identity
protected

# Symbolized, underscored version of the model (class) to use.
def model_type
raise(NotImplementedError, "#model_type must be defined in the TypicalSituation implementation.")
Expand Down
Loading