Today I Learned Notes to self about software development

    Rails Rollback to Specific Migration

    Occassionally, when working on an app with a lot of active branches that affect the database I run into an issue where I can’t rollback.

    I’ll get an error like this:

    rails aborted! ActiveRecord::UnknownMigrationVersionError:

    No migration with version number 20230620205505.

    This usually happens when I switch to another branch and I want to rollback a change I made to update/remove the migration. I assume the migration referenced in database doesn’t exist on the current branch.

    If this happens, you can roll back to the last migration on the branch with:

    rails db:migrate:down VERSION=n
    

    where n is the timestamp from the latest migration (something like 20230607135355).

    This is only applicable for non-sqlite3 databases, since the database doesn’t live in the project directory.

    Hiccup with Rails 7 Generation

    The server won’t work out of the box if you run the generate command with npm version < 7.1.

    In particular, with < 7.1, you have to add the build commands to the package.json scripts yourself.

    See this SO answer for more details.

    I had mistakenly generated an app with a npm v6.8 b/c I used n to switch NodeJS versions to debug a student assignment and apparently never switched back.

    I sure hope that hasn’t been the cause of other issues I ran into 😅

    Redirecting stderr to stdout

    Normally if you run a file from inside another script, it can be tricky to get get error message sometimes.

    For example:

    # test.rb
    
    
    
    p("{}"
    

    3.2.1 :001 > `ruby test.rb`
    test.rb: --> test.rb
    Unmatched `(', missing `)' ?          
    > 4  p("{}"                           
    test.rb:4: syntax error, unexpected end-of-input, expecting ')' (SyntaxError)
    p("{}"                                
          ^                               
                                          
     => ""                                
    3.2.1 :002 > 
    

    The error message still is displayed in the shell, but it’s missing from the return value, which is the important thing if we’re running this inside another script/app.

    The output is being displayed through stderr and not stdout.

    There’s a trick you can do to merge stderr into stdout.

    ruby test.rb 2>&1 which looks a little silly.

    but now you get the error in the return value:

    3.2.1 :004 > `ruby test.rb 2>&1`
     => "test.rb: --> test.rb\nUnmatched `(', missing `)' ?\n> 4  p(\"{}\"\ntest.rb:4: syntax error, unexpected end-of-input, expecting ')' (SyntaxError)\np(\"{}\"\n      ^\n\n" 
    

    A breakdown of how it works:

    File descriptor 1 is the standard output (stdout).

    File descriptor 2 is the standard error (stderr).

    At first, 2>1 may look like a good way to redirect stderr to stdout. However, it will actually be interpreted as “redirect stderr to a file named 1”.

    & indicates that what follows and precedes is a file descriptor, and not a filename. Thus, we use 2>&1. Consider >& to be a redirect merger operator.

    Better Rspec tests with shared examples

    TIL about shared examples in Rspec!

    They’re primarily good for reducing duplicate code.

    They work well with subject and let.

    This is a made up example:

    describe "Todos" do
      context 'when on the edit page' do
        subject { Todo.create(title: "Day 1", body: "Hello, World!") }
        let(:path) { edit_todo_path(subject) }
    
        it "displays form" do
          visit path
    
          expect(page).to have_css("form")
        end
        
        it "redirects after submitting update form" do
          visit path
    
          click_on "Update #{subject.class}"
    
          expect(page.current_url).not_to be path
        end
      end
    end
    

    You could write very similar tests for other models.

    With shared examples you could do something like this:

    shared_examples_for "Editing a resource" do
      it "displays form" do
        visit path
        expect(page).to have_css("form")
      end
    
      it "redirects after submitting update form" do
        visit path
        click_on "Update #{subject.class}"
        expect(page.current_url).not_to be path
      end
    end
    
    describe "Todos" do
      context 'when on the edit page' do
        subject { Todo.create(title: "Day 1", body: "Hello, World!") }
        let(:path) { edit_todo_path(subject) }
    
        it_behaves_like "Editing a resource"
      end
    end
    

    Then in the future when you need a test that does something similar, you can make/reference a a shared example group.

    DRY with dry_config gem

    I was curious about how you could make gems have configuration settings (like from an initializer). I finally had a use case when writing appdev_support.

    My goal was to be able to configure the gem like this:

    # config/initializers/appdev_support.rb
    AppdevSupport.config do |config|
      config.action_dispatch = true;
      config.active_record   = true;
    end
    

    and have these settings determine which class of method overrides should be loaded.

    Previously, I was defining attr_writers like this

    module AppdevSupport
      class << self
        attr_writer :active_record, :action_dispatch
    
        def action_dispatch
          @action_dispatch || true
        end
    
        def active_record
          @active_record || true
        end
      end
      # ...
    end
    

    Note to self, I still don’t really understand what class << self does or how it works (I think it makes a Singleton somehow?) but with that change I could dynamically load files by calling a class method:

       def self.init
        if @active_record
          load "appdev_support/active_record/delegation.rb"
          load "appdev_support/active_record/attribute_methods.rb"
          load "appdev_support/active_record/relation/to_s.rb"
        end
        if @action_dispatch
          load "appdev_support/action_dispatch/request/session/fetch.rb"
          load "appdev_support/action_dispatch/request/session/store.rb"
          load "appdev_support/action_dispatch/cookies/cookie_jar/fetch.rb"
          load "appdev_support/action_dispatch/cookies/cookie_jar/store.rb"
        end
      end
    

    I wanted to make adding additional config settings easier to do and more seemless (like not having to run AppdevSupport.init if you just wanted the defaults).

    TIL about the dry-configurable gem, which has been around for a while!

    It really cleans up the “defining an instance variable/attr_writer” process and makes it a lot easier to define nested structures too!

    require 'dry-configurable'
    
    module App
      extend Dry::Configurable
    
      setting :api_url
      setting :repository, reader: true do
        # Can pass a default value
        setting :type, default: :local
        setting :encryption do
          setting :cipher, default: 'aes-256-cbc'
        end
      end
    end
    
    App.config.api_url = 'https://jelani.dev'
    App.config.api_url # => 'https://jelani.dev'
    

    This let me start to refactor the gem:

    module AppdevSupport
      extend Dry::Configurable
      setting :active_record,   default: true
      setting :action_dispatch, default: true
      setting :pryrc,           default: :minimal
      # ...
    end
    

    While this was more concise and flexible, I still wasn’t able to get the defaults to load without calling AppdevSupport.init like before.

    In searching for answers I found a post that taught me more responsible ways to define Monkeypatches and I incorperated some of those techniques into the gem as well.