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_accessor
s
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
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
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
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
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.