Slide 1

Slide 1 text

Tim Uruski Clio and YYC Ruby @timuruski

Slide 2

Slide 2 text

Structure and Interpretation of Computer Programs Ruby

Slide 3

Slide 3 text

"I need you to export some data."

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

"Actually I need it formatted as JSON."

Slide 6

Slide 6 text

#!/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)

Slide 7

Slide 7 text

"Can you filter it by date range?"

Slide 8

Slide 8 text

#!/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)

Slide 9

Slide 9 text

"We also need the data from other environments."

Slide 10

Slide 10 text

#!/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)

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

#!/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)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

#!/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)

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

#!/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)

Slide 17

Slide 17 text

"One more thing..."

Slide 18

Slide 18 text

Time to refactor

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Execution Dependency

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Execution

Slide 23

Slide 23 text

Execution Dependency

Slide 24

Slide 24 text

at_exit do # script goes here end

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Example

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

#!/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}"

Slide 30

Slide 30 text

#!/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])

Slide 31

Slide 31 text

#!/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])

Slide 32

Slide 32 text

#!/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

Slide 33

Slide 33 text

#!/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

Slide 34

Slide 34 text

#!/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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Confident Ruby http://confidentruby.com/

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Helpful Gist https://git.io/sirp