Created
May 18, 2012 15:12
-
-
Save danchoi/2725789 to your computer and use it in GitHub Desktop.
Following Rich Hickey's advice
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.