Rails Gem files need some help

Fire up a fresh Rails project and you might think everything is clean, fresh, and ready to go.  Just don’t open the Gemfile as it’s a mess.

Here’s what a default Gemfile for a brand-spankin’ new Rails project looks like:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
 
ruby '2.5.1'
 
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'mini_racer', platforms: :ruby
 
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
 
# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'
 
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
 
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false
 
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
 
group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0'
end
 
group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15', '< 4.0'
  gem 'selenium-webdriver'
  # Easy installation and use of chromedriver to run system tests with Chrome
  gem 'chromedriver-helper'
end
 
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Start grouping gems together by functionality (completely arbitrarily) and you might end up with something like this:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
 
ruby '2.5.1'
 
# framework
gem 'rails'
 
# rack/middleware
gem 'rack-cors'
gem 'rack-attack'
 
# Low-Level Data
gem 'pg'
gem 'pg_search'            # search support
# gem 'pghero'               # db-analysis
gem "hiredis"
gem "redis", "~> 4.0"
# gem "aws-sdk-s3", require: false # image storage
gem 'oj'                   # fast JSON parse
 
# High-level Data
gem 'aasm'                 # state-machine support
gem 'valid_email2'         # email validator
gem 'phone'                # phone number parsing & validation
gem 'ensurance'            # adds ability to User.ensure()
gem 'seedbank'             # better data seeds
gem 'activerecord-import', '>= 0.11.0' # bulk insertion of data
gem 'storext'              # store attributes in a jsonb column
 
# main application
gem 'ancestry'             # parent-child relationships for User
gem "aws-sdk-s3", require: false # Document storage
gem "rolify"               # role mgmt
 
# GraphQL
gem 'graphql'
 
# Services
gem 'interactor'           # encapsulate application's business logic
gem 'interactor-contracts' # even better services
 
# utils
gem 'jwt'                  # JSON Web Token
gem 'bcrypt', '~> 3.1.7'   # secure password hashing
gem 'figaro'               # ENV variable management
gem 'recursive-open-struct'
gem 'awesome_print'        # print anything in console
gem 'hirb'                 # Model formatter in the console
gem 'colorize'             # colorize strings
gem 'fast_blank'           # faster .blank? / .present? using C
 
gem 'bootsnap', require: false
gem 'parallel', require: false
 
# web server
gem 'puma'
 
# Logging
# gem 'timber', '~> 2.6'
 
group :development, :test do
  gem 'annotate', github: 'ctran/annotate_models', branch: :develop
  gem 'faker'
  # gem 'letter_opener'
end
 
group :development do
  gem 'pry-rails'
end
 
group :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails'
  gem 'rspec_junit_formatter' # for circleci
end

taken from an actual project

Why can’t the default Rails’ Gemfile be more like that?

You need Ensurance

Update Nov. 2017:  Took these ideas and created a gem Ensurance

 

Been plowing through a massive refactoring of a legacy Rails app.   One of the things I find myself doing all the time now is ensuring.  It’s a handy little concern that works like this:

In the old days, we’d do this:

class SomeClass
  def some_method(person_id)
    person = Person.find(person_id)
 
    # then proceed to do something useful
  end
end

But when testing in the console I was always having to lookup a Person record to get the id, or sometimes I’d remember the ID. Wouldn’t it be nice if I could pass anything(within reason) and be able to ensure that I have a Person record?

[granted…this is Rails specific]

module Ensurable
  extend ActiveSupport::Concern
 
  ## ---
  # Allow you to ensure you have the class you expect... it's similar to
  # result = value.is_a?(Person) ? value : Person.find(value)
  #
  # You can add fields to "ensure_by" (:id is included always)
  # e.g.
  #  if you add `ensure_by :token` to the User class
  #   User.ensure() works
  #   User.ensure(:user_id) works
  #   User.ensure(:token) works
  #
  #  .ensure() returns nil if the record is not found
  #  .ensure!() throws an exception if the record is not found
 
  class_methods do
    def ensure_by(*args)
      @_ensure_by ||= [:id]
      @_ensure_by += [args].flatten
    end
 
    def ensure(thing = nil)
      return nil unless thing.present?
      return thing if thing.is_a?(self)
      found = nil
 
      @_ensure_by ||= [:id]
      @_ensure_by.each do |ensure_field|
        value = thing.try(:fetch, ensure_field.to_sym, nil) || thing.try(:fetch, ensure_field.to_s, nil) || thing
        found = self.find_by(ensure_field => value)
        break if found.is_a?(self)
      end
 
      found
    end
 
    def ensure!(thing = nil)
      result = self.ensure(thing)
      raise ActiveRecord::RecordNotFound unless result
      result
    end
 
  end
end

Now, you can just:

class SomeClass
  def some_method(person)
    person = Person.ensure!(person)
 
    # then proceed to do something useful
  end
end

Once you start using it, you’ll wish all objects would be as friendly.

Example:

class Date
  def self.ensure(thing)
    thing.is_a?(Date) ? thing : Date.parse(thing.to_s)
  end
end

enjoy. 🙂

Update: June 10, 2017

I’ve found these two extensions to Time and Date most helpful:

class Time
  def self.ensure(thing)
    case thing
    when Time
      thing
    when Date
      thing.beginning_of_day
    when Integer, Float
      Time.at(thing)
    when String
      if thing.to_i.to_s == thing
        Time.at(thing.to_i)
      elsif thing.to_f.to_s == thing
        Time.at(thing.to_f)
      else
        Time.parse(thing)
      end
    else
      raise ArgumentError.new("Unknown Type for Time to ensure: #{thing.class.name}")
    end
  end
end
 
class Date
  def self.ensure(thing)
    case thing
    when Date
      thing
    when String
      Date.parse(thing)
    else
      if thing.respond_to?(:to_date)
        thing.to_date
      else       
        raise ArgumentError.new("Unknown Type for Date to ensure: #{thing.class.name}")
      end
    end
  end
end

Iterating a Date Range in Ruby by Day

I wanted to take a date range and iterate over it to produce results by day, but when I went to check the results, there were results by second.

def my_date_thing(a_range)
  a_range.each do |date|
     # something interesting by date
  end
end
 
my_date_thing(10.days.ago .. 0.days.ago)

Turns out, that by default those cool days.ago methods return DateTime objects not Date objects. DateTime objects iterate by seconds, not by days.

But here’s a quick fix: ensure that the range is based on dates first

def my_date_thing(a_range)
  a_range = (a_range.begin.to_date .. a_range.end.to_date)
  a_range.each do |date|
     # something interesting by date
  end
end
 
my_date_thing(10.days.ago .. 0.days.ago)

Now, the range a_range will always iterate by day.

Refactoring Rails: Introduction

This photo is available in my Instacanvas gallery at: http://instacanv.as/bsharpe

A photo posted by Ben Sharpe (@bsharpe) on

I have loved green-fielding apps in Rails for years. I think my first Rails app, a timesheet for my wife’s employees, was in sub 1.0 Rails. I ended up migrating that app through every major version of Rails up to 3.0. Then, she sold her business.

I used to work for a really great consultancy in Seattle, called Ubermind. I was one of two Rails specialists in a sea of iOS devs. It was so much fun to crank our api’s or backend services for client’s iOS apps. My longest term project was a legacy-maintenance gig that eventually turned into a very successful rewrite. Ah, the good times.

Along the way I’ve learned a few tricks of the trade when dealing with legacy apps or migrating to new Rails versions. I hope to add a few of those tips here.

ProTip for Corona: Finding Typos

One of the most common mistakes in programming is typos.   Your brain wants to type one thing and your fingers another.

In Corona (and Lua in general), variables aren’t declared explicitly before you use them.  When  Lua sees a variable name that you haven’t declared as local, it starts looking up the chain of scopes until it reaches the Global namespace (_G).  If it gets there, it creates a key in the _G table with your variable name.

This little trick (shown to me by my friend Chris Blackwell) makes Corona tell you whenever this happens.  Sometimes it’s fine, but most of the time you’ll find a typo (as I did in the Particle Candy library).   I even found that loading the built-in JSON library causes it to check for a global called “Chipmunk.”  Granted, Chipmunk isn’t a typo, but it is accessing a variable that doesn’t exist.  This can happen if you’re check to see if something has been loaded or not.  In those cases this warning can be ignored.

So, put this in your app’s main.lua, run your app in the simulator and watch the console output.  You might just find a problem you didn’t know you had.

Note: You only want this in debug builds.  You could wrap it with a check for running on the simulator, but the best bet is to just remove it before building your final version.

local globalsmeta = {
__index = function(table, key)
print("!!! access to nonexistent global variable "..tostring(key).."n"..debug.traceback())
end
}
setmetatable(_G, globalsmeta)

This works by adding a metatable to the Global namespace to call this function whenever you access an unknown key in its table.

 

CoronaSDK and SublimeText2 Build System

[Update May 15, 2013 –  Fixed launcher for osx and windows]

I’ve recently been trying out different editors as I can see the end of the road for TextMate from where I’m sitting.   I really wanted to be super-awesome and finally learn vi or emacs, but despite several attempts, my brain hasn’t really latched onto them like it did when TM came onto the scene.

Now there’s SublimeText 2 (in beta) which is starting to be the editor that TM wanted to be when it grew up.  There’s still a lot of funky things with it, but I can see the potential.  Not to mention it’s cross platform like the ancient gods vi and emacs.   I suppose that’s really just icing on the cake because I rarely leave my home platform for reasons other than duress (Windows) or curiosity (Linux).

The one thing I really missed from TM was the ability to launch the CoronaSDK simulator from the editor.   It’s fantastic to be able to edit a file and hit a button and have it fire up the simulator and then start reloading the simulator each time the file changes. (actually the simulator does that work for you)  I took a look at the Corona bundle for TM and found quite a few lines of AppleScript that are doing the work (i kid not).

tl;dr

In Sublime, select “Tools > Build System > New Build System…”

This opens up a new text file.  Copy and paste this into it:

{
"osx" : {
"cmd": ["/Applications/CoronaSDK/Corona Simulator.app/Contents/MacOS/Corona Simulator","main.lua"]
},
"windows" : {
"cmd": ["C:\Program Files\Corona Labs\Corona SDK\Corona Simulator.exe","main.lua"]
},
"file_regex": "^[ ]*File "(...*?)", line ([0-9]*)",
"working_dir": "${project_path:${folder}}",
"selector": "source.lua"
}

By setting the working_dir to the project folder and specifying ‘main.lua’ as the file, you can launch the simulator for a project no matter what file you happen to be looking at (or in what subfolder).

Save the file as “CoronaSDK.sublime-build”

  1. Go to your Corona project
  2. Open a file to edit
  3. Select  Tools > Build System > CoronaSDK
  4. Press Command-B
  5. profit!

For Reference,  custom build system files are usually stored in the following locations:

OS X: ~/Library/Application Support/Sublime Text 2/Packages/User

Windows: %APPDATA%/Sublime Text 2/Packages/User

Amazon's Kindle Fire

CoronaSDK: Building and installing on a Kindle Fire

Amazon's Kindle Fire

  1. Build the app using  File > Build > Amazon/Kindle…
  2. On the Fire, go to Settings and tap More
  3. Tap Device
  4. Turn ON “Allow Installation of Applications — from unknown sources”
  5. Plug in a Micro-B usb cable into the Fire and the other into your computer (in this case a MacBook Air)
  6. The Kindle should be showing up on your desktop
  7. Copy the .apk file generated by Corona onto the Kindle (I’m not sure that it makes a difference where — I used the “downloads” folder)
  8. Eject the Kindle from the Mac
  9. On the Kindle you’ll need a File management application installed (I used “ES File Explorer” because it was free and had the most reviews)
  10. Use the File manager to find your app and tap it to install it
  11. Profit!

CoronaSDK: How to know if a file exists?

Update: Dejay was correct, you have to try and open the file to know for sure if it’s there or not.

Want to know if a file exists before you use it?

Lately, I’ve been wanting something a little more dynamic in my projects for opening various Scenes (think Director, but not).   I needed a way of looking to see if a file existed or not before I tried to load it up.  Here’s the result:

function fileExists(fileName, base)
  assert(fileName, "fileName is missing")
  local base = base or system.ResourceDirectory
  local filePath = system.pathForFile( fileName, base )
  local exists = false
 
  if (filePath) then -- file may exist. won't know until you open it
    local fileHandle = io.open( filePath, "r" )
    if (fileHandle) then -- nil if no file found
      exists = true
      io.close(fileHandle)
    end
  end
 
  return(exists)
end

Typical usage:

if fileExists("myGame.lua") then
  -- do something wonderful
end

If the file does not exist, the CoronaSDK library function system.pathForFile() returns nil.  It also returns a Warning in the console, which can be safely ignored.

By default it checks your app’s asset directory (system.ResourceDirectory), however, if you’d like to check if a file you created exists you can pass in an alternative base path like so:

if fileExists("some_file.txt", system.DocumentsDirectory) then
  -- do something wonderful
end

The three valid options for base path are:

  1. system.ResourceDirectory
  2. system.DocumentsDirectory
  3. system.TemporaryDirectory