Outro: Ruminate, Riff, Resolve
Outro is a Rails engine that provides your application with a chord library, general music theory tools, and tools for creating and displaying chord charts. Attach a transposable, instrument-aware chord chart to any model in your app with a single line.
Installation
Add this line to your application's Gemfile:
# Gemfile
gem 'outro_rails'
then bundle install.
Copy the engine's migrations into your app and run them:
bin/rails outro:install:migrations
bin/rails db:migrate
This creates four tables: outro_rails_chords, outro_rails_chord_voicings,
outro_rails_chord_charts, and outro_rails_chord_chart_voicings.
Outro ships its chord library as seed data inside the gem. Load it either by
adding this to your app's db/seeds.rb (so it runs with your normal
bin/rails db:seed):
OutroRails::Engine.load_seed
or by running the provided task directly:
bin/rails outro_rails:seed
Seeding is idempotent, so it's safe to re-run at any time.
Usage
To view the chord library and music theory tools, mount the engine in your routes.rb:
Rails.application.routes.draw do
mount OutroRails::Engine => "/outro"
end
If you use Devise and want the whole
engine to require a signed-in user, wrap the mount in Devise's
authenticate routing helper. Unauthenticated visitors will be redirected to
sign in before any engine page renders:
Rails.application.routes.draw do
authenticate :user do
mount OutroRails::Engine => "/outro"
end
end
Model: has_chord_chart
Attach a chord chart to any model, the same way ActionText attaches rich text:
class Song < ApplicationRecord
has_chord_chart :chord_sheet
end
This gives the model chord_sheet/chord_sheet=/chord_sheet?, backed by a
OutroRails::ChordChart row (record, name, plus artist, song
(title), tuning, capo, key, and body). A model can attach more
than one chart under different names.
Forms: chord_chart_area
In a form, pair it with the chord_chart_area form builder method:
<%= form_with model: @song do |form| %>
<%= form.chord_chart_area :chord_sheet %>
<%= form.submit %>
<% end %>
chord_chart_area renders artist/song/tuning/capo/key/body fields. Tuning
options are pulled from Instruments::Guitar::Tunings, capo options are capped
at fret 7 (Instruments::Guitar::Capo::FRETS), and key options are set from
from Theory::Key.
Controller: Permitting the Parameters
chord_chart_area submits the chart as nested attributes, so your
controller has to permit them alongside your own columns. The association
is named chord_chart_<name>, where <name> is the argument you passed
to has_chord_chart. A model declaring has_chord_chart :chord_sheet needs
the key chord_chart_chord_sheet_attributes:
class SongsController < ApplicationController
# ...
private
def song_params
params.require(:song).permit(
:description,
chord_chart_chord_sheet_attributes: %i[artist body capo key song tuning]
)
end
end
The six permitted attributes are exactly the fields chord_chart_area
renders. record and name are managed by the engine and should not be
permitted. If a model attaches more than one chart, each one needs its own
key: has_chord_chart :lead_sheet is permitted as
chord_chart_lead_sheet_attributes.
With the parameters permitted, the chart saves through your existing update action.
Views: Displaying a Chart
To display it, embed the engine's chart partial in your own show view:
<%= render "outro_rails/chord_charts/chart",
chord_chart: @song.chord_sheet,
instrument: params[:instrument], # "guitar" or "piano", default "guitar"
to_key: params[:to_key], # preview transposed into another key; view-only, nothing is saved
editable: @song.editable? %>
This renders the chart's metadata, an instrument toggle, a transpose-to-key picker, the chart body with recognized chords highlighted, and a diagram for every chord the body references.
Authorizing Chart Edits
The chart's own content (artist, title, body, key, ...) is edited through
your forms via chord_chart_area, so it's already protected by whatever
protects the controller those forms submit to. The one write surface the
engine itself owns is voicing selection.
Two things gate that:
editable:(the partial local) only decides whether the edit UI renders. It is not access control - the routes behind the buttons exist whether or not any page showed them.OutroRails.chord_chart_voicing_authorizationis the server-side check those routes enforce. It's a callable receivingchord_chart:andcontroller:, and it defaults to open (-> { true }) so the engine works with zero configuration.
If charts belong to users, set the lambda in an initializer. The
controller argument is an engine controller, not one of your app's own,
so helpers defined only on your ApplicationController aren't
available on it:
# config/initializers/outro_rails.rb
# Or, with hand-rolled session auth:
OutroRails. = ->(chord_chart:, controller:) {
user = User.find_by(id: controller.session[:user_id])
user && chord_chart.record.user_id == user.id
}
With Devise
Devise installs current_user, user_signed_in?, etc. on
ActionController::Base itself, so they are available inside the
engine's controllers. The lambda can use them directly. The usual rule,
"whoever can edit the record can edit its chart", looks like:
# config/initializers/outro_rails.rb
OutroRails. = ->(chord_chart:, controller:) {
user = controller.current_user
user.present? && chord_chart.record.user_id == user.id
}
Adjust the ownership clause to your schema (record.user == user, a
membership check, an admin? bypass, ...). If your Devise scope isn't
:user, call its helper instead (controller.current_admin, ...). If
you run Pundit or CanCanCan on top of Devise, delegate to it so the rule
lives in one place:
OutroRails. = ->(chord_chart:, controller:) {
user = controller.current_user
user.present? && Pundit.policy(user, chord_chart.record).update?
}
Then have your view ask the same question when deciding whether to render the edit UI, so the buttons never appear for someone the server would refuse:
<%= render "outro_rails/chord_charts/chart",
chord_chart: @song.chord_sheet,
editable: OutroRails.chord_chart_voicing_authorized?(
chord_chart: @song.chord_sheet, controller: controller
) %>
Development
Clone the repository and copy the .env.tmpl file to .env:
git clone git@gitlab.com:endtoendpaper/outro_rails.git
cd outro_rails
cp .env.tmpl .env
Configure the variables in .env
- Leave the POSTGRES_USER and POSTGRES_PASSWORD as the PostgreSQL image defaults.
- Set
UIDandGIDto your local user's values so files created inside the container have the correct permissions. (id -u,id -g)
Example .env:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
UID=1000
GID=1000
Build the docker containers and start the application:
docker compose build
docker compose up -d
docker compose exec gem bundle exec rails app:db:prepare
Access the dummy app from this link: http://localhost:3002
It seeds the chord library and four example songs (spec/dummy/db/seeds.rb).
The example songs are listed on the home page. To illustrate permissions,
the dummy app sets even record ids as editable and odd ones as view-only.
Publishing a New Version
1. Bump the version. Edit lib/outro_rails/version.rb
2. Update the changelog. Edit CHANGELOG.md
3. Commit and push
git commit -m "Release v0.1.0"
git push origin main
4. Normalize file permissions. Files that aren't world-readable get packaged that way and will break installs.
chmod -R a+rX app config db lib
5. Build the gem.
gem build outro_rails.gemspec
6. Verify package contents.
gem spec outro_rails-0.1.0.gem files
7. Push it.
gem push outro_rails-0.1.0.gem
License
The gem is available as open source under the terms of the MIT License.