$30 off During Our Annual Pro Sale. View Details »

Structure and Interpretation of Ruby Programs

Tim Uruski
December 06, 2016

Structure and Interpretation of Ruby Programs

A lightning talk about how to structure one-off scripts that don't turn into a huge mess as features are added.

Tim Uruski

December 06, 2016
Tweet

More Decks by Tim Uruski

Other Decks in Programming

Transcript

  1. Tim Uruski
    Clio and YYC Ruby
    @timuruski

    View Slide

  2. Structure and
    Interpretation of
    Computer Programs
    Ruby

    View Slide

  3. "I need you to
    export some data."

    View Slide

  4. #!/usr/bin/env ruby
    require 'cheezburger'
    db = DatabaseWrapper.default_connection
    user_id = ARGV.shift
    locations = LocationStream.new(db, user_id)
    puts locations

    View Slide

  5. "Actually I need it
    formatted as JSON."

    View Slide

  6. #!/usr/bin/env ruby
    require 'cheezburger'
    require 'json'
    db = DatabaseWrapper.default_connection
    user_id = ARGV.shift
    locations = LocationStream.new(db, user_id)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => [location.lng, location.lat]
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  7. "Can you filter it
    by date range?"

    View Slide

  8. #!/usr/bin/env ruby
    require 'cheezburger'
    require 'json'
    db = DatabaseWrapper.default_connection
    user_id = ARGV.shift
    query_date = ARGV.shift || Tume.now
    min_recorded_at = query_date.beginning_of_day.utc.to_i * 1000
    max_recorded_at = query_date.end_of_day.utc.to_i * 1000
    query = {
    order_by: 'recorded_at',
    start_at: min_recorded_at,
    end_at: end_recorded_at
    }
    locations = LocationStream.new(db, user_id, query)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => [location.lng, location.lat]
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  9. "We also need the data
    from other environments."

    View Slide

  10. #!/usr/bin/env ruby
    require 'cheezburger'
    require 'json'
    if database_url = ENV.fetch('DATABASE_URL')
    db = DatabaseWrapper.new(database_url)
    else
    db = DatabaseWrapper.default_connection
    end
    user_id = ARGV.shift
    query_date = ARGV.shift || Tume.now
    min_recorded_at = query_date.beginning_of_day.utc.to_i * 1000
    max_recorded_at = query_date.end_of_day.utc.to_i * 1000
    query = {
    order_by: 'recorded_at',
    start_at: min_recorded_at,
    end_at: end_recorded_at
    }
    locations = LocationStream.new(db, user_id, query)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => [location.lng, location.lat]
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  11. "Can I just give you
    the user's email address
    instead of that weird ID?"

    View Slide

  12. #!/usr/bin/env ruby
    require 'cheezburger'
    require 'json'
    if database_url = ENV.fetch('DATABASE_URL')
    db = DatabaseWrapper.new(database_url)
    else
    db = DatabaseWrapper.default_connection
    end
    identifier = ARGV.shift
    begin
    user = UserStore.new(db).find(identifier)
    rescue NoUserFound
    warn "No user found for #{identifier}"
    exit 1
    end
    query_date = ARGV.shift || Tume.now
    min_recorded_at = query_date.beginning_of_day.utc.to_i * 1000
    max_recorded_at = query_date.end_of_day.utc.to_i * 1000
    query = {
    order_by: 'recorded_at',
    start_at: min_recorded_at,
    end_at: end_recorded_at
    }
    locations = LocationStream.new(db, user.id, query)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => [location.lng, location.lat]
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  13. "Can you also filter
    by X, Y and Z fields?"

    View Slide

  14. #!/usr/bin/env ruby
    require 'cheezburger'
    require 'json'
    window_size = 10
    stay_radius = 500
    activity_threshold = 2.5
    OptionParser.new do |opts|
    window_desc = 'Window size in minutes, default: 5'
    opts.on('-wSIZE', '--window=SIZE', window_desc, Integer) do |value|
    window_size = value * 60
    end
    activity_desc = 'Activity within window threshold, default: 23'
    opts.on('-aCOUNT', '--activity=COUNT', activity_desc, Integer) do |value|
    activity_threshold = value
    end
    radius_desc = 'Size of stay radius in metres, default: 50.0'
    opts.on('-rSIZE', '--radiusSIZE', radius_desc, Float) do |value|
    stay_radius = value
    end
    end.parse!
    if database_url = ENV.fetch('DATABASE_URL')
    db = DatabaseWrapper.new(database_url)
    else
    db = DatabaseWrapper.default_connection
    end
    identifier = ARGV.shift
    begin
    user = UserStore.new(db).find(identifier)
    rescue NoUserFound
    warn "No user found for #{identifier}"
    exit 1
    end
    query_date = ARGV.shift || Tume.now
    min_recorded_at = query_date.beginning_of_day.utc.to_i * 1000
    max_recorded_at = query_date.end_of_day.utc.to_i * 1000
    query = {
    order_by: 'recorded_at',
    start_at: min_recorded_at,
    end_at: end_recorded_at
    }
    locations = LocationStream.new(db, user.id, query)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => [location.lng, location.lat]
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  15. "How do I use that script
    you wrote for exports?"

    View Slide

  16. #!/usr/bin/env ruby
    require 'json'
    require 'optparse'
    require 'bundler/setup'
    require 'active_support'
    require 'active_support/core_ext/time'
    require_relative '../lib/cheezburger'
    include Cheezburger
    window_size = 10
    stay_radius = 500
    activity_threshold = 2.5
    OptionParser.new do |opts|
    window_desc = 'Window size in minutes, default: 5'
    opts.on('-wSIZE', '--window=SIZE', window_desc, Integer) do |value|
    window_size = value * 60
    end
    activity_desc = 'Activity within window threshold, default: 23'
    opts.on('-aCOUNT', '--activity=COUNT', activity_desc, Integer) do |value|
    activity_threshold = value
    end
    radius_desc = 'Size of stay radius in metres, default: 50.0'
    opts.on('-rSIZE', '--radiusSIZE', radius_desc, Float) do |value|
    stay_radius = value
    end
    end.parse!
    if ARGV.empty?
    warn "Usage: [DATABASE_URL=] #{$0} [2016-02-01]"
    exit 1
    end
    user_id = args.shift
    date = args.shift
    database_url = ENV.fetch('DATABASE_URL')
    database = DatabaseWrapper.new(database_url)
    def find_user(database, identifier)
    begin
    user = UserStore.new(database).find(identifier)
    rescue NoUserFound
    warn "No user found for #{identifier}"
    exit 1
    end
    end
    def fetch_locations(database, user, query_date)
    min_recorded_at = query_date.beginning_of_day.utc.to_i * 1000
    max_recorded_at = query_date.end_of_day.utc.to_i * 1000
    query = { order_by: 'recorded_at', start_at: min_recorded_at, end_at: end_recorded_at }
    begin
    LocationStream.new(database, user, query)
    rescue NoLocationsFound
    export_date = query_date.strftime('%Y-%m-%d')
    warn "No locations found for #{export_date}"
    exit 1
    end
    end
    def to_coords(location)
    [location.lng, location.lat]
    end
    user = find_user(database, options[:user_id])
    query_date = options[:date] ? Time.parse(options[:date]) : Time.now
    locations = fetch_locations(database, user, query_date)
    geojson = {
    "type" => "FeatureCollection",
    "features" => locations.map { |location|
    {
    "type" => "Feature",
    "geometry" => {
    "type" => "Point",
    "coordinates" => coords(location)
    }
    "properties" => {
    "name": "location.name"
    }
    }
    }
    }
    end
    print JSON.pretty_generate(geojson)

    View Slide

  17. "One more thing..."

    View Slide

  18. Time to refactor

    View Slide

  19. "I'll extract some methods,
    that's a good trick!"

    View Slide

  20. Execution
    Dependency

    View Slide

  21. Inverted Pyramid
    Headline
    Who? What? Where? When?
    Background Details
    General Info

    View Slide

  22. Execution

    View Slide

  23. Execution
    Dependency

    View Slide

  24. at_exit do
    # script goes here
    end

    View Slide

  25. at_exit do
    opts = parse_opts
    result = frobulate(opts)
    puts result
    end

    View Slide

  26. 1.Wrap the script core in at_exit
    2.Organize dependencies downwards
    3.Parse options in one place

    View Slide

  27. Example

    View Slide

  28. #!/usr/bin/env ruby
    puts "Hello, #{ARGV.shift || 'world'}"

    View Slide

  29. #!/usr/bin/env ruby
    require 'optparse'
    name = 'world'
    OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet') do |value|
    name = value
    end
    end.parse!
    puts "Hello, #{name}"

    View Slide

  30. #!/usr/bin/env ruby
    require 'optparse'
    def greet(name)
    puts "Hello, #{name}"
    end
    options = {
    name: 'world'
    }
    OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet') do |value|
    options[:name] = value
    end
    end.parse!
    greet(options[:name])

    View Slide

  31. #!/usr/bin/env ruby
    require 'optparse'
    def greet(name)
    puts "Hello, #{name}"
    end
    options = {
    name: 'world'
    }
    OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet') do |value|
    options[:name] = value
    end
    end.parse!
    greet(options[:name])

    View Slide

  32. #!/usr/bin/env ruby
    require 'optparse'
    at_exit do
    options = parse_options
    greet(options[:name])
    end
    def parse_options(args = ARGV)
    {
    name: 'world'
    }.tap do |options|
    parser = OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet', String) do |value|
    options[:name] = value
    end
    end
    parser.parse!(args)
    end
    end
    def greet(name)
    puts "Hello, #{name}"
    end

    View Slide

  33. #!/usr/bin/env ruby
    require 'optparse'
    at_exit do
    options = parse_options
    greet(options[:name])
    end
    def parse_options(args = ARGV)
    {
    name: 'world'
    }.tap do |options|
    parser = OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet', String) do |value|
    options[:name] = value
    end
    end
    parser.parse!(args)
    end
    end
    def greet(name)
    puts "Hello, #{name}"
    end

    View Slide

  34. #!/usr/bin/env ruby
    require 'optparse'
    at_exit do
    options = parse_options
    greet(options[:name])
    end
    def parse_options(args = ARGV)
    {
    name: 'world'
    }.tap do |options|
    parser = OptionParser.new do |opts|
    opts.on('-n=NAME', '--name=NAME', 'Name to greet', String) do |value|
    options[:name] = value
    end
    end
    parser.parse!(args)
    end
    end
    def greet(name)
    puts "Hello, #{name}"
    end

    View Slide

  35. 1.Wrap the script core in at_exit
    2.Organize dependencies downwards
    3.Parse options in one place

    View Slide

  36. Confident Ruby
    http://confidentruby.com/

    View Slide

  37. Structuring Ruby Scripts
    http://timuruski.net/blog/2016/structuring-ruby-scripts/

    View Slide

  38. Helpful Gist
    https://git.io/sirp

    View Slide