Skip to content

Instantly share code, notes, and snippets.

@danchoi
Created May 18, 2012 15:12
Show Gist options
  • Save danchoi/2725789 to your computer and use it in GitHub Desktop.
Save danchoi/2725789 to your computer and use it in GitHub Desktop.
Following Rich Hickey's advice
# This is the command line layer of a detachable Sinatra endpoint for the JavaScript MVC api
require 'sequel'
require 'json'
require 'csv'
require 'sinatra/base'
module Api2
DB = Sequel.connect "postgres:///adatabase"
def parse_composite(s)
c = s[/\{(.*)\}/,1]
CSV.parse_line(c).map {|item| item.split(':')}
end
def base_categories
DB[:categories].filter("parent_id is ?", nil).to_a
end
def categories(base_cat_id)
sql = <<-SQL
with recursive subcats(id, parent_id, name, slug) as (
select id, parent_id, name, slug from categories where id = #{base_cat_id}
union all
select c.id, c.parent_id, c.name, c.slug
from subcats sc, categories c
where c.parent_id = sc.id
) select * from subcats
SQL
ds = DB[sql]
ds.to_a
end
def items search_params
params = search_params
ds = DB[:entries].select("entries.id, type, title".lit).
select_append("substring(content for 240) as content, string_agg(categories.name || '/' || categories.id::varchar, '; ') as categories".lit).
select_append("
max(latitude) as lat,
max(longitude) as lng,
max(locations.street) as street, max(locations.street_extra) as street_extra, max(locations.city) as city".lit).
join(:locations, entries__location_id: :locations__id).
join(:categories_entries, entry_id: :id).
join(:categories, id: :category_id).
group(:entries__id)
if (t = params[:type])
ds = ds.filter("type = ?", t)
end
if (q = params[:q])
ds = ds.select_append("ts_rank(entries.textsearchable_index_col, plainto_tsquery('#{q}')) as score".lit).
filter("entries.textsearchable_index_col @@ plainto_tsquery(?)", q).
order_append("ts_rank(entries.textsearchable_index_col, plainto_tsquery('#{q}')) desc".lit)
end
if (cat_id = (params[:category] || params[:category_id]))
cat_ids = categories(cat_id).map {|x| x[:id]}
ds = ds.filter("categories.id in ?", cat_ids)
end
if (lat = params[:lat]) && (lng = params[:lng])
# convert miles to meters
radius = (params[:radius] || 150).to_f * 1609
ds = ds.
filter("st_distance(st_transform(st_geomfromtext('POINT(#{lng} #{lat})', 4326), 2163), st_transform(locations.geom, 2163)) < ?", radius).
select_append("max(st_distance(st_transform(st_geomfromtext('POINT(#{lng} #{lat})', 4326), 2163), st_transform(locations.geom, 2163))/1609) as distance".lit).
order_append(:distance.asc)
end
ds = ds.limit 100
{ items: ds.to_a }
end
class Service < Sinatra::Application
include Api2
before do
headers["Access-Control-Allow-Origin"] = "*" # change this in production
headers["Access-Control-Allow-Methods"] = %w{GET POST PUT DELETE}.join(",")
headers["Access-Control-Allow-Headers"] = %w{Origin Accept Content-Type X-Requested-With X-CSRF-Token}.join(",")
content_type :json
end
options '*' do
200
end
get '/items' do
items(params).to_json
end
get '/causes' do
base_categories.to_json
end
get '/types' do
{ '0' => 'All',
'Nonprofit' => 'Nonprofits',
'Event' => 'Events',
'VolunteerOpportunity' => 'Volunteer Opportunities',
'Job' => 'Jobs',
'DonationRequest' => 'Donation Requests',
'Article' => 'Articles',
'Program' => 'Programs' }.map {|k,v| {id: k, name: v}}.to_json
end
end
end
if __FILE__ == $0
Api2::Service.run!
#include Api2
#puts categories(ARGV.first.to_i).inspect
#puts items(q: ARGV.first).to_json
#puts items(category: 39).to_json
#puts items(lat: 42.3, lng: -71.0).to_json
#puts items(type: 'Program', lat: 42.3, lng: -71.0, category:39).to_json
end
@danchoi
Copy link
Author

danchoi commented May 18, 2012

This implements an API endpoint for a Rails app.

Before, this code was embedded in a Rails app and dragged in ActionController & all these ActiveRecord models. It was impossible to run without starting the whole Rails stack.

I stripped out all ActiveRecord classes and dependencies on Rails. Now I can run the API code from the command line very quickly to test that it returns correct data given specific parameters, and I can run it in a very light Sinatra wrapper app (defined at bottom) to test the HTTP responses with curl. After that is done, I can just mount it in a Rails route and use it as part of my Rails app with confidence.

Following the advice Rich Hickey gave in his RailsConf 2012 talk, the style of this code is very non-OO. It's just a few functions that return simple arrays of hashes. This is useful for lightweight development and low-overhead testing, because I can easily run each function and convert its output to JSON (could be YAML) and inspect the results easily from the command line -- without a huge and unwieldy test setup infrastructure. (See the commented out tests at the bottom, which I was using to do exactly this).

CAVEAT: I know that I have a SQL injection vulnerability with the q, lat, and lng params. I'll take care of that later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment