Thoughts on software development and occasionally other things.

Requiring Ruby standard library packages

After working with Rails so much, it’s easy to forget all the magic it adds to Ruby. A colleague had a question about why the CSV library worked while in a development environment, but failed to in a production environment. After some quick debugging, we discovered that it was simply because the CSV library wasn’t loaded in the production environment. It was likely that a library loaded only during development was requiring the CSV library, thus making it available to use.

It’s probably a good practice to require all the standard library packages needed by code in a project, and not rely on them coming from somewhere else (such as a gem, that might only be loaded in certain contexts). This is certainly something that I have overlooked lately in my own code, as things like CSV, ‘just worked’.

Systemd and Puma

This website is served by a combination of Nginx and Puma on a Ubuntu host (all on a virtual private server in the cloud somewhere). It took some time to get all of these components talking to each other in the way that I wanted. System administration has always been a hobby of mine ever since taking up programming, however I am in no way an expert. This is as much for others as it is for my self. Without a doubt it will be useful in my future as I build and maintain servers, and in the mean time it may also help someone else struggling with a similar setup.

This assumes Puma is serving a Ruby on Rails application. I am also using Capistrano for deployment, which determines the folder structures used in the configuration files.

Here is the Puma configuration script living at /var/www/my-app/shared/puma.rb.

environment "production"

bind  "unix:///var/www/my-app/shared/tmp/sockets/puma.sock"
pidfile "/var/www/my-app/shared/tmp/pids/puma.pid"
state_path "/var/www/my-app/shared/tmp/pids/puma.state"
directory "/var/www/my-app/current"
rackup "/var/www/my-app/current/config.ru"

stdout_redirect "/var/www/my-app/shared/log/puma_access.log", "/var/www/my-app/shared/log/puma_error.log", true

workers 0
threads 0,16

activate_control_app "unix:///var/www/my-app/shared/tmp/sockets/pumactl.sock"

prune_bundler

Definitely be sure to tweak all the settings as needed. Also note that the typical daemonize true line is not present in this configuration. This is key to make managing Puma with Systemd simpler.

Next is the Systemd configuration file for the Puma application server in /etc/systemd/system/puma.service.

[Unit]
Description=Puma application server
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/my-app/current
Environment=RAILS_ENV=production

ExecStart=/home/my-user/.rbenv/shims/bundle exec puma -C /var/www/my-app/shared/puma.rb --pidfile /var/www/my-app/shared/tmp/pids/puma.pid -e production
ExecStop=/home/my-user/.rbenv/shims/bundle exec pumactl -S /var/www/my-app/shared/tmp/pids/puma.state stop
ExecReload=/home/my-user/.rbenv/shims/bundle exec pumactl -S /var/www/my-app/shared/tmp/pids/puma.state phased-restart
Restart=always
KillMode=process

[Install]
WantedBy=multi-user.target

If daemonize true is included in the Puma configuration, then the Systemd configuration would need to include something like PIDFile=/var/www/my-app/shared/tmp/pids/puma.pid (a value set at the top of the Puma configuration file).

With these two files in place, the typical Systemd commands all behave as expected status, start, stop, restart, etc. Assuming everything else (Nginx, database, etc.) is setup properly, Puma should boot back up and be serving the application if the host machine ever needs to be restarted.

Auto-resizing text fields with Stimulus

Having a text field grow with its contents is a much nicer experience than having to scroll all the way through it. Writing and reviewing articles for my website wasn’t the easiest because of that. I mostly wrote in a text editor and copied/pasted to avoid having to scroll as much. That work-flow put some barriers up in terms of getting an article written. I’ve been experimenting with Stimulus lately, and figured it would be a good learning experience to solve the problem using it.

It really isn’t all that complex. There are different, and likely better, ways to solve this problem.

// expandable_field_controller.js
import { Controller } from 'stimulus'

export default class extends Controller {

  static targets = ['input']

  initialize() {
    this.setup()
    this.resize()
  }

  setup() {
    this.inputTarget.style.height = `${this.inputTarget.scrollHeight}px`
  }

  resize() {
    this.inputTarget.style.height = 'auto'
    this.inputTarget.style.height = `${this.inputTarget.scrollHeight}px`
  }

}

It’s worth noting that I have a min-height set on <textarea> controls that this is used with. Setting the height to auto on the resize() call prevented a weird bug when reducing the height (?). I chose to use the input event to trigger the behaviour.

<div data-controller="expandable-field">
  <%=
    form.text_area :content,
      data: {
        target: "expandable-field.input",
        action: "input->expandable-field#resize"
      }
  %>
</div>

Dynamic routes with Ruby on Rails

Often the need to build dynamic pages with static routes into Ruby on Rails applications comes up. One such example is an, ‘About Us’, page. The content has to be modifiable through a web interface, but the route is typically static, such as, /about. There are many approaches to handling this without getting into building something complex like a full content management system.

Here is a minimal model to represent a page.

Page(id: uuid, title: string, permalink: string, content: string)

A typical model would have a resourceful route defined for it (resources :pages) letting a page be accessible at /pages/:id. Overriding Page#to_param and setting it to the permalink attribute is a step in the right direction by producing a URL like /pages/about. It’s not ideal to define a named route that points to an action which loads a particular page:

##
# This approach works, but has drawbacks...
#

# config/routes.rb
get 'about', to: 'pages#about'

# app/controllers/pages_controller.rb
...
def about
  @page = Page.find_by(permalink: 'about')
end
...

This works, but it’s too rigid. If the permalink changes, or another page is added, the routes and controller would need to be updated. Routes cannot be dynamically generated and updated (i.e. defining named routes for all pages at present and in the future). However, Rails provides routing constraints, which can be used to achieve something similar.

get ':permalink',
  to: 'pages#show',
  constraints: lambda { |request| Page.exists?(permalink: request[:permalink]) }

This effectively defines a top-level route for all pages. In most cases it’s best to place this route near the bottom, so that is has the lowest priority. Here’s the controller action to back this route up:

# app/controllers/pages_controller.rb
...
def show
  @page = Page.find_by(permalink: params[:permalink])
end
...

An advantage of this approach over routing all unmatched requests to a controller action is that the Rails router can still take care of serving the 404 page!

Permalinks with Ruby on Rails

There are many ways to get nice looking permalinks using Ruby on Rails. By default, Rails uses the resource name and its primary key (/articles/1001 for example). I wanted something a little different for my personal website. There are more sophisticated ways to achieve this, but I went with a straight-forward approach using normal routes.

constraints year: /\d{4}/, month: /\w{3}/, day: /\d{1,2}/, permalink: /[\w\-]+/ do
  get 'articles/:year/:month/:day/:permalink', to: 'articles#show', as: :article
end

The other piece that makes this work is an instance method on the Article model.

def url
  [
    "",
    "articles",
    article.created_at.year,
    article.created_at.strftime("%b").downcase,
    article.created_at.day,
    article.permalink
  ] * "/"
end

Now calling @article.url will return a string that can be used to link directly to an article! The controller can use any logic it wants to find the article in the database. If the permalink column is unique in the database, then it could query it directly.

It is safe to expect more

It has been a long time since this domain was home to anything more than an empty page. Things are changing. I am making a commitment to my self to keep this, my personal space on the web, much more than an empty page.