SlideShare a Scribd company logo
1 of 47
Building high-performance APIs for
the video game industry with Goliath,
Grape, and EventMachine
Matt E. Patterson
Digimonkey Studios
Monday, June 10, 13
Matt Patterson
Software Consultant
(Ruby, Rails, Agile, etc.)
Web software since 1998 (ColdFusion and
PHP)
Ruby and Rails since 2006 (Rails 1.0)
Monday, June 10, 13
Game Web Services
Must be fast.
Must be reliable.
Must be scalable...
...and able to handle sudden bursts
(launches, new DLC, etc.)
Monday, June 10, 13
Goliath
open-source, non-blocking, asychronous Ruby web server
framework from postrank-labs
EventMachine reactor.
HTTP parser, Rack, Ruby 1.9+, Ruby Fibers
each request executes in its own Fiber
Monday, June 10, 13
Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
Monday, June 10, 13
Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
Monday, June 10, 13
Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
Monday, June 10, 13
Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/
Monday, June 10, 13
Ubiquitous Goliath
“Hello, world” example
require 'goliath'
class Hello < Goliath::API
def response(env)
[ 200, {}, "Hello, world!" ]
end
end
$ ruby hello.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/
Hello, world!
Monday, June 10, 13
What I needed...
a full-featured API
versioned endpoints
multiple resources
standard RESTful CRUD
JSON requests / responses
secured requests
logging
localization / translation
file attachments with S3
storage
asynchronous MySQL
queries
tests!
Monday, June 10, 13
What I used...
Grape
REST-like API micro-
framework for Ruby
Carrierwave, Fog, MiniMagick
file uploads and S3 support
Rspec + FactoryGirl
testing
Rabl + MultiJson
JSON requests / responses
em-synchrony + mysql2
async MySQL
globalize3
localization / translation
standalone_migrations
Rails-style migrations
capistrano
deployment stuff
Monday, June 10, 13
Simple Goliath + Grape
./app.rb
require 'rubygems'
require 'bundler/setup'
require 'goliath'
require 'em-synchrony/activerecord'
require 'grape'
Dir["./app/models/*.rb"].each { |f| require f }
require './app/api'
class Application < Goliath::API
def response(env)
::API.call(env)
end
end
Monday, June 10, 13
Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
Simple Goliath + Grape
./app/apis/v1/unlocks.rb
class APIv1
class Unlocks < Grape::API
version 'v1', using: :path, format: :json
resource :unlocks do
# GET /unlocks/1.json
desc "Returns a single Unlock record by ID"
get "/:id" do
unlock = Unlock.find(params[:id])
custom_render "api_v1/unlocks/show", unlock, 200
end
end
end
end
Monday, June 10, 13
Simple Goliath + Grape
./app/apis/v1/unlocks.rb
class APIv1
class Unlocks < Grape::API
version 'v1', using: :path, format: :json
resource :unlocks do
# GET /unlocks/1.json
desc "Returns a single Unlock record by ID"
get "/:id" do
unlock = Unlock.find(params[:id])
custom_render "api_v1/unlocks/show", unlock, 200
end
end
end
end
Wait, what?
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
back to our ./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
helpers do
def custom_render(rabl_template, object, status, args={})
args[:format] ||= 'json'
args[:success] ||= true
render_options = { format: args[:format] }
render_options[:locals] = args[:locals] if args[:locals]
data = Rabl::Renderer.new(rabl_template, object, render_options).render
%({ "success": #{args[:success]}, "data": #{data} })
end
end
mount APIv1::Unlocks
...
Monday, June 10, 13
Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
Monday, June 10, 13
Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
Monday, June 10, 13
Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
renders =>
Monday, June 10, 13
Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
renders =>
{ "id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000 }
Monday, June 10, 13
Simple Goliath + Grape
./app/views/api_v1/unlocks/show.json.rabl
attributes :id, :name, :code, :description, :created_at
node(:unique_tags) { |unlock| unlock.tags.uniq }
child :images => :images do
attributes :id, :caption, :mime_type, :url
end
renders =>
{ "success": true, "data": {"id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000} }
Monday, June 10, 13
Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
Monday, June 10, 13
Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
require 'rocket_tag'
class Unlock < ActiveRecord::Base
has_many :images, as: :image_attachable, dependent: :destroy
attr_taggable :tags
validates :name, presence: true
validates :code, presence: true
end
Monday, June 10, 13
Simple Goliath + Grape
./app/models/unlock.rb
Well, that would have worked, if we had an Unlock model...
require 'rocket_tag'
class Unlock < ActiveRecord::Base
has_many :images, as: :image_attachable, dependent: :destroy
attr_taggable :tags
validates :name, presence: true
validates :code, presence: true
end
Ordinary ActiveRecord like you’re accustomed to...
Monday, June 10, 13
Simple Goliath + Grape
Try it out!
Monday, June 10, 13
Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
Monday, June 10, 13
Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
Monday, June 10, 13
Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/v1/unlocks/1
Monday, June 10, 13
Simple Goliath + Grape
Try it out!
$ ruby app.rb -sv -p 8000
[45584:INFO] 2013-05-28 11:52:52 :: Starting server on
0.0.0.0:8000 in development mode. Watch out for stones.
$ curl http://lvh.me:8000/v1/unlocks/1
{ "success": true, "data": {"id":1,"name":"Fancy
Thing","code":"001FT","description":"Enim doloribus id
minima.","unique_tags":["foo","bar","woot"],"images":
[{"image":{"id":"1","caption":"dolor sit
amet","mime_type":"image/jpg","url":"http://s3.amazon.com/
whatever.jpg"}}],"created_at":1364916167.000000000}}} }
Monday, June 10, 13
Simple Goliath + Grape
“Could the API
give us users
too? That’d be
great...”
Monday, June 10, 13
Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
Simple Goliath + Grape
./app/api.rb
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Remember me?
Monday, June 10, 13
Simple Goliath + Grape
./app/api.rb
Remember me?
Dir["./app/apis/v1/*.rb"].each { |f| require f }
class API < Grape::API
mount APIv1::Unlocks
mount APIv1::Users
resource 'servicehealth' do
desc "Returns a basic status report."
get "/" do
MultiJson.dump({
status: 'OK',
environment: Goliath::env })
end
end
end
Monday, June 10, 13
Need communication?
Build a Client Gem!
Monday, June 10, 13
Need communication?
Build a Client Gem!
require 'virtus'
require 'rest_client'
require 'multi_json'
module UnlocksClient
class Unlock
include Virtus
attribute :id
attribute :name
attribute :code
attribute :description
attribute :tags
attr_accessor :media_rewards
def self.find(id, params={})
client = RestClient::Resource.new("#{BASE_URL)}/unlocks/#{id}")
response = client.get({params: params})
data = MultiJson.load(response)["data"]
return nil if !response || data.empty?
new(params)
end
end
end
Monday, June 10, 13
Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
Monday, June 10, 13
Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
Monday, June 10, 13
Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: em_mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
Monday, June 10, 13
Asynch MySQL Gotcha
Used to Rails?
If you’re not paying attention, you might do this...
development:
host: localhost
adapter: em_mysql2
database: unlocks_dev
pool: 20
timeout: 5000
reconnect: true
username: dbuser
password: 123whatever
YMMV
Monday, June 10, 13
Goliath Tips
curl is your friend on the command line.
Console mode!
ruby app.rb -svC
using Pry to debug stuff: Just add binding.pry
Monday, June 10, 13
Links
Goliath / Grape / EM Stuff
https://github.com/
postrank-labs/goliath
https://github.com/
igrigorik/em-synchrony
https://github.com/
intridea/grape
Matt Stuff
code.digimonkey.com
mepatterson.net
github.com/mepatterson
twitter.com/mepatterson
Monday, June 10, 13

More Related Content

Recently uploaded

Pigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 3652toLead Limited
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountPuma Security, LLC
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024Scott Keck-Warren
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupFlorian Wilhelm
 
Pigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxMalak Abu Hammad
 
APIForce Zurich 5 April Automation LPDG
APIForce Zurich 5 April  Automation LPDGAPIForce Zurich 5 April  Automation LPDG
APIForce Zurich 5 April Automation LPDGMarianaLemus7
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksSoftradix Technologies
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...Fwdays
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationSlibray Presentation
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubKalema Edgar
 
CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):comworks
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Alan Dix
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticscarlostorres15106
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsPrecisely
 

Recently uploaded (20)

Pigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food Manufacturing
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path Mount
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project Setup
 
Pigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping Elbows
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptx
 
APIForce Zurich 5 April Automation LPDG
APIForce Zurich 5 April  Automation LPDGAPIForce Zurich 5 April  Automation LPDG
APIForce Zurich 5 April Automation LPDG
 
DMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special EditionDMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special Edition
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other Frameworks
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck Presentation
 
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptxE-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
 
The transition to renewables in India.pdf
The transition to renewables in India.pdfThe transition to renewables in India.pdf
The transition to renewables in India.pdf
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding Club
 
CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):CloudStudio User manual (basic edition):
CloudStudio User manual (basic edition):
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power Systems
 

Featured

Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)contently
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024Albert Qian
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsKurio // The Social Media Age(ncy)
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Search Engine Journal
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summarySpeakerHub
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next Tessa Mero
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentLily Ray
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best PracticesVit Horky
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project managementMindGenius
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...RachelPearson36
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Applitools
 
12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at WorkGetSmarter
 
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...DevGAMM Conference
 
Barbie - Brand Strategy Presentation
Barbie - Brand Strategy PresentationBarbie - Brand Strategy Presentation
Barbie - Brand Strategy PresentationErica Santiago
 
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them wellGood Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them wellSaba Software
 

Featured (20)

Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie Insights
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search Intent
 
How to have difficult conversations
How to have difficult conversations How to have difficult conversations
How to have difficult conversations
 
Introduction to Data Science
Introduction to Data ScienceIntroduction to Data Science
Introduction to Data Science
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best Practices
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project management
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
 
12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work
 
ChatGPT webinar slides
ChatGPT webinar slidesChatGPT webinar slides
ChatGPT webinar slides
 
More than Just Lines on a Map: Best Practices for U.S Bike Routes
More than Just Lines on a Map: Best Practices for U.S Bike RoutesMore than Just Lines on a Map: Best Practices for U.S Bike Routes
More than Just Lines on a Map: Best Practices for U.S Bike Routes
 
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...
Ride the Storm: Navigating Through Unstable Periods / Katerina Rudko (Belka G...
 
Barbie - Brand Strategy Presentation
Barbie - Brand Strategy PresentationBarbie - Brand Strategy Presentation
Barbie - Brand Strategy Presentation
 
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them wellGood Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
Good Stuff Happens in 1:1 Meetings: Why you need them and how to do them well
 

Building high-performance APIs for the video game industry with Goliath, Grape and EventMachine

  • 1. Building high-performance APIs for the video game industry with Goliath, Grape, and EventMachine Matt E. Patterson Digimonkey Studios Monday, June 10, 13
  • 2. Matt Patterson Software Consultant (Ruby, Rails, Agile, etc.) Web software since 1998 (ColdFusion and PHP) Ruby and Rails since 2006 (Rails 1.0) Monday, June 10, 13
  • 3. Game Web Services Must be fast. Must be reliable. Must be scalable... ...and able to handle sudden bursts (launches, new DLC, etc.) Monday, June 10, 13
  • 4. Goliath open-source, non-blocking, asychronous Ruby web server framework from postrank-labs EventMachine reactor. HTTP parser, Rack, Ruby 1.9+, Ruby Fibers each request executes in its own Fiber Monday, June 10, 13
  • 5. Ubiquitous Goliath “Hello, world” example require 'goliath' class Hello < Goliath::API def response(env) [ 200, {}, "Hello, world!" ] end end Monday, June 10, 13
  • 6. Ubiquitous Goliath “Hello, world” example require 'goliath' class Hello < Goliath::API def response(env) [ 200, {}, "Hello, world!" ] end end $ ruby hello.rb -sv -p 8000 Monday, June 10, 13
  • 7. Ubiquitous Goliath “Hello, world” example require 'goliath' class Hello < Goliath::API def response(env) [ 200, {}, "Hello, world!" ] end end $ ruby hello.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. Monday, June 10, 13
  • 8. Ubiquitous Goliath “Hello, world” example require 'goliath' class Hello < Goliath::API def response(env) [ 200, {}, "Hello, world!" ] end end $ ruby hello.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. $ curl http://lvh.me:8000/ Monday, June 10, 13
  • 9. Ubiquitous Goliath “Hello, world” example require 'goliath' class Hello < Goliath::API def response(env) [ 200, {}, "Hello, world!" ] end end $ ruby hello.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. $ curl http://lvh.me:8000/ Hello, world! Monday, June 10, 13
  • 10. What I needed... a full-featured API versioned endpoints multiple resources standard RESTful CRUD JSON requests / responses secured requests logging localization / translation file attachments with S3 storage asynchronous MySQL queries tests! Monday, June 10, 13
  • 11. What I used... Grape REST-like API micro- framework for Ruby Carrierwave, Fog, MiniMagick file uploads and S3 support Rspec + FactoryGirl testing Rabl + MultiJson JSON requests / responses em-synchrony + mysql2 async MySQL globalize3 localization / translation standalone_migrations Rails-style migrations capistrano deployment stuff Monday, June 10, 13
  • 12. Simple Goliath + Grape ./app.rb require 'rubygems' require 'bundler/setup' require 'goliath' require 'em-synchrony/activerecord' require 'grape' Dir["./app/models/*.rb"].each { |f| require f } require './app/api' class Application < Goliath::API def response(env) ::API.call(env) end end Monday, June 10, 13
  • 13. Simple Goliath + Grape ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API mount APIv1::Unlocks resource 'servicehealth' do desc "Returns a basic status report." get "/" do MultiJson.dump({ status: 'OK', environment: Goliath::env }) end end end Monday, June 10, 13
  • 14. Simple Goliath + Grape ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API mount APIv1::Unlocks resource 'servicehealth' do desc "Returns a basic status report." get "/" do MultiJson.dump({ status: 'OK', environment: Goliath::env }) end end end Monday, June 10, 13
  • 15. Simple Goliath + Grape ./app/apis/v1/unlocks.rb class APIv1 class Unlocks < Grape::API version 'v1', using: :path, format: :json resource :unlocks do # GET /unlocks/1.json desc "Returns a single Unlock record by ID" get "/:id" do unlock = Unlock.find(params[:id]) custom_render "api_v1/unlocks/show", unlock, 200 end end end end Monday, June 10, 13
  • 16. Simple Goliath + Grape ./app/apis/v1/unlocks.rb class APIv1 class Unlocks < Grape::API version 'v1', using: :path, format: :json resource :unlocks do # GET /unlocks/1.json desc "Returns a single Unlock record by ID" get "/:id" do unlock = Unlock.find(params[:id]) custom_render "api_v1/unlocks/show", unlock, 200 end end end end Wait, what? Monday, June 10, 13
  • 17. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 18. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 19. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 20. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 21. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 22. Simple Goliath + Grape back to our ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API helpers do def custom_render(rabl_template, object, status, args={}) args[:format] ||= 'json' args[:success] ||= true render_options = { format: args[:format] } render_options[:locals] = args[:locals] if args[:locals] data = Rabl::Renderer.new(rabl_template, object, render_options).render %({ "success": #{args[:success]}, "data": #{data} }) end end mount APIv1::Unlocks ... Monday, June 10, 13
  • 23. Simple Goliath + Grape ./app/views/api_v1/unlocks/show.json.rabl attributes :id, :name, :code, :description, :created_at node(:unique_tags) { |unlock| unlock.tags.uniq } child :images => :images do attributes :id, :caption, :mime_type, :url end Monday, June 10, 13
  • 24. Simple Goliath + Grape ./app/views/api_v1/unlocks/show.json.rabl attributes :id, :name, :code, :description, :created_at node(:unique_tags) { |unlock| unlock.tags.uniq } child :images => :images do attributes :id, :caption, :mime_type, :url end Monday, June 10, 13
  • 25. Simple Goliath + Grape ./app/views/api_v1/unlocks/show.json.rabl attributes :id, :name, :code, :description, :created_at node(:unique_tags) { |unlock| unlock.tags.uniq } child :images => :images do attributes :id, :caption, :mime_type, :url end renders => Monday, June 10, 13
  • 26. Simple Goliath + Grape ./app/views/api_v1/unlocks/show.json.rabl attributes :id, :name, :code, :description, :created_at node(:unique_tags) { |unlock| unlock.tags.uniq } child :images => :images do attributes :id, :caption, :mime_type, :url end renders => { "id":1,"name":"Fancy Thing","code":"001FT","description":"Enim doloribus id minima.","unique_tags":["foo","bar","woot"],"images": [{"image":{"id":"1","caption":"dolor sit amet","mime_type":"image/jpg","url":"http://s3.amazon.com/ whatever.jpg"}}],"created_at":1364916167.000000000 } Monday, June 10, 13
  • 27. Simple Goliath + Grape ./app/views/api_v1/unlocks/show.json.rabl attributes :id, :name, :code, :description, :created_at node(:unique_tags) { |unlock| unlock.tags.uniq } child :images => :images do attributes :id, :caption, :mime_type, :url end renders => { "success": true, "data": {"id":1,"name":"Fancy Thing","code":"001FT","description":"Enim doloribus id minima.","unique_tags":["foo","bar","woot"],"images": [{"image":{"id":"1","caption":"dolor sit amet","mime_type":"image/jpg","url":"http://s3.amazon.com/ whatever.jpg"}}],"created_at":1364916167.000000000} } Monday, June 10, 13
  • 28. Simple Goliath + Grape ./app/models/unlock.rb Well, that would have worked, if we had an Unlock model... Monday, June 10, 13
  • 29. Simple Goliath + Grape ./app/models/unlock.rb Well, that would have worked, if we had an Unlock model... require 'rocket_tag' class Unlock < ActiveRecord::Base has_many :images, as: :image_attachable, dependent: :destroy attr_taggable :tags validates :name, presence: true validates :code, presence: true end Monday, June 10, 13
  • 30. Simple Goliath + Grape ./app/models/unlock.rb Well, that would have worked, if we had an Unlock model... require 'rocket_tag' class Unlock < ActiveRecord::Base has_many :images, as: :image_attachable, dependent: :destroy attr_taggable :tags validates :name, presence: true validates :code, presence: true end Ordinary ActiveRecord like you’re accustomed to... Monday, June 10, 13
  • 31. Simple Goliath + Grape Try it out! Monday, June 10, 13
  • 32. Simple Goliath + Grape Try it out! $ ruby app.rb -sv -p 8000 Monday, June 10, 13
  • 33. Simple Goliath + Grape Try it out! $ ruby app.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. Monday, June 10, 13
  • 34. Simple Goliath + Grape Try it out! $ ruby app.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. $ curl http://lvh.me:8000/v1/unlocks/1 Monday, June 10, 13
  • 35. Simple Goliath + Grape Try it out! $ ruby app.rb -sv -p 8000 [45584:INFO] 2013-05-28 11:52:52 :: Starting server on 0.0.0.0:8000 in development mode. Watch out for stones. $ curl http://lvh.me:8000/v1/unlocks/1 { "success": true, "data": {"id":1,"name":"Fancy Thing","code":"001FT","description":"Enim doloribus id minima.","unique_tags":["foo","bar","woot"],"images": [{"image":{"id":"1","caption":"dolor sit amet","mime_type":"image/jpg","url":"http://s3.amazon.com/ whatever.jpg"}}],"created_at":1364916167.000000000}}} } Monday, June 10, 13
  • 36. Simple Goliath + Grape “Could the API give us users too? That’d be great...” Monday, June 10, 13
  • 37. Simple Goliath + Grape ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API mount APIv1::Unlocks resource 'servicehealth' do desc "Returns a basic status report." get "/" do MultiJson.dump({ status: 'OK', environment: Goliath::env }) end end end Monday, June 10, 13
  • 38. Simple Goliath + Grape ./app/api.rb Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API mount APIv1::Unlocks resource 'servicehealth' do desc "Returns a basic status report." get "/" do MultiJson.dump({ status: 'OK', environment: Goliath::env }) end end end Remember me? Monday, June 10, 13
  • 39. Simple Goliath + Grape ./app/api.rb Remember me? Dir["./app/apis/v1/*.rb"].each { |f| require f } class API < Grape::API mount APIv1::Unlocks mount APIv1::Users resource 'servicehealth' do desc "Returns a basic status report." get "/" do MultiJson.dump({ status: 'OK', environment: Goliath::env }) end end end Monday, June 10, 13
  • 40. Need communication? Build a Client Gem! Monday, June 10, 13
  • 41. Need communication? Build a Client Gem! require 'virtus' require 'rest_client' require 'multi_json' module UnlocksClient class Unlock include Virtus attribute :id attribute :name attribute :code attribute :description attribute :tags attr_accessor :media_rewards def self.find(id, params={}) client = RestClient::Resource.new("#{BASE_URL)}/unlocks/#{id}") response = client.get({params: params}) data = MultiJson.load(response)["data"] return nil if !response || data.empty? new(params) end end end Monday, June 10, 13
  • 42. Asynch MySQL Gotcha Used to Rails? If you’re not paying attention, you might do this... Monday, June 10, 13
  • 43. Asynch MySQL Gotcha Used to Rails? If you’re not paying attention, you might do this... development: host: localhost adapter: mysql2 database: unlocks_dev pool: 20 timeout: 5000 reconnect: true username: dbuser password: 123whatever Monday, June 10, 13
  • 44. Asynch MySQL Gotcha Used to Rails? If you’re not paying attention, you might do this... development: host: localhost adapter: em_mysql2 database: unlocks_dev pool: 20 timeout: 5000 reconnect: true username: dbuser password: 123whatever Monday, June 10, 13
  • 45. Asynch MySQL Gotcha Used to Rails? If you’re not paying attention, you might do this... development: host: localhost adapter: em_mysql2 database: unlocks_dev pool: 20 timeout: 5000 reconnect: true username: dbuser password: 123whatever YMMV Monday, June 10, 13
  • 46. Goliath Tips curl is your friend on the command line. Console mode! ruby app.rb -svC using Pry to debug stuff: Just add binding.pry Monday, June 10, 13
  • 47. Links Goliath / Grape / EM Stuff https://github.com/ postrank-labs/goliath https://github.com/ igrigorik/em-synchrony https://github.com/ intridea/grape Matt Stuff code.digimonkey.com mepatterson.net github.com/mepatterson twitter.com/mepatterson Monday, June 10, 13