Documentation using Yard

Part 26 of building a Rails 7 application

Adding documentation to a project is never the most fun aspect of software development. Ideally the code I write is clean and elegant enough that documentation isn't necessary. However as part of this learning journey I'm going to investigate adding tooling around producing documentation for my classes and methods.

There will no doubt be a feeling of "glad I did this" when I go look back at code I have written some time in the past and it has a nice set of descriptive tags attached to it. I'll be attempting to only add extra comments (above and beyond method and class tagging) that describes why something works the way it does rather than the how. The why could also be "why would I use this class or method or parameter".

There are a few different existing options for this task, the most well known being rdoc (ruby.github.io/rdoc) and yard (yardoc.org). For no specific reason other than I'm generally a fan of what thoughtbot produces I'm going to use yard for this application.

Installation is easy:

gem install yard

I want to use markdown as the markup language so to do that I can add a .yardopts file to the root directory:

.yardopts

--markup markdown

There is also a built in server for viewing the documentation that will be generated. I can run it with "live reloading" so it will update the content as soon as it detects changes in my code.

yard server -r

Now I can start annotating my code. By default on the public methods are documented which makes sense, as these are the only ones a caller should be interested in. Here's a few examples of what I've done so far.

Setting a return on attr_accessors

app/components/breadcrumb/component.rb

module Breadcrumb
  class Component < ViewComponent::Base
    # @return [String] a unique identifier for setting the id on HTML elements in the component
    attr_accessor :id
    # @return [String] the label to display on the breadcrumb
    attr_accessor :label
    # @return [String] the URL to redirect to if the breadcrumb is clicked
    attr_accessor :url
    # @return [Boolean] whether this breadcrumb should be styled as being active
    attr_accessor :active

    def initialize(id:, label:, url:, active: false)
      super
      @id = id
      @label = label
      @url = url
      @active = active
    end

    private

    def active_classes
      active ? 'font-bold text-sky-500 hover:text-sky-700' : 'font-medium text-gray-500 hover:text-gray-700'
    end
  end
end

image.png

Documenting a hash parameter and giving example usage

This is an interesting one as the difficulty in attempting to document it lead to adding a Todo for refactoring the options parameter smell so it is easier to work with.

app/components/button/component.rb

    # @param [Hash] options additional configuration options for the button
    # @option options [String] :title appears when hovering over the button
    # @option options [String] :colour_classes TailwindCSS colour related classes to add
    # @option options [Hash] :icon see {IconsHelper} for more detail
    # @option options [Hash] :data Stimulus related data attributes. `action` is a [String] with each action separated by a space, `params` are a [Hash] with a `name` and `value`
    # @example With Stimulus events and parameters
    #   Button::Component.new(
    #     id: :toggle,
    #     label: 'Toggle',
    #     options: { action: 'click->editor#toggle', params: { name: 'editor-id', value: 1 } }
    #   )
    # @example With an icon
    #   Button::Component.new(
    #     id: :delete,
    #     label: 'Delete',
    #     options: { icon: { name: :trash, colour: :white } }
    #   )
    # @todo Encapsulate the icon and data hashes into their own objects.
    #   This would clean up this nebulous options hash somewhat.
    def initialize(id:, label:, options: {})
      super
      @id = id
      @label = label
      @options = options
    end

image.png

Using @see to provide links to useful external information

app/helpers/icons_helper.rb

module IconsHelper
  # @see https://heroicons.com/ Available Heroicon names
  # @see https://tailwindcss.com/docs/customizing-colors Standard TailwindCSS colours
  # @param [Symbol] name the name of the Heroicon to display
  # @param [Symbol] colour the TailwindCSS colour the icon will use
  # @param [Boolean] active the style of the icon will change depending on whether it is active or not
  # @param [Hash] options additional configuration options for the icon
  # @option options [String] :style The Heroicon style to use.
  #   By default this is "outline". Unsupported alternative is "solid".
  # @option options [String] :size
  # @option options [String] :classes TailwindCSS colour related classes to add
  # @example Basic icon
  #   icon(name: :trash, colour: :white)
  # @example With a different size specified
  #   icon(name: :question_mark_circle, colour: :blue, options: { size: 5 })
  def icon(name:, colour:, active: false, options: {})
    options.reverse_merge!(default_icon_options)
    content_tag(:svg, {
      class: icon_class(classes: options[:classes], active: active, colour: colour, size: options[:size]),
      xmlns: 'http://www.w3.org/2000/svg',
      fill: 'none',
      viewBox: '0 0 24 24',
      stroke: 'currentColor',
      "aria-hidden": 'true'
    }
    ) { safe_join(heroicons[options[:style]][name].map { |d| concat(svg_path_content(draw: d)) }) }
  end

image.png

Marking a method as deprecated

My application will not be rendering a new page for resources, instead the index page knows how to render a modal with a form for creating new resources. I can mark the old new method as deprecated until I can remove it altogether.

app/controllers/concerns/actions.rb

# DRY up controllers by extracting generic action methods that can be overridden if necessary
module Actions
  extend ActiveSupport::Concern

  # GET /collection or /collection.json
  def index; end

  # GET /collection/1 or /collection/1.json
  def show; end

  # @deprecated The UI will render a modal instead from the index page
  def new
    @resource = resource_class.new
  end
  ...
end

image.png

Retrospectively documenting the existing code base will take some time, but all my new public methods and attributes will be documented fully from the start.