Natural Born Slugger

A gem for managing composite attributes, especially natural keys and url slugs.

Supports automatically-updated natural keys/slugs. Has ORM and Rack extensions.

Installation

Using bundler, add to your Gemfile:

gem 'natural_born_slugger'

Otherwise:

gem install natural_born_slugger

and load it:

require 'natural_born_slugger'

Usage

Composing Attributes

At its core, NaturalBornSlugger is a simple, well-tested DSL to compose dynamic attributes dependent on other fields:

Person = Struct.new(:first_name, :middle_name, :last_name) do
  include NaturalBornSlugger
  has_slug first_name: nil, middle_name: nil, last_name: nil
end

bambino = Person.new('George', 'Herman', 'Ruth')

bambino.slug #=> "GeorgeHermanRuth"

bambino.first_name, bambino.middle_name = 'Babe', nil

bambino.slug #=> "BabeRuth"

Setting Transformations

NaturalBornSlugger also lets you set tranformations on each dependency:

Person = Struct.new(:id, :name) do
  include NaturalBornSlugger
  has_natural_key name: :dashify, id: :hashify, join_with: '-'
end

buster = Person.new(23, 'Lou Gehrig')

buster.natural_key #=> "lou-gehrig-37693cf"

buster.name = 'The Iron Horse'

buster.natural_key #=> "the-iron-horse-37693cf"

NaturalBornSlugger interprets many objects as valid transformations:

Symbols:
  • Converts dependency to string and sends the symbol tranformation to the String instance.
  • NaturalBornSlugger extends String with two methods:
    1. hashify - an alias for ActiveSupports's parameterize method
    2. dashify - convert string to an MD5 hash and use the first 7 digits
  • Examples: :hashify, :dashify, :capitalize, :chomp, :downcase, :reverse, :squeeze, :strip, :upcase, etc
Strings:
  • Attempts to use the string tranformation as a format string for the dependency.
  • If the string tranformation isn't a format string, it will replace the dependency in the slug, which probably isn't what you want.
  • Examples: id: "%05d", cost: "$%.02f"
Regexps:
  • Scans the dependency using the regex tranformation and joins all matches.
  • Joins matches with the same join_with used in the rest of the slug.
  • Examples: Can't think of any uses
Procs:
  • Passes the dependency into the provided lambda or Proc.
  • Allows custom tranformations. The Proc must accept one parameter and return a string or nil.
  • Examples: compacting arrays of related objects, ie: followers: ->(followers){ "%05d" % followers.count }
nil, false, or anything else:
  • Convert dependency to string and leave untouched.
  • nil is preferred.

Composed Attribute Options

After including NaturalBornSlugger into your class, you can define an composite attribute through one of three methods:

  • has_slug
  • has_natural_key
  • has_composed_attribute

Each of these methods accepts the same arguments: a name and a hash of options. has_slug and has_natural_key will use default names of 'slug' and 'natural_key' respectively if you do not supply one. has_composed_attribute requires the name.

The options hash is where you define the attribute's dependencies. It also uses special keys to customize the attribute's behavior:

:join_with
  • String to use for joining dependencies.
  • Default: ""
  • Example:
    Person = Struct.new(:first_name, :middle_name, :last_name) do
      include NaturalBornSlugger
      has_slug first_name: nil, middle_name: nil, last_name: nil, join_with: '-'
    end

    bambino = Person.new('Babe', nil, 'Ruth')
    bambino.slug #=> "Babe-Ruth"


##### `:require_all`
  - Checks to make sure all attributes exist before composing attribute.
  - Makes attribute return nil until all dependencies exist.
  - Default: false
  - Example:

Person = Struct.new(:first_name, :middle_name, :last_name) do
  include NaturalBornSlugger
  has_slug first_name: nil, middle_name: nil, last_name: nil, require_all: true
end

bambino = Person.new('Babe', nil, 'Ruth')
bambino.slug #=> nil

bambino.first_name, bambino.middle_name = 'George', 'Herman'
bambino.slug #=> "George-Herman-Ruth"

##### `:callback`
  - Provides a callback function `#{attribute_name}_change` to allow you to persist old slugs as you please.
  - Default: false
  - Example:

Person = Struct.new(:first_name, :middle_name, :last_name) do
  include NaturalBornSlugger
  attr_accessor :formerly_known_as
  has_slug first_name: nil, middle_name: nil, last_name: nil, callback: ->(old_name, new_name) do
    self.formerly_known_as ||= []
    self.formerly_known_as << old_name
  end

end

bambino = Person.new('George', 'Herman', 'Ruth')
bambino.slug #=> "GeorgeHermanRuth"

bambino.first_name, bambino.middle_name = 'Babe', nil
bambino.slug #=> "BabeRuth"
bambino.formerly_known_as #=> ["GeorgeHermanRuth"]

##### _Note:_

If you happen to have methods or attributes called `join_with`, `require_all`, or `callback`, you can still build composite attributes out of them. Just use a string instead of a symbol when declaring your attribute's dependencies. `NaturalBornSlugger` only looks for symbols when reading your dependencies for these settings.