ActsAsFlinn http://www.actsasflinn.com/articles.rss en-us 40 $ ./script/plugin install acts_as_flinn JSON-P Rack Handler <p class="note">Updated 2008-06-19 &#8211; better support Halcyon</p> <h2><span class="caps">JSON</span>-P Rack Handler</h2> <h3>Juicing Ruby</h3> <p>I&rsquo;ve been trying to find ways to squeeze all the juice out of Ruby lately. So many blog posts talk about how Rails doesn&rsquo;t scale, Ruby is slow, blah blah. I had a convo with another developer at work today that went something like this:</p> <p>Me: if we&rsquo;re willing to do anything for performance we&rsquo;d we just switch to Java</p> <p>Jared: Yeah let&rsquo;s not do that.</p> <p>Switching to Java would be a big trade off in performance but also in development time. That&rsquo;s a big trade off that none of us around here think is worth. That&rsquo;s where this Rack handler comes in.</p> <h3><span class="caps">JSON</span>-P Caching</h3> <p>I&rsquo;ve posted before about <a href="http://actsasflinn.com/articles/2008/04/20/jsonp-on-rails-with-jquery">how to use <span class="caps">JSON</span>-P in Rails</a> and <a href="http://actsasflinn.com/articles/2008/06/13/cross-domain-restful-json-p-with-rails#railscachingjsonp">how to cache <span class="caps">JSON</span>-P in Rails</a> with fairly decent results (500+ reqs/sec) but I felt like I could do better. Action caching always seemed like the best way to cache the full <span class="caps">JSON</span> output of a request but the fact that jQuery uses a dynamic callback takes action caching out of the equation.</p> <h3>Speeding up <span class="caps">JSON</span>-P</h3> <p>500+ reqs/sec is good and all but I felt like if I could action cache then somehow pad that cached <span class="caps">JSON</span> result with the callback I&rsquo;d get better performance. I thought I&rsquo;d see what Merb could offer on the <span class="caps">JSON</span>-P front. In my quest to juice Ruby my test setup looks like this: Merb, Datamapper, Memcached (the c gem), Memcached (the server) and Ebb. I&rsquo;m really impressed with Merb and Datamapper in terms of development and I&rsquo;m equally impressed with Rack and Ebb for performance.</p> <p>I read recently it&rsquo;s possible to <a href="http://groups.google.com/group/rack-devel/browse_thread/thread/8d5a8199d60a0894">use Rack to filter results to gzip output</a>, which got me thinking. Why not try to do the same to pad my action cached <span class="caps">JSON</span>. Well it is possible and <strong>I&rsquo;m squeezing out <span class="caps">JSON</span>-P at ~1200 reqs/sec with the <span class="caps">JSON</span>-P Rack Handler</strong> with my test stack.</p> <h3><span class="caps">JSON</span>-P Rack Handler</h3> <pre><code> # config/jsonp.rb class JsonP def initialize(app) @app = app end def call(env) status, headers, response = @app.call(env) request = Rack::Request.new(env) response = pad(request.params.delete('callback'), response) if request.params.include?('callback') [status, headers, response] end def pad(callback, response, body = "") response.each{ |s| body &lt;&lt; s } "#{callback}(#{body})" end end</code></pre> <h3>Config</h3> <pre><code> # config/rack.rb require 'config/jsonp' use JsonP run Merb::Rack::Application.new </code></pre> <h3>In Halcyon</h3> <pre><code> # runner.ru ... require 'config/jsonp.halcyon' use JsonP run Halcyon::Runner.new </code></pre> <h3>Rails Support Soon</h3> <p>One last thing&#8230; whenever Rails starts using Rack, you&rsquo;ll be able to use this in your Rails app.</p> <p class="note">About Me: I&rsquo;m a developer with <a href="http://www.sportstechinc.com/">Sports Technologies</a>, a Rails firm specializing in community focused web applications aimed at sports and entertainment. We&rsquo;re always looking for Rails talent, if you&rsquo;re looking to work on rewarding high profile projects with a seasoned team of professionals give us a shout.</p> Mon, 16 Jun 2008 21:58:00 -0400 urn:uuid:59de356f-8b0d-4d97-b28f-f58660c02ba9 http://www.actsasflinn.com/articles/2008/06/16/json-p-rack-handler#comments Rails AJAX json p caching merb rack halcyon http://www.actsasflinn.com/trackbacks?article_id=json-p-rack-handler&day=16&month=06&year=2008 http://www.actsasflinn.com/articles/2008/06/16/json-p-rack-handler Cross domain RESTful JSON-P with Rails <h2>Rails based JSON-P</h2> <p>For the last 2 months I've been working on part of a project for a large publisher rolling out some new web services that use Rails based JSON to display comments on static web pages. This project has been a learning experience for nearly everyone involved on the Rails side and on the front end development side. We&rsquo;ve overcome a number of limitations with the lack of a safe data transport in web browsers and limitations in the way Rails handles JSON-P and cross site REST. Some of the problems are unique to the solution we&rsquo;re providing but they are no doubt a thorn in other developers&rsquo; sides. This is a follow up to a post I made about a month ago called <a href="http://actsasflinn.com/articles/2008/04/20/jsonp-on-rails-with-jquery">JSON-P on Rails with JQuery</a>. Below I&rsquo;ve laid out some of the Rails problems and solutions related to <strong class="keyword">JSON-P</strong>, <strong class="keyword">cross domain JSON using JSON-P</strong>, <strong class="keyword">JSON-P with jQuery</strong> and <strong class="keyword">Rails caching JSON-P</strong>.</p> <ol> <li><a href="#jsonp">JSON-P with Rails</a></li> <li><a href="#jsonpwithjquery">JSON-P with jQuery</a></li> <li><a href="#railscachingjsonp">Rails Caching JSON-P</a></li> <li><a href="#crossdomainjson">Cross Domain RESTful JSON-P</a></li> </ol> <h3>JSON-P with Rails<a name="jsonp">&nbsp;</a></h3> <p>My son: &ldquo;What are you doing?&rdquo;</p> <p>Me: &ldquo;Writing a blog post about JSON-P.&rdquo;</p> <p>My son: &ldquo;Who is Jason P.?&rdquo;</p> <p>Me: &ldquo;Javascript Object Notation with Padding.&rdquo;</p> <p>My son: &ldquo;Oh.&rdquo;</p> <p>So as you hopefully know JSON-P isn&rsquo;t some guy working in the back office of your IT department. JSON-P <u>is</u> a powerful data transport method in use all over the internet by some heavy hitters like Yahoo!, CNN, ESPN and lots more. JSON-P is a hack of sorts to get around a few problems like the dreaded <a href="http://en.wikipedia.org/wiki/Same_origin_policy">Same Origin Policy</a>. Plain old JSON is great but it can&rsquo;t do cross site/cross domain. <strong>Short of constructing a same origin proxy JSON-P is your best option for cross site AJAX</strong>. The best part about JSON-P is you can use it as an alternative to XMLHttpRequest to transport JSON data across sites.</p> <h4>JSON-P Structure</h4> <p>JSON-P can be expressed a number of ways:</p> <ol> <li>Embedded in an HTML request as an invisible iframe that includes a callback to instantiate JSON into the global scope. (clunky)</li> <li>With a static callback wrapping JSON (good for server cache, bad because of browser cache).</li> <li>With a dynamic callback wrapping JSON (harder to server cache).</li> </ol> <p>Out of the box Rails best supports option #3 like so.</p> <pre><code> render :json =&gt; @chats, :callback =&gt; params[:callback] </code></pre> <p>This could result in the following JSON-P response, <u>foo</u> being the callback param you submitted as part of your query string.</p></p> <pre><code> foo( {"chats": [ {"user": "actsasflinn", "created_on": "June 04, 2008", "id": 109328, "body": "Hey there Jim, I have never seen that kind of hat before!. Where did you get it?"}, {"user": "jim", "created_on": "June 04, 2008", "id": 109329, "body": "It's an ass hat. I got it from my ex."} ] } ) </code></pre> <h3>JSON-P with jQuery<a name="jsonpwithjquery">&nbsp</a></h3> <p>Using the above you can use some built-in <a href="http://jquery.com/">jQuery</a> methods to make life easy once again. In case you don&rsquo;t know, <strong>jQuery is the balls</strong>! jQuery&rsquo;s solution to the same origin policy is support for the <strong>script tag transport</strong>. jQuery will handle the transport magic if you specify a <em>callback=?</em> as a query param (yes question mark - jQuery fills it with a dynamic callback name in for you). This tells jQuery to add a dynamic callback and to use padding and the script tag transport. Check out the <a href="http://docs.jquery.com/Ajax/jQuery.getJSON">getJSON</a> method example using the dynamic callback query param.</p> <pre><code> $.getJSON("http://example.com/chats.json?callback=?", function(data){ $.each(data.chats, function(i,chat){ alert(chat.body); }); }); </code></pre> <h3>Rails Caching JSON-P<a name="railscachingjsonp">&nbsp;</a></h3> <p>The dynamic callback does a few important things like defeating the browser cache but it kills Rails&rsquo; ability to do the same. <u>Action caching the resulting JSON data doesn&rsquo;t work because jQuery changes the callback name with each request.</u> Overcoming the lack of caches_action isn&rsquo; t so bad as long you use data caching within your format.json block. The to_json method is pretty intensive and it&rsquo;ll kill your reqs/sec so it makes sense to cache the output from to_json. Using cache_fu something like the below will get you rolling with 500+ reqs/sec...</p> <pre><code> class Chat < ActiveRecord::Base acts_as_cached def self.recent_chats(format = nil) chats = find(:all, :order => "created_on desc", :limit => 10) chats.send(format) unless format.blank? end end class ChatsController < ApplicationController def index respond_to do |format| format.json do @json_chats = Chat.caches(:recent_chats, :with => :to_json) render :json => @json_chats, :callback => params[:callback] end end end end </code></pre> <p>It&rsquo;s not as great as action cache but it minimizes the db hit and the to_json processing time (which is hefty) reducing the response to simply padding the memcached JSON.</p> <h3>Cross Domain RESTful JSON-P<a name="crossdomainjson">&nbsp;</a></h3> <p>If you&rsquo;ve gone this far JSON-P is working wonders... unless of course you want to do AJAX POSTs (argh)! So this next bit of mojo is indeed a hack and not for the faint of heart. As you might know most browsers don&rsquo;t support the HTTP DELETE and PUT methods so Rails spoofs them by making an POST and passing in a query param called _method. Wonderful but you can&rsquo;t do an AJAX POST across domains. The AJAX REST Nazi says &ldquo;no REST for you!&rdquo; You can of course monkey patch Rails to use _method regardless of the actual HTTP method enabling you to spoof <strong>AJAX and REST across domains</strong>.</p> <pre><code> module ActionController class AbstractRequest def request_method @request_method ||= begin method = (parameters[:_method].blank? ? @env['REQUEST_METHOD'] : parameters[:_method].to_s).downcase if ACCEPTED_HTTP_METHODS.include?(method) method.to_sym else raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}" end end end end end </code></pre> <p>Achtung! Monkey patching with the above will expose your create method without using an actual post. Imho no big deal, others might be more cautious (<a href="http://actsasflinn.com/articles/2008/04/27/captcha-sucks">CAPTCHA</a> is always an option), ymmv. On to the controller...</p> <pre><code> class ChatsController < ApplicationController ... def create @chat = Chat.new(params[:chat]) respond_to do |format| if @chat.save format.json { render :json => { :chat => @chat }, :callback => params[:callback], :status => :created, :location => @chat } else format.json { render :json => { :errors => @chat.errors.full_messages }, :callback => params[:callback], :status => :unprocessable_entity } end end end end </code></pre> <p>Using jQuery to serialize your form, a spoofed AJAX POST might look something like this.</p> <pre><code> $(document).ready(function() { $('#chat_form').submit(function() { var chat = $('#chat_form').serializeArray(); $.getJSON("http://example.com/chats.json?_method=post&callback=?", chat, function(data){ if (data.errors == undefined) { alert(data.chat.body); } else { $.each(data.errors, function(error) { alert(error); }) } }); }); }); </code></pre> <p><strong>Huzah!</strong> Easy right? It took quite a while to put all this together into a cohesive process that everyone could work with. At the onset Rails didn&rsquo;t support spoofing REST, action caching JSON-P or even cross domain JSON but with a little ingenuity we made it happen and you can too. JSON-P is the solution to put your <strong>RESTful Cross Site XMLHttpRequest</strong> woes to bed.</p> <p>p.s. none of the above code has been tested so if you copy and paste it and it doesn&rsquo;t work, don&rsquo;t complain unless you include working code.</p> <p class="note">About Me: I&rsquo;m a developer with <a href="http://www.sportstechinc.com/">Sports Technologies</a>, a Rails firm specializing in community focused web applications aimed at sports and entertainment. We&rsquo;re always looking for Rails talent, if you&rsquo;re looking to work on rewarding high profile projects with a seasoned team of professionals give us a shout.</p> Fri, 13 Jun 2008 21:49:00 -0400 urn:uuid:e487fea3-aa51-4e4c-a9d1-a8a9e39a8544 http://www.actsasflinn.com/articles/2008/06/13/cross-domain-restful-json-p-with-rails#comments http://www.actsasflinn.com/trackbacks?article_id=cross-domain-restful-json-p-with-rails&day=13&month=06&year=2008 http://www.actsasflinn.com/articles/2008/06/13/cross-domain-restful-json-p-with-rails Passenger mod_rails - I'm a believer. <p>I was initially skeptical of <a href="http://modrails.com/">Passenger aka mod_rails</a> (because it seems to good to be true) but tonight I became a believer. We recently finished work on fairly high profile project. The installation seemed to be running fine but after installing monit regular tests revealed mongrel instances were hanging or unresponsive and randomly coming back online. After 3 days of the mysterious mongrel issues, apache tweaking of the proxy config, replacing mod_proxy_balancer with haproxy, and experimenting with other backends like thin and ebb I decided to give Passenger a shot. The gem was super easy and the apache install application was super easy and very helpful. After installing Passenger the new site is rocking. Goodbye mongrel.</p> Sun, 01 Jun 2008 02:55:00 -0400 urn:uuid:6643e530-061e-4f4c-8b07-7985e1769ec7 http://www.actsasflinn.com/articles/2008/06/01/passenger-mod_rails-im-a-believer#comments Rails ruby rails mod_rails passenger http://www.actsasflinn.com/trackbacks?article_id=passenger-mod_rails-im-a-believer&day=01&month=06&year=2008 http://www.actsasflinn.com/articles/2008/06/01/passenger-mod_rails-im-a-believer CAPTCHA Sucks <p>I was on Slashdot and there was actually something I cared to comment about&#8230; then I got this.</p> <p><img src="/files/wtf.jpg" alt="WTF Does This Say?" /></p> <p>Then I left.</p> Sun, 27 Apr 2008 17:59:00 -0400 urn:uuid:da1a23b8-dd08-429e-8782-1048d8c94077 http://www.actsasflinn.com/articles/2008/04/27/captcha-sucks#comments http://www.actsasflinn.com/trackbacks?article_id=captcha-sucks&day=27&month=04&year=2008 http://www.actsasflinn.com/articles/2008/04/27/captcha-sucks Rails Hosting Options <h1>Rails Hosting</h1> <p>I get a lot of questions about Rails hosting from this blog, so I&#8217;ll take a minute to post about some of my experiences.</p> <h2>Site5 Rails Hosting</h2> <p>If you&#8217;re looking for <strong class="keyword">hosting</strong> for a small website like a typo or mephisto blog <a href="http://www.site5.com/in.php?id=10080-6">Site5 is a great hosting option.</a> I&#8217;ve had great luck with them over the last few years. They have a Rails based control panel called backstage which is tied to a customized version of CPanel called SiteAdmin. The engineers at Site5 are Rails guys and they&#8217;re <a href="http://weblog.site5.com/">always doing some innovative stuff</a>. They also have reseller plans if you need to manage client websites. This blog has been <strong class="keyword">hosted</strong> there for a while now and because of that I highly recommend them for <a href="http://www.site5.com/in.php?id=10080-6"><strong>Shared Rails hosting</strong></a>.</p> <h2>Slicehost Rails Hosting</h2> <p>When you&#8217;re looking for a more <strong class="keyword">dedicated hosting solution for Rails apps</strong> <a href="https://manage.slicehost.com/customers/new?referrer=475869576">Slicehost</a> is the place. You get a virtualized Xen host with the latest stable Ubuntu. They&#8217;re perfect for <strong class="keyword">hosting Rails apps</strong> that need a little more juice than a shared hosting solution will give you. I&#8217;ve had great luck with them in terms of uptime and support. It&#8217;s one of those types of places that you just don&#8217;t have to worry about once you&#8217;ve got an app going. If you&#8217;re looking for an <strong class="keyword">excellent Rails hosting</strong> environment, especially for development <a href="https://manage.slicehost.com/customers/new?referrer=475869576">Slicehost</a> is perfect.</p> <h2>Honorable Mentions</h2> <p>I&#8217;ve had luck with <a href="http://rimuhosting.com/">Rimuhostng</a> in the past. They&#8217;re great operation and the support was always really good. They also have an out of the box <strong class="keyword">easy Rails stack</strong> install script that makes running a current Rails environment simple.</p> <h2>Non-Starters</h2> <p>I haven&#8217;t had a Dreamhost account myself but I have managed an account for a client there and from what I saw the control panel felt hacked, the servers were overbooked and the support wasn&#8217;t good. That client switched from them after they wanted more money for a dedicated IP address and more money for support and more money for less value than what other had for the money. I&#8217;m sure everyone has heard of the fact that they charged their customers over $7 million after on of the programmers ran a billing script with a bug in it (yikes)!</p> Mon, 21 Apr 2008 21:26:00 -0400 urn:uuid:185222b5-cf0c-48f1-a948-296b84b8427b http://www.actsasflinn.com/articles/2008/04/21/rails-hosting#comments Rails rails hosting http://www.actsasflinn.com/trackbacks?article_id=rails-hosting&day=21&month=04&year=2008 http://www.actsasflinn.com/articles/2008/04/21/rails-hosting JSON-P on Rails with JQuery <h4><span class="caps">JSON</span>-P based Comment</h4> Over the last several months I&#8217;ve had the pleasure of developer <span class="caps">JSON</span> based commenting solutions for publishers. I couldn&#8217;t be happier using <span class="caps">JSON</span> as a transfer method for moving ActiveRecord based object from the database almost directly to the consumer. I&#8217;ve run into some difficulties here and there mainly due to the <strong>Same Origin Policy</strong> which is a bastard of a browser rule that makes sending <span class="caps">JSON</span> across domains difficult. Even though <a href="http://prototypejs.org/">prototype.js</a> seems to support sending requests across domains there is no native <strong>script transport in prototype</strong> and worse yet it appear some browsers won&#8217;t allow <strong><span class="caps">AJAX</span> across subdomains</strong> to overcome the <span class="caps">SOP</span> rules. I searched all over the web and found a few hacks for adding the script tag to prototype.js but I found the best solution was built native into <a href="http://docs.jquery.com/Ajax/jQuery.getJSON">JQuery&#8217;s getJSON</a>. I was really taken with JQuery&#8217;s ability to handle the script transport method out of the box but I think even more I&#8217;m taken with JQuery&#8217;s syntax. I had a conversation with <a href="http://lesseverything.com">Less Everything</a>&#8217;s Steven Bristol about 6 months ago and he asked if I had checked out JQuery. I had briefly looked at it but I wasn&#8217;t wowed by it (probably because I didn&#8217;t use it on a project). Steven: &#8220;isn&#8217;t it the best thing you&#8217;ve ever seen?&#8221; Me: It&#8217;s ok. (*my answer to everything &#8211; ask my wife). Six months later I get to use it on a project and my answer to you Steven is <strong><span class="caps">YES</span></strong> it is the greatest. <h4><span class="caps">JSON</span>-P</h4> Back to the subject&#8230; So the problem is that a browser doesn&#8217;t want to send a request across domains for fear that your secret info will be compromised by some would be hacker. The solution <span class="caps">JSON</span> padded with a callback method combined with a plain old script tag aka <span class="caps">JSON</span>-P. With JQuery the idea is that you can pass a <span class="caps">URL</span> into JQuery&#8217;s getJSON method with a ? for JQuery to bind it&#8217;s on changing function name that handles a callback. Sound like a hack? Yes but everyone is doing it. I&#8217;ve seen some <a href="http://unclehulka.com/ryan/blog/archives/2005/12/12/jsonpyoure-joking-right/">skeptics</a> but overall this is an accepted solution. And until <a href="http://www.json.org/JSONRequest.html">JSONRequest</a> is accepted as a safe cross site transport <span class="caps">JSON</span>-P is here to stay. <h4>Does Rails support <span class="caps">JSON</span>-P?</h4> Bet your ass it does. Right out of the box Rails supports the callback option to be passed in on a :json render. Like so: <pre><code> render :json =&gt; @comments, :callback =&gt; params[:callback] </code></pre> <h4><span class="caps">JSON</span>-P Problems on Rails</h4> So the issue you&#8217;ll run into primarily with JQuery is the changing name of the callback. JQuery binds a random function name to the request (presumably to protect from attacks of some sort, maybe for anti caching). The issue is that with action caching caches the entire response body meaning the next call has a stale cache (because of the now changed callback name) or has to generate a new response for a new callback name. While <span class="caps">JSON</span> is typically very light weight the to_json method can be expensive and when you&#8217;re dealing with high traffic situations you always want to squeeze every bit of performance out that you can. We also do paginated result sets with will_paginate so it means there is a bit of post processing after a plain old find_all_by_blah&#8230; The solution I&#8217;ve been using is a mix of cache_fu for model level caching along with fragment caching the to_json results then interpolating the cached json into the callback. Sound like a hack? It is but it feels a bit better when you crunch out over 500/reqs/s as opposed to 14/reqs/s. <h4>For the future</h4> Let&#8217;s cross our fingers for <a href="http://incubator.apache.org/couchdb/">CouchDB</a>. I&#8217;ve been experimenting with Couch over the last week or two and I&#8217;m really pleased with the possibility of a RESTful document based database with native <span class="caps">JSON</span> and coming support for <a href="http://en.wikipedia.org/wiki/MapReduce">MapReduce</a>. The future looks bright, I can imagine not so distant support for <a href="http://stone.rubyforge.org">native ruby object level persistence</a>. Yay! Sun, 20 Apr 2008 13:22:00 -0400 urn:uuid:ad6f6024-c473-480d-bfb5-e0bde7a14024 http://www.actsasflinn.com/articles/2008/04/20/jsonp-on-rails-with-jquery#comments Rails AJAX jquery ajax json p http://www.actsasflinn.com/trackbacks?article_id=jsonp-on-rails-with-jquery&day=20&month=04&year=2008 http://www.actsasflinn.com/articles/2008/04/20/jsonp-on-rails-with-jquery Handling APIs with Ruby XML Parsing <h2>Have you ever <s>wanted</s> needed to write a Ruby wrapper for an <span class="caps">XML</span> based <span class="caps">API</span>?</h2> <p>If you have a burning desire to seamlessly exchange data over the web or you just want to use the latest Interweb 2.0 service &#8211; you&#8217;re most likely contemplating writing your <span class="caps">API</span> client in Ruby&#8230; yes that&#8217;s why you&#8217;re here isn&#8217;t it?</p> <h2>Why Use Ruby to Parse <span class="caps">XML</span>?</h2> <p>It&#8217;s a fact &#8211; <a href="http://www.google.com/search?client=safari&#38;rls=en-us&#38;q=ruby+xml&#38;ie=UTF-8&#38;oe=UTF-8">Ruby kicks ass at parsing <span class="caps">XML</span>.</a> You can find <a href="http://www.google.com/search?hl=en&#38;client=safari&#38;rls=en-us&#38;q=ruby+gem+api&#38;btnG=Search">tons of examples of <span class="caps">XML API</span> clients written in Ruby</a>.</p> <h3>ActiveResource parses <span class="caps">XML</span> and handles RESTful <span class="caps">HTTP</span></h3> <p>The new <a href="http://ryandaigle.com/articles/2006/06/30/whats-new-in-edge-rails-activeresource-is-here">ActiveResource</a> Rails gem found in Rails2 makes pretty light work of handling <span class="caps">XML</span> APIs via <span class="caps">REST</span>. Unfortunately not everyone is rushing to support <span class="caps">REST</span> just yet. If you support a big Rails 1.2 app you can&#8217;t just run out an add ActiveResource to your project (which is my case). This post is not about <span class="caps">REST</span> or ActiveResource so if you&#8217;re looking for that, click the link you just <s>clicked</s> skipped over.</p> <h2>Enough with the <s>useful</s> useless Ruby <span class="caps">XML</span> facts&#8230;</h2> <h3>Show me how to write a Ruby <span class="caps">XML API</span> wrapper</h3> <p>I&#8217;ve been working with an <span class="caps">API</span> recently for a third party registration system on a project we&#8217;re rolling out soon. The third party provides their <span class="caps">API</span> using domain scoped query URLs and <span class="caps">HTTP GET</span> params and returns <span class="caps">XML</span> documents.</p> <h4>Huh?</h4> <p><strong>http://example.com/aaflinn/lookup?username=billlumberg&#38;password=swingline</strong></p> <h3><span class="caps">XML</span> Messages</h3> <p>When you get a matching user/pass combination you get something like this.</p> <div style="text-align:left;color:#ffffff; background-color:#000000; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#91dc93;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;</span><br /> <span style="color:#ed77e5;">&lt;auth&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;user&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;username&gt;</span><span style="color:#ff0000;">&lt;![CDATA[billlumberg]]&gt;</span><span style="color:#ed77e5;">&lt;/username&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;fullname&gt;</span><span style="color:#ff0000;">&lt;![CDATA[Bill Lumberg]]&gt;</span><span style="color:#ed77e5;">&lt;/fullname&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;zipcode&gt;</span><span style="color:#ff0000;">&lt;![CDATA<sup><a href="#fn92131">92131</a></sup>]&gt;</span><span style="color:#ed77e5;">&lt;/zipcode&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;email&gt;</span><span style="color:#ff0000;">&lt;![CDATA[bill.lumberg@initech.com]]&gt;</span><span style="color:#ed77e5;">&lt;/email&gt;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ed77e5;">&lt;/user&gt;</span><br /> <span style="color:#ed77e5;">&lt;/auth&gt;</span></div> <p>When you put the wrong password you get an error like so.</p> <div style="text-align:left;color:#ffffff; background-color:#000000; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#91dc93;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;</span><br /> <span style="color:#ed77e5;">&lt;auth&gt;</span><br /> &nbsp;&nbsp;<span style="color:#ed77e5;">&lt;error&gt;</span><span style="color:#ff0000;">&lt;![CDATA[Invalid username/password combination]]&gt;</span><span style="color:#ed77e5;">&lt;/error&gt;</span><br /> <span style="color:#ed77e5;">&lt;/auth&gt;</span></div> <p>If you don&#8217;t enter a username at all you&#8217;ll get an error <a href="http://www.altonbrown.com/">thusly</a>.</p> <div style="text-align:left;color:#ffffff; background-color:#000000; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#91dc93;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;</span><br /> <span style="color:#ed77e5;">&lt;auth&gt;</span><br /> &nbsp;&nbsp;<span style="color:#ed77e5;">&lt;error&gt;</span><span style="color:#ff0000;">&lt;![CDATA[No username given.]]&gt;</span><span style="color:#ed77e5;">&lt;/error&gt;</span><br /> <span style="color:#ed77e5;">&lt;/auth&gt;</span></div> Pretty easy, no? <h2><span class="caps">API</span> Wrapper Concepts</h2> <p>Here are some concepts that I felt were important when I started writing my wrapper.</p> <ol> <li><strong>Simple</strong> &#8211; it should be easy to code (I hate writing stupid code)</li> <li><strong><span class="caps">DRY</span></strong> &#8211; if it&#8217;s worth writing the wrapper make sure it&#8217;s reusable</li> <li><strong>Self Documenting</strong> &#8211; it should be as self documenting as possible (rdoc)</li> <li><strong>&#8220;Exceptional Code&#8221;</strong> &#8211; it should raise errors on exceptions and handle errors from the libraries it makes use of</li> <li><strong>Don&#8217;t Spam</strong> &#8211; Don&#8217;t abuse APIs (grrr!)</li> </ol> <h3>Requirements</h3> <p>The wrapper should use a <span class="caps">GET</span> query string to perform a query to the service provider passing an md5 hashed password and username combination. If the user exists parse the <span class="caps">XML</span> result document and return an instantiated user object based on the <span class="caps">XML</span>. If no user exists raise some type of rescuable error (RecordNotFound).</p> <p>Additionally the wrapper should be able to create a new user. This particular service provider uses an <span class="caps">HTTP GET</span> query string but in some cases you might find a plain old <span class="caps">POST</span> like you&#8217;d see in a form or you&#8217;ll need to build <span class="caps">XML</span> and pass that back to the service provider (which I am not doing here). The wrapper should be able to be able to interpret error messages and determine the status of our create request in a graceful way.</p> <h3>Ruby <span class="caps">API</span> Wrapper by Example</h3> <p>The names have been changed to protect the innocent. I&#8217;ve edited the wrapper a bit to reduce some complexity and renamed it to hide the actual <span class="caps">API</span> provider. Read on in the comments of the code, I&#8217;ve done everything on that bullet list and you can read the code as you read the comments setting up just about every line of code written.</p> <div style="text-align:left;color:#ffffff; background-color:#000000; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#ff4a4a;"># =Example <span class="caps">API</span> Ruby Wrapper=<br /> # <br /> # Usage<br /> #<br /> # === setup ===<br /> # &nbsp;&nbsp;require &#8216;ApiExample&#8217;<br /> # &nbsp;&nbsp;ApiExample::account = &#8216;test&#8217;<br /> # &nbsp;&nbsp;ApiExample::logger = Logger.new(&#8216;example.log&#8217;)<br /> #<br /> # ===Find A User===<br /> # user = ApiExample::User.find(&#8216;bill.lumberg&#8217;, :password =&gt; &#8216;swingline&#8217;)<br /> #<br /> # ===Create A User===<br /> # user = ApiExample::User.create(:username =&gt; &#8216;bill.lumberg&#8217;, :password =&gt; &#8216;swingline&#8217;, :email =&gt; &#8216;bill.lumberg@initech.com&#8217;)<br /> #<br /> </span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;base64&#8217;</span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;digest/md5&#8217;</span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;net/http&#8217;</span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;rexml/document&#8217;</span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;cgi&#8217;</span><br /> <span style="color:#9469f9;">require</span> <span style="color:#f48700;">&#8216;logger&#8217;</span><br /> <br /> <span style="color:#7d9ffa;">module</span> ApiExample<br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># Example Database Host<br /> </span>&nbsp;&nbsp;HOST = <span style="color:#f48700;">&#8216;www.example.com&#8217;</span><br /> <br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># These params are required to register a new user<br /> </span>&nbsp;&nbsp;REGISTRATION_PARAMS = [ <span style="color:#fac586;">:username</span>, <span style="color:#fac586;">:password</span>, <span style="color:#fac586;">:email</span> ]<br /> <br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># ApiExample account (brand account not user account)<br /> </span>&nbsp;&nbsp;<span style="color:#73ffff;">@@account</span> = <span style="color:#7d9ffa;">nil</span><br /> &nbsp;&nbsp;<span style="color:#73ffff;">@@success_url</span> = <span style="color:#7d9ffa;">nil</span><br /> &nbsp;&nbsp;<span style="color:#73ffff;">@@fail_url</span> = <span style="color:#7d9ffa;">nil</span><br /> &nbsp;&nbsp;<span style="color:#73ffff;">@@logger</span> = <span style="color:#9469f9;">Logger</span>.new(<span style="color:#9e5e77;"><span class="caps">STDERR</span></span>)<br /> <br /> &nbsp;&nbsp;mattr_accessor <span style="color:#fac586;">:account</span>, <span style="color:#fac586;">:logger</span>, <span style="color:#fac586;">:success_url</span>, <span style="color:#fac586;">:fail_url</span><br /> <br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># Exception handling<br /> </span>&nbsp;&nbsp;<span style="color:#7d9ffa;">class</span> ApiExampleError &lt; <span style="color:#9469f9;">StandardError</span>; <span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">class</span> UnexpectedError &lt; ApiExampleError; <span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">class</span> RegistrationError &lt; ApiExampleError; <span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">class</span> RecordNotFound &lt; ApiExampleError; <span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> md5password(password)<br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#9469f9;">Base64</span>.encode64(Digest::MD5.digest(password)).strip<br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># Using Struct here allows us to make our object act similar to an<br /> </span>&nbsp;&nbsp;<span style="color:#ff4a4a;"># ActiveRecord object. &nbsp;In this example it&#8217;s not so obvious of a pain<br /> </span>&nbsp;&nbsp;<span style="color:#ff4a4a;"># in the ass it is but the real class has about 20 or so attributes<br /> </span>&nbsp;&nbsp;<span style="color:#ff4a4a;"># Using means I don&#8217;t have to create attribute read and writes<br /> </span>&nbsp;&nbsp;<span style="color:#ff4a4a;"># attribution &#8211; I got this idea from the Ben Vinegar&#8217;s<br /> </span>&nbsp;&nbsp;<span style="color:#ff4a4a;"># &lt;a href=&quot;http://rubyforge.org/projects/freshbooks/&quot;&gt;freshbooks gem&lt;/a&gt;<br /> </span><br /> &nbsp;&nbsp;User = <span style="color:#9469f9;">Struct</span>.new(<span style="color:#fac586;">:username</span>, <span style="color:#fac586;">:email</span>, <span style="color:#fac586;">:password</span>, <span style="color:#fac586;">:fullname</span>, <span style="color:#fac586;">:zipcode</span>)<br /> <br /> &nbsp;&nbsp;<span style="color:#ff4a4a;"># Extend the Struct attributes by adding the class and instance methods we want<br /> </span>&nbsp;&nbsp;<span style="color:#7d9ffa;">class</span> User<br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">attr_accessor</span> <span style="color:#fac586;">:attributes</span>, <span style="color:#fac586;">:errors</span>, <span style="color:#fac586;">:new_record</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># class method for create a new user<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Usage:<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user = ApiExample::User.create(:username =&gt; &#8216;bill.lumberg&#8217;, :password =&gt; &#8216;swingline&#8217;, :email =&gt; &#8216;bill.lumberg@initech.com&#8217;)<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> <span style="color:#7d9ffa;">self</span>.create(attributes = {})<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object = new(attributes)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object.create<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object<br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># class method for finding an existing user<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Usage:<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># ApiExample::User.find(&#8216;bill.lumberg&#8217;, &#8216;swingline&#8217;) # plain text will be sent md5 hashed rather than clear text<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> <span style="color:#7d9ffa;">self</span>.find(username, password)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;query_params = { <span style="color:#fac586;">:username</span> =&gt; username, <span style="color:#fac586;">:password</span> =&gt; ApiExample::md5password(password_option[<span style="color:#fac586;">:password</span>]) }<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;query = query_params.collect{ |k, v| [k, v].map{ |kv| <span style="color:#9469f9;"><span class="caps">CGI</span></span>::escape(kv.to_s) }.join(<span style="color:#f48700;">&#8217;=&#8217;</span>) }.join(<span style="color:#f48700;">&#8217;&amp;&#8217;</span>)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># @@account doesn&#8217;t need to be encoded because we are setting internally, the other stuff is vulnerable<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;uri = <span class="caps">URI</span>::HTTP.build(<span style="color:#fac586;">:host</span> =&gt; ApiExample::HOST, <span style="color:#fac586;">:path</span> =&gt; <span style="color:#f28720;">&quot;/</span><span style="color:#669999;">#{ApiExample::account}</span><span style="color:#f28720;">/lookup&quot;</span>, <span style="color:#fac586;">:query</span> =&gt; query)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># I&#8217;ve got a bunch of these loggers throughout which are used to debug, remove as you see fit.<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(<span style="color:#f28720;">&quot;URI: </span><span style="color:#669999;">#{uri}</span><span style="color:#f28720;">&quot;</span>)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Make the actual <span class="caps">HTTP</span> Request<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result = Net::HTTP.get_response(uri)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Parse the <span class="caps">XML</span> Result of the <span class="caps">HTTP</span> Request<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response = <span class="caps">REXML</span>::Document.new(result.body)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(<span style="color:#f28720;">&quot;RESPONSE: </span><span style="color:#669999;">#{response}</span><span style="color:#f28720;">&quot;</span>)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Check to see if there is a user node, if there&#8217;s not raise an error similar to ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">unless</span> response.elements[<span style="color:#f48700;">&#8217;//user&#8217;</span>].nil?<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;attributes = {}<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Members is the Struct method for the attributes we setup&#8230;they correspond to what<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># we expect the <span class="caps">XML</span> to return, so lets only handle what we know<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;members.each <span style="color:#7d9ffa;">do</span> |field_name|<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;node = response.elements[<span style="color:#f48700;">&#8217;//user&#8217;</span>].elements[field_name.to_s]<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">next</span> <span style="color:#7d9ffa;">if</span> node.nil?<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;attributes[field_name.to_sym] = node.text <span style="color:#ff4a4a;"># you can do casting here if you need, I did<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object = allocate<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object.attributes = attributes<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;object<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">else</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Raise an error, setting the message to what the <span class="caps">API</span> sets as the error in <span class="caps">XML</span><br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># if there is no &#8216;error&#8217; node in the <span class="caps">XML</span> set an &#8216;unknown error&#8217; message<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#9469f9;">raise</span> RecordNotFound, (response.elements[<span style="color:#f48700;">&#8217;//error&#8217;</span>].text <span style="color:#7d9ffa;">rescue</span> <span style="color:#f28720;">&quot;Couldn&#8217;t find ApiExample::User with Username: </span><span style="color:#669999;">#{username}</span><span style="color:#f28720;"> because of an unknown error!&quot;</span>)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># instance method to setup new object ala ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Usage:<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user = ApiExample::User.new(:username =&gt; &#8216;bill.lumberg&#8217;)<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> initialize(attributes = {})<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#6699cc;">@new_record</span> = <span style="color:#7d9ffa;">true</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">self</span>.attributes = attributes<br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># instance method ala ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> errors<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#6699cc;">@errors</span> = [] <span style="color:#7d9ffa;">if</span> <span style="color:#6699cc;">@errors</span>.blank?<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#6699cc;">@errors</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># instance method to get an attributes hash ala ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> attributes<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;butes = {}<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;members.each{ |member| butes[member.to_sym] = <span style="color:#7d9ffa;">self</span>.send(member) } <span style="color:#ff4a4a;"># you can cast here, I did<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;butes<br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># instance method to set attributes via a hash ala ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> attributes=(new_attributes = {})<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;members.each <span style="color:#7d9ffa;">do</span> |member|<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">unless</span> new_attributes.has_value?(member.to_sym)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># #{member}= because you might possibly write an attribute writer to handle the input to =<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">self</span>.send(member+<span style="color:#f48700;">&#8217;=&#8217;</span>, new_attributes[member.to_sym]) <span style="color:#ff4a4a;"># you can cast here, I did<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(<span style="color:#f28720;">&quot;</span><span style="color:#669999;">#{member}</span><span style="color:#f28720;">: </span><span style="color:#669999;">#{self.send(member).inspect}</span><span style="color:#f28720;">&quot;</span>) <span style="color:#ff4a4a;"># for snooping<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># instance method to create the new instantited object ala ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># note the difference in create and self.create are the same as in ActiveRecord<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Usage:<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user = ApiExample::User.new(:username =&gt; &#8216;bill.lumberg&#8217;)<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user.password = &#8216;swingline&#8217;<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user.email = &#8216;bill.lumberg@initech.com&#8217;<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># user.create<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;">#<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">def</span> create<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># we pass our success and fail URLs even though we aren&#8217;t using them for their intended purpose<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;query_params = { <span style="color:#fac586;">:success</span> =&gt; ApiExample::success_url, <span style="color:#fac586;">:</span><span style="color:#9469f9;">fail</span> =&gt; ApiExample::fail_url }.merge(attributes)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(query_params.inspect) <span style="color:#ff4a4a;"># for snooping<br /> </span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># check to make sure all required params are included<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">if</span> ApiExample::REGISTRATION_PARAMS.all?{ |param| query_params.<span style="color:#7d9ffa;">include</span>?(param) }<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># <span class="caps">URL</span> Encode everything<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;query = query_params.collect{ |k, v| [k, v].map{ |kv| <span style="color:#9469f9;"><span class="caps">CGI</span></span>::escape(kv.to_s) }.join(<span style="color:#f48700;">&#8217;=&#8217;</span>) <span style="color:#7d9ffa;">unless</span> v.blank? }.compact.join(<span style="color:#f48700;">&#8217;&amp;&#8217;</span>)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># @@account doesn&#8217;t need to be encoded because we are setting internally, the other stuff is vulnerable<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;uri = <span class="caps">URI</span>::HTTP.build(<span style="color:#fac586;">:host</span> =&gt; ApiExample::HOST, <span style="color:#fac586;">:path</span> =&gt; <span style="color:#f28720;">&quot;/</span><span style="color:#669999;">#{ApiExample::account}</span><span style="color:#f28720;">/register&quot;</span>, <span style="color:#fac586;">:query</span> =&gt; query)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(<span style="color:#f28720;">&quot;URI: </span><span style="color:#669999;">#{uri}</span><span style="color:#f28720;">&quot;</span>) <span style="color:#ff4a4a;"># for snooping<br /> </span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Make the actual <span class="caps">HTTP</span> Request<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response = Net::HTTP.get_response(uri)<br /> <br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::logger.debug(<span style="color:#f28720;">&quot;RESPONSE: </span><span style="color:#669999;">#{response}</span><span style="color:#f28720;">&quot;</span>) <span style="color:#ff4a4a;"># for snooping<br /> </span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Check for failure and errors<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">if</span> response[<span style="color:#f48700;">&#8216;location&#8217;</span>].<span style="color:#7d9ffa;">include</span>?(ApiExample::fail_url)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Raise an error for each message<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#9469f9;"><span class="caps">CGI</span></span>::parse(URI::parse(response[<span style="color:#f48700;">&#8216;location&#8217;</span>]).query)[<span style="color:#f48700;">&#8216;error&#8217;</span>].each <span style="color:#7d9ffa;">do</span> |error_message|<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#9469f9;">raise</span> RegistrationError, error_message<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">elsif</span> response[<span style="color:#f48700;">&#8216;location&#8217;</span>].<span style="color:#7d9ffa;">include</span>?(ApiExample::success_url)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># Change the status to reflect the fact that we&#8217;ve saved the object&#8230;this could get much more<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># in-depth in terms of ensuring our data was correctly saved&#8230; you wouldn&#8217;t expect to do<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># that kind of thing in an <span class="caps">RDBMS</span> so why here?<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">self</span>.new_record = <span style="color:#7d9ffa;">false</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">else</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># it wasn&#8217;t the fail url, it wasn&#8217;t the success url so what the hell was it?<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#9469f9;">raise</span> UnexpectedError, <span style="color:#f28720;">&quot;Unknown response <span class="caps">URL</span>!&quot;</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">else</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#ff4a4a;"># For each missing required param add an error message<br /> </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ApiExample::REGISTRATION_PARAMS.each <span style="color:#7d9ffa;">do</span> |missing_param|<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">self</span>.errors &lt;&lt; <span style="color:#f28720;">&quot;</span><span style="color:#669999;">#{missing_param}</span><span style="color:#f28720;"> can&#8217;t be blank.&quot;</span> <span style="color:#7d9ffa;">unless</span> query_params.<span style="color:#7d9ffa;">include</span>?(missing_param)<br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> &nbsp;&nbsp;<span style="color:#7d9ffa;">end</span><br /> <span style="color:#7d9ffa;">end</span><br /> </div> Fri, 25 Jan 2008 22:08:00 -0500 urn:uuid:8ce9a8a3-eb8b-4a05-b3b0-256a541b3b09 http://www.actsasflinn.com/articles/2008/01/25/handling-apis-with-ruby-xml-parsing#comments Plugins Ruby snippet api ruby http://www.actsasflinn.com/trackbacks?article_id=handling-apis-with-ruby-xml-parsing&day=25&month=01&year=2008 http://www.actsasflinn.com/articles/2008/01/25/handling-apis-with-ruby-xml-parsing Rails Deployments Tips <p>Very often when projects go into production there are a few tweaks needed to make sure the app works correctly in its production environment and <strong>stays working</strong>. Below are some tips I&#8217;ve come up with from my experience rolling rails apps. Most of them are no brainers and some probably apply only to the Rails development we&#8217;re doing at work.</p> <h3>Rotate Rails Log Files</h3> <p>Log files get big really quick on high traffic sites. Some server apps rely on the operating system or a log rotating script to archive or delete old logs. Rails has a built in feature thanks to Ruby&#8217;s Logger class. The following code will keep 50 archived logs each 1mb in size and automatically rotate the log out once it hits 1mb.</p> <p>In <strong>conf/environments.rb</strong></p> <pre><code> Rails::Initializer.run do |config| # ... config.logger = Logger.new(File.join(RAILS_ROOT, 'log', "#{RAILS_ENV}.log"), 50, 1.megabyte) </code></pre> <h3>Ignore Sensitive Parameters in Logs</h3> <p>Sick of seeing your bank password showing up in your Rails based shiny new web 2.0 app? Yeah I know don&#8217;t use your bank password but hey if you&#8217;ve done it users of your shiny new web 2.0 app probably will too. Keep them safe by keeping their passwords out of your logs. The following hides the password and password_confirmation params from your logs.</p> <p>In <strong>conf/environments.rb</strong></p> <pre><code> # Include your application configuration below ActionController::Base.filter_parameter_logging :password, :password_confirmation </code></pre> <h3>Robots.txt</h3> <p>If you have a development site that isn&#8217;t password protected to the public for whatever reason you&#8217;ll want to make sure the development site isn&#8217;t getting indexed by search engines and cluttering up results for your production site.</p> <p>Create a file called <strong>robots-development.txt</strong> and on your development site use mod_rewrite to point to the file.</p> <pre><code> User-agent: * Disallow: / </code></pre> <h3>Password Protect Mod Proxy</h3> <p>So this goes along with the last topic. If you&#8217;re using password protection along with mod proxy you&#8217;ll notice that your assets are password protected but page requests still make it to mongrel. Painful on your search results if msn, yahoo or googlebot come to visit.</p> <pre><code> &lt;Proxy balancer://mongrel_cluster&gt; AuthName "Keep Out" AuthType Basic AuthUserFile htpasswd.users Require valid-user BalancerMember http://192.168.1.2:8000 #... </code></pre> <h3>Turn on /etc/init.d scripts!</h3> <p>This seems like a no-brainer but if you installed apache from source and/or you are using mongrel cluster you&#8217;re doing this manually.</p> <p>Copy the script from the mongrel_cluster resource directory to /etc/init.d</p> <pre><code> # cp /usr/local/lib/ruby/gems/1.8/gems/mongrel_cluster-1.0.5/resources/mongrel_cluster /etc/init.d/mongrel_cluster # chmod +x /etc/init.d/mongrel_cluster </code></pre> <p><strong>On Redhat</strong></p> <pre><code> # chkconfig --level 345 httpd on # chkconfig --level 345 mongrel_cluster on </code></pre> <strong>On Debian</strong> <pre><code> # update-rc.d httpd defaults # update-rc.d mongrel_cluster defaults </code></pre> <h3>More to come&#8230;</h3> Wed, 16 Jan 2008 10:04:00 -0500 urn:uuid:645e52d1-73c0-47cc-804e-28df64d9c033 http://www.actsasflinn.com/articles/2008/01/16/rails-deployments-tips#comments Rails deployment tips http://www.actsasflinn.com/trackbacks?article_id=rails-deployments-tips&day=16&month=01&year=2008 http://www.actsasflinn.com/articles/2008/01/16/rails-deployments-tips And We're Back!!!! <p>Some friends recently reminded me that my blog has been down for almost 2 months. So I&#8217;ve managed to spend some time to bring the site back up in <a href="http://www.typosphere.org/">Typo 5</a>. It kind of goes without saying that deploying rails apps on shared hosts sucks and I can stand behind that. But that&#8217;s OK &#8211; right tool for the right job.</p> <p>In the last two months there&#8217;s been a lot written about Ruby. There&#8217;s been some bad press (zed and friends) and a lot of good press (<a href="http://www.infoq.com/news/2008/01/engine-yard-gets-three-five">Rails Machine</a>, <a href="http://weblog.rubyonrails.org/2007/12/7/rails-2-0-it-s-done">Rails 2</a>, <a href="http://www.ruby-lang.org/en/news/2007/12/25/ruby-1-9-0-released/">Ruby 1.9</a>). Some crying fowl about Rails not being enterprise ready, the community sucking, it&#8217;s hard to deploy or whatever else. Who cares! Most of the complaints and accusations I&#8217;ve read are from people that need to point the finger in the mirror.</p> <p>While my site was down a lot changed. Anyone keeping track of me (yeah lots of you I&#8217;m sure) knows my startup was a non-starter and I spent 2 long dreadful <span class="caps">PHP</span> filled months at a search marketing company and have since moved on to more reputable endeavors with a Connecticut firm building Rails based entertainment apps. I&#8217;m working with a team of experienced producers and engineers and I couldn&#8217;t be happier. So you can expect some new Ruby and Rails tidbits here again soon.</p> <p>One last thing to mention before I sign-off&#8230;if you read my post mentioning <span class="caps">PHP</span>&#8217;s lack of late static binding <a href="http://actsasflinn.com/articles/2007/08/10/php-and-activerecord-continued" title="continued"><span class="caps">PHP</span> and ActiveRecord</a> you can rejoice or cringe depending on your perspective because the team is adding the static:: runtime resolution that will get <span class="caps">PHP</span> that life extending kidney implant that it so desperately needs. This <strong>new feature</strong> will allow Zend and friends to build a pragmatic Active Record in <span class="caps">PHP</span>. I&#8217;ve bolded this because a friend of mine said they are fixing the problem but it&#8217;s not a fix &#8211; it&#8217;s no doubt a feature request from Zend that was originally slated for <span class="caps">PHP6</span>. Given the slow adoption rate of <span class="caps">PHP5 I</span>&#8217;m sure the team decided to add almost <a href="http://inside.e-novative.de/uploads/PHP6ALookAhead.pdf">all</a> <a href="http://blog.felho.hu/whats-new-in-php-53-part-1-namespaces.html">the</a> <a href="http://blog.felho.hu/what-is-new-in-php-53-part-2-late-static-binding.html">new</a> <a href="http://blog.felho.hu/what-is-new-in-php-53-part-3-mysqlnd.html">goodies</a> <a href="http://blog.felho.hu/what-is-new-in-php-53-part-4-__callstatic-openid-support-userini-xslt-profiling-and-more.html">originally</a> <a href="http://www.php.net/~derick/meeting-notes.html">slated for <span class="caps">PHP6</span></a> to <span class="caps">PHP5</span>.3 in an effort to keep up. With all the new (untested) features I&#8217;m sure sysadmins will be rushing to deploy this new version.</p> Sun, 13 Jan 2008 12:39:00 -0500 urn:uuid:35543148-7b7c-4e25-9df9-3be979131456 http://www.actsasflinn.com/articles/2008/01/13/and-were-back#comments Random http://www.actsasflinn.com/trackbacks?article_id=and-were-back&day=13&month=01&year=2008 http://www.actsasflinn.com/articles/2008/01/13/and-were-back Derek Sivers Blaims Rails for Project Mismanagement <p>Some of you might have seen the O&#8217;Reilly article by former Rails trumpeter Derek Sivers entitled <a href="http://www.oreillynet.com/ruby/blog/2007/09/7_reasons_i_switched_back_to_p_1.html">7 reasons I switched back to <span class="caps">PHP</span></a>. Or the Slashdot article, even more ominously named <a href="http://developers.slashdot.org/developers/07/09/23/1249235.shtml">Thinking about Rails? Think Again</a>. In typical fashion, Slashdot ran the link with a suggestive name which I&#8217;m sure will effect the credibility of Rails in the eyes of the uninitiated masses of <span class="caps">PHP</span> developers trolling the website.</p> <p>I happened to read the article earlier in the morning before it made it to Slashdot and didn&#8217;t think too much of the arguments behind the article. I can honestly say from my own experiences that mismanagement of a project, not limitations of a framework or language are usually to blame for missed deadlines, and project failures. I can say that from experience after developing the same project in both <span class="caps">PHP</span> and Rails with about 1/3 of the tables (and probably logic).</p> <p>Anyone with a lick of common sense can tell you that if you&#8217;ve got a legacy application with 90 tables, migrating that data and accompanying application to an <span class="caps">MVC</span> framework is going to be a big project. With a 2 person team, 1 of which is the owner of the company (who should be focusing on more important things), the project is going to take a long time, cost a lot of money and the likelihood of failure is high.</p> <p>I think Derek&#8217;s logic and motivations for pursuing the project should be at question, not his choice of programming language or framework. Why do companies go into business? To make money of course. What possible reason does a company then have to rebuild it&#8217;s entire infrastructure from the ground up? To either make more money, or save money ie. make more money. The 7 reasons he switched back should have coincided with business considerations before he started the project not after the project failed. I think it&#8217;s shameful to place the blame of the project&#8217;s failure on the language and framework choice.</p> <p>I&#8217;d love to hear what bitsweat honestly has to say about the project but we can be sure either an <span class="caps">NDA</span> or professional courtesy will keep that from coming out.</p> <p>The comments at the end of the article do this opinionated scapegoat of an article justice.</p> <pre> Useless post without a concrete illustration. Show us an example of: 1) What you tried to accomplish. 2) How you tried to implement it in rails. 3) The rails code that "didn't work." 4) The "beautiful" PHP code you created instead. Richard Hertz | September 22, 2007 08:03 PM </pre> <p>I especially like this one:</p> <pre> I'm a little reluctant to add to the wasteland that is this post and these comments, but here goes. I'm familiar with the situation here. The deal was this: Derek was not a programmer; he was a musician. He learned some PHP and cobbled together the old CDBaby site by himself. It was good. Then, he heard about Rails, and became infatuated with it. He proceeded to attempt a rolling rewrite of CDBaby's frontend and backend both (the backend is large, because of inter-label and digital distribution stuff) in Rails. At this time, Derek had no experience with the following things: * any language other than PHP * systems integration and interoperability * Rails * object-orientation * the MVC pattern * managing a development team Project fails. All right. As he has learned in #2, legacy compatibility trumps everything. Also, ship early and often. As you can see in Derek's post about MySQL encodings, he's not always the clearest thinker. Even above he says that REST means POST-only destruction, which misses the point entirely. His team was fine (mostly just Jeremy, until another developer was hired in the last months). Rails was fine. But there were a lot of things wrong with the project plan ("rewrite everything, eventually") and with the project leader, who was convinced he had found a silver bullet. No framework saves you from your own inexperience. Out. wellwisher | September 23, 2007 01:47 AM </pre> Sun, 23 Sep 2007 11:01:00 -0400 urn:uuid:dcc292a5-7a9a-4484-91d1-8a2dc1f94a3d http://www.actsasflinn.com/articles/2007/09/23/derek-sivers-blaims-rails-for-project-mismanagement#comments Rails Rants rails php scapegoating http://www.actsasflinn.com/trackbacks?article_id=derek-sivers-blaims-rails-for-project-mismanagement&day=23&month=09&year=2007 http://www.actsasflinn.com/articles/2007/09/23/derek-sivers-blaims-rails-for-project-mismanagement