Lost sleep over JSON and Rack::PostBodyContentTypeParser

I’ve been fighting this issue the last couple nights. I wrote earlier about how Rack::PostBodyContentTypeParser can automagically turn a posted JSON object into a Rack / Sinatra params hash. So, I wrote some tests to make sure this was the case and moved on. Well, it turns out in real life things weren’t working and I couldn’t figure out why. Everything looked cool, but the hash wasn’t getting set when I did an AJAX call in the browser – everything was empty. I looked at everything, from the server, to the JS library, to the browser, to setting different content types in prototype.js etc… UGH!

The short of it is that Rack::PostBodyContentTypeParser requires exactly application/json in order to automagically turn the posted JSON object into Rack params and prototype.js (and jquery.js were adding an encoding type of charset=UTF-8 so the entire header entry was coming across as this CONTENT_TYPE: application/json; charset=UTF-8. So, as a fix, I’m just including the Rack::PostBodyContentTypeParser in the Sinatra application with one small change. Here’s the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module Rack
 
  # A Rack middleware for parsing POST/PUT body data when Content-Type is
  # not one of the standard supported types, like <tt>application/json</tt>.
  #
  # TODO: Find a better name.
  #
  class PostBodyContentTypeParser
 
    # Constants
    #
    CONTENT_TYPE = 'CONTENT_TYPE'.freeze
    POST_BODY = 'rack.input'.freeze
    FORM_INPUT = 'rack.request.form_input'.freeze
    FORM_HASH = 'rack.request.form_hash'.freeze
 
    # Supported Content-Types
    #
 
    ################## turned into regex so it matches type with encoding data...
    #APPLICATION_JSON = 'application/json'.freeze
    APPLICATION_JSON = /^application\/json/.freeze
 
    def initialize(app)
      @app = app
    end
 
    def call(env)
      case env[CONTENT_TYPE]
      when APPLICATION_JSON
        env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
      end
      @app.call(env)
    end
 
  end
end

I tested that this worked by writing the following:

1
2
3
4
5
6
7
8
9
10
def test_post_as_json_converts_to_params
  # sanity check that post with normal params works...
  post '/test_params_as_json', :param1=>"param one"
  assert_equal last_response.body,"params[:param1]=param one"
  post '/test_params_as_json', {:param1=>"param one"}.to_json, "CONTENT_TYPE"=>"application/json"
  assert_equal last_response.body,"params[:param1]=param one"
  # this is the problem, adding a charset to the content type seems to breaks rack-contrib/post_body_content_type_parser.rb
  post '/test_params_as_json', {:param1=>"param one"}.to_json, "CONTENT_TYPE"=>"application/json; charset=UTF-8"
  assert_equal last_response.body,"params[:param1]=param one"    
end
Posted: April 4th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , , , , | 4 Comments »

Couldn’t get that last thing to work so keeping it simple (stupid)

This could undoubtedly be more elegant, but it’s late and I want it to work now. May take another stab at it later…

TODO: limit the content types and only allow rendering if they are ok.

Setting up a before filter:

1
2
3
4
5
6
7
before do
  # remove and grab the file extension
  request.path_info.sub! %r{\.([^\./]+)$}, ''
  @format=$1 || 'html'
  @charset=mime_type($1) || 'text/html'
  content_type @charset, :charset => 'utf-8'
end

and using a case statement:

1
2
3
4
5
6
7
8
9
10
11
get "/home" do
  case @format
  when 'html'
    @stylesheet='home.css'
    haml :home, :layout=>:layout_simple
  when 'js'
    "{'js':true}"
  else
    pass
  end
end
Posted: April 1st, 2010 | Author: jay | Filed under: Code | Tags: , , , , | No Comments »

Doing something different depending on file extension (MIME type) in Sinatra

This looks like a solution: sinatra-respond_to

Posted: March 31st, 2010 | Author: jay | Filed under: Code | Tags: , , , | No Comments »

Using Rack middleware to parse JSON

In attempting to AJAX-ize the site, I had the desire to handle JSON as if it were form post data. Queue a Rack middleware solution. rack-contrib contains a bunch of common middleware extensions, one being the horribly named PostBodyContentTypeParser. To get this working I added:

1
require 'rack/contrib'

with all of the rest of the required files.

Added:

1
use Rack::PostBodyContentTypeParser

to my application class

And went about over testing it like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_json_creates_params_hash
  params_hash={"user"=>{"username"=>"testuser","email"=>"test@test.com","password"=>"pass1","password_confirmation"=>"pass1"}}
  post '/test_json', params_hash
  assert !last_request.params.blank?
  assert_equal params_hash, last_request.params
  assert last_response.ok?
  json_string="{\"user\":{\"password_confirmation\":\"pass1\",\"username\":\"testuser\",\"password\":\"pass1\",\"email\":\"test@test.com\"}}"
  post '/test_json', JSON(json_string)
  assert !last_request.params.blank?
  assert_equal params_hash, last_request.params
  assert last_response.ok?
  post '/test_json', json_string, "CONTENT_TYPE"=>"application/json"
  assert_equal last_request.env["CONTENT_TYPE"], "application/json"
  assert !last_request.params.blank?
  assert_equal params_hash, last_request.params
  assert last_response.ok?
end
Posted: March 30th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , , , | No Comments »

Rack::Test and request / response objects

Rack::Test uses last_response and last_request objects instead of Rack’s typical request and response objects. This is probably normally fine, but when you are testing functionality that requires accessing the Rack’s normal objects, they aren’t there. I found (in the comments section of this post) that you can fix this by overriding them in your test_helper.rb:

1
2
3
4
5
6
7
8
9
10
11
12
module Test::Unit
  class TestCase
    include Rack::Test::Methods
    ...
 
    def request(*args)
      args.empty? ? last_request : rack_test_session.request(*args)
    end
 
    ...
  end
end
Posted: March 26th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , , , , , | No Comments »

OAuth summarized

Application Scope

1
2
# create the consumer...
consumer ||= OAuth::Consumer.new(KEY, SECRET, {:site => SITE, :authorize_path => PATH })

Session Scope

1
2
3
4
5
# create the request token...
rt=consumer.get_request_token({ :oauth_callback => OAUTH_CALLBACK_URL })
# save the request token and secret in the session...
session[:r_token]=rt.token
session[:r_secret]=rt.secret

User Scope (Model)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# use session values to create the request token...
rt=OAuth::RequestToken.new(consumer, session[:r_token], session[:r_secret])
# grab the user data from the OAuth provider...
access_token=rt.get_access_token({:oauth_verifier=>params[:oauth_verifier]})
oauth_user_json=access_token.get(VERIFY_PATH).body
oauth_user=JSON.parse(oauth_user_json)
# create or find the the user (using twitter.com for the email address - could use some work)... 
u=TwitterUser.first_or_create(:email=>"#{oauth_user['screen_name']}@twitter.com")
u.username=oauth_user['screen_name']
u.save!
u.oauth_tokens.all.destroy
u.oauth_tokens.new(:user_access_token=>access_token.to_yaml)
u.save!
# set the session user for future use...
session[:user]=u.id
...
# and when you need access to the OAuth provider again, use the access_token stored in the User model
u=User.first(:id=>session[:user])
access_token=YAML::load(u.oauth_tokens.first.user_access_token)
verify=access_token.get(OAUTH_PROVIDERS["https://twitter.com"][:verify_path]).body
Posted: March 17th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , , , , , | No Comments »

Taking a pass on URLs that aren’t supposed to exist (in Sinatra)

This is simple, but I’m dumping it here anyway… I’d like to use the Twitter model of account access: http://example.com/USERNAME

So, if the user exists, if should display that user’s page. If the user doesn’t exist (or the user is deactivated) then this route will pass, which in this case, passes to the 404 error page. Here’s how I’m dealing with that in my routes:

1
2
3
4
5
    get '/:username' do
      @user=User.first(:username=>params[:username],:activated=>true)
      pass unless @user
      haml :user_page
    end

Note:This should be low on the chain of routes so that users with names or actual routes can’t be rendered as user pages. Example, a user with a username of “login” should not interfere with the application function of “login”. I might look further into protecting this beyond it’s position in the URL parsing chain.

Posted: March 13th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , | No Comments »

Using haml for view templates, ERB for email templates…

and I’m not sure how I feel about this. Maybe I should use Mustache for tempting because this whole thing is for moustaches…

Posted: March 10th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , | No Comments »

Testing helpers in Sinatra

How do you test your helper methods in a Sinatra::Base app?

1
2
3
4
5
6
7
module FundAStache
  module Helpers    
    def logged_in?
      false # you shall not pass...
    end
  end
end

Include them in your tests…

1
2
3
4
class FundastacheUserTest < Test::Unit::TestCase
  include Rack::Test::Methods
  include FundAStache::Helpers
  ...
Posted: March 9th, 2010 | Author: jay | Filed under: Code | Tags: , , , , , | No Comments »

Here’s what my test_helper.rb file looks like…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require 'fundastache'
require 'test/unit'
require "rack/test"
require "rack/flash/test"
 
ENV['RACK_ENV'] = 'test'
 
# set test environment
set :environment, :test
set :run, false
set :raise_errors, true
set :logging, false
 
DataMapper.setup(:default, "sqlite3::memory:") 
DataMapper.auto_migrate!
Posted: March 9th, 2010 | Author: jay | Filed under: Code | Tags: , , , | No Comments »