#+title:  Programming in Ruby
#+author: Howard X. Abrams
#+date:   2022-09-01
#+tags: emacs ruby programming

A literate programming file for configuring Emacs to support the Ruby programming language.

#+begin_src emacs-lisp :exports none
  ;;; ha-programming-ruby --- Ruby configuration. -*- lexical-binding: t; -*-
  ;;
  ;; © 2022-2023 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams
  ;; Created: September  1, 2022
  ;;
  ;; While obvious, GNU Emacs does not include this file or project.
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            ~/src/hamacs/ha-programming-ruby.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
  #+end_src

* Getting Started
Ruby is probably already installed on the system, and if not, we certainly can download it from [[https://www.ruby-lang.org/en/downloads/][ruby-lang.org]], but since I need to juggle different versions for each project, I use [[https://direnv.net/docs/ruby.html][direnv]] and [[https://www.ruby-lang.org/en/documentation/installation/#ruby-install][ruby-install]]:
#+begin_src sh
  brew install ruby-install
#+end_src
And then install one or more versions:
#+begin_src sh
  ruby-install -U
  ruby-install ruby 3
#+end_src

** New Project
While we /could/ use a large project templating system, I keep it simple. For each project, create the following directory structure:
#+begin_example
├── Gemfile
├── Rakefile
├── lib
│   └── hello_world.rb
└── test
    └── hello_world_test.rb
#+end_example
For instance:
#+begin_src sh
  mkdir -p ~/src/ruby-xp    # Change me
  cd ~/src/ruby-xp
  mkdir -p lib test
#+end_src

Now, do the following steps.

  1. Create a =.envrc= file with the Ruby you want to use:
     #+begin_src sh
     use ruby 2.6.10
     #+end_src

  2. Next, get Bundler:
     #+begin_src sh
     gem install bundle
     #+end_src

  3. Create a minimal =Gemfile=:
     #+begin_src ruby :tangle ~/src/ruby-xp/Gemfile
       source 'https://rubygems.org'

       gem 'rake', group: :development
       gem 'rubocop', group: :development
       gem 'solargraph', group: :development
     #+end_src

  4. Grab all the dependencies:
     #+begin_src sh
       bundle install
     #+end_src

  5. Create a minimal =Rakefile=:
     #+begin_src ruby :tangle ~/src/ruby-xp/Rakefile
       task default: %w[test]

       task :run do
         ruby 'lib/hello_world.rb'
       end

       task :test do
         ruby 'test/hello_world_test.rb'
       end
     #+end_src

  6. Create the first program:
     #+begin_src ruby :tangle ~/src/ruby-xp/lib/hello_world.rb
       # frozen_string_literal: true

       # The basic greeting class
       class HelloWorld
         attr_reader :name

         def initialize(name = nil)
           @name = name
         end

         def greeting
           if @name
             "Hello there, #{@name}"
           else
             'Hello World!'
           end
         end
       end

       puts HelloWorld.new(ARGV.first).greeting
     #+end_src

  7. Create the first test:
     #+begin_src ruby :tangle ~/src/ruby-xp/test/hello_world_test.rb
       require 'test/unit'
       require_relative '../lib/hello_world'

       class TestHelloWorld < Test::Unit::TestCase
         def test_default
           assert_equal 'Hello World!', HelloWorld.new.greeting
         end

         def test_name
           assert_equal 'Hello there, Bob', HelloWorld.new('Bob').greeting
         end
       end
     #+end_src
Or something like that.

** Existing Projects
For projects from work, I have found we need to isolate the Ruby environment. Once we use [[https://github.com/rbenv/rbenv][rbenv]] or [[https://rvm.io/][rvm]], but now, I just use [[https://direnv.net/docs/ruby.html][direnv]].  Approach one is to use a local directory structure. Assuming I have a =use_ruby= function in [[file:~/.config/direnv/direnvrc][~/.config/direnv/direnvrc]]:
#+begin_src conf
  # From Apple: /usr/bin/ruby
  use ruby 2.6.10
  # Could use 3.2.1 from /opt/homebrew/bin/ruby
#+end_src

A better solution is to create a container to hold the Ruby environment. Begin with a =Dockerfile=:
#+begin_src dockerfile :tangle ~/src/ruby-xp/Dockerfile
  ## -*- dockerfile-image-name: "ruby-xp" -*-
  FROM alpine:3.13

  ENV NOKOGIRI_USE_SYSTEM_LIBRARIES=1

  ADD Gemfile /

  RUN apk update \
  && apk add ruby \
             ruby-etc \
             ruby-bigdecimal \
             ruby-io-console \
             ruby-irb \
             ca-certificates \
             libressl \
             bash \
  && apk add --virtual .build-dependencies \
             build-base \
             ruby-dev \
             libressl-dev \
  && gem install bundler || apk add ruby-bundler \
  && bundle config build.nokogiri --use-system-libraries \
  && bundle config git.allow_insecure true \
  && gem install json \
  && bundle install \
  && gem cleanup \
  && apk del .build-dependencies \
  && rm -rf /usr/lib/ruby/gems/*/cache/* \
            /var/cache/apk/* \
            /tmp/* \
            /var/tmp/*
#+end_src

Next, create a =.envrc= in the project’s directory:
#+begin_src sh :tangle ~/src/ruby-xp/.envrc
  CONTAINER_NAME=ruby-xp:latest
  CONTAINER_WRAPPERS=(bash ruby irb gem bundle rake solargraph rubocop)

  container_layout
#+end_src
While that approach works /fairly well/ with [[file:ha-programming.org::*direnv][my direnv configuration]], [[file:ha-programming.org::*Flycheck][Flycheck]] seems to want the checkers to be installed globally.
* Configuration
While Emacs supplies a Ruby editing environment, we’ll still use =use-package= to grab the latest:
#+begin_src emacs-lisp
  (use-package ruby-mode
    :mode (rx "." (optional "e") "rb" eos)
    :mode (rx "Rakefile" eos)
    :mode (rx "Gemfile" eos)
    :mode (rx "Berksfile" eos)
    :mode (rx "Vagrantfile" eos)
    :interpreter "ruby"

    :init
    (setq ruby-indent-level 2
          ruby-indent-tabs-mode nil)

    :hook (ruby-mode . superword-mode))
#+end_src
** Ruby REPL
  I am not sure I can learn a new language without a REPL connected to my editor, and for Ruby, this is [[https://github.com/nonsequitur/inf-ruby][inf-ruby]]:
  #+BEGIN_SRC elisp
    (use-package inf-ruby
      :config
      (ha-local-leader 'ruby-mode-map
        "R" '("REPL" . inf-ruby)))
  #+END_SRC
** Electric Ruby
The [[https://melpa.org/#/ruby-electric][ruby-electric]] project is a minor mode that aims to add the /extra syntax/ when typing Ruby code.
#+begin_src emacs-lisp :tangle no
  (use-package ruby-electric
    :hook (ruby-mode . ruby-electric-mode))
#+end_src
** Testing
The [[https://github.com/r0man/ruby-test-mode][ruby-test-mode]] project aims a running Ruby test from Emacs seemless:
#+begin_src emacs-lisp
  (use-package ruby-test-mode
    :hook (ruby-mode . ruby-test-mode)

    :config
    (ha-local-leader 'ruby-mode-map
      "t"  '(:ignore t :which-key "test")
      "t t" '("test one" . ruby-test-run-at-point)
      "t g" '("toggle code/test" . ruby-test-toggle-implementation-and-specification)
      "t A" '("test all" . ruby-test-run)
      "t a" '("retest" . ruby-test-rerun)))
#+end_src
** Bundler
The [[https://github.com/endofunky/bundler.el][Bundler project]] integrates [[https://bundler.io/][bundler]] to install a projects Gems.

#+begin_src emacs-lisp
  (use-package bundler
    :config
    (ha-local-leader 'ruby-mode-map
      "g"  '(:ignore t :which-key "bundler")
      "g o" '("open" . bundle-open)
      "g g" '("console" . bundle-console)
      "g c" '("check" . bundle-check)
      "g i" '("install" . bundle-install)
      "g u" '("update" . bundle-update)))
#+end_src


** Rubocop?
  The lint-like style checker of choice for Ruby is [[https://github.com/bbatsov/rubocop][Rubocop]]. The [[https://github.com/bbatsov/rubocop-emacs][rubocop.el]] mode should work with [[https://github.com/flycheck/flycheck][Flycheck]]. First install it with:
#+begin_src sh
  gem install rubocop
#+end_src
And then we may or may not need to enable the =rubocop-mode=:
  #+BEGIN_SRC elisp :tangle no
    (use-package rubocop
      :hook (ruby-mode . rubocop-mode))
  #+END_SRC
* Auxiliary Support
** Cucumber
Seems that to understand and edit Cucumber /feature/ definitions, you need [[https://github.com/michaelklishin/cucumber.el][cucumber.el]]:
#+begin_src emacs-lisp
  (use-package feature-mode)
#+end_src
** RSpec
https://github.com/pezra/rspec-mode
* LSP
The built-in Eglot package natively supports [[https://github.com/castwide/solargraph][Solargraph]] via =eglot-server-programs= (no extra package needed):

#+begin_src emacs-lisp
  (use-package eglot
    :straight nil
    :after ruby-mode
    :defer t
    :hook (ruby-mode . eglot-ensure))
#+end_src

Keybindings for the eglot-based LSP sub-menu, wired into the =ruby-mode= local leader:

#+begin_src emacs-lisp
  (use-package eglot
    :straight nil
    :after ruby-mode
    :config
    (ha-local-leader 'ruby-mode-map
      "w"  '(:ignore t :which-key "eglot")
      "wD" '("disconnect" . eglot-shutdown)
      "wR" '("reconnect" . eglot-reconnect)
      "w?" '("describe" . eglot-show-workspace-configuration)
      "wa" '("code actions" . eglot-code-actions)
      "wo" '("organize imports" . eglot-code-action-organize-imports)
      "wr" '("rename" . eglot-rename)
      "w=" '("format buffer" . eglot-format-buffer)))
#+end_src

Need to install [[https://github.com/castwide/solargraph][Solargraph]] for the LSP server experience:
#+begin_src sh
  gem install solargraph
#+end_src
Or add it to your =Gemfile=:
#+begin_src ruby
  gem 'solargraph', group: :development
#+end_src
Once installed, [[*eglot][eglot]] finds and talks to =solargraph= automatically; no further Emacs configuration is needed.
* Technical Artifacts                                :noexport:

Let's =provide= a name so we can =require= this file:

#+begin_src emacs-lisp :exports none
  (provide 'ha-programming-ruby)
  ;;; ha-programming-ruby.el ends here
  #+end_src

#+description: configuring Emacs to support the Ruby programming language.

#+property:    header-args:sh :tangle no
#+property:    header-args:emacs-lisp  :tangle yes
#+property:    header-args    :results none :eval no-export :comments no mkdirp yes

#+options:     num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options:     skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt:  view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
