Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

added middleware/rack_parser walkthrough

  • Loading branch information...
commit 02e1b153edd54aac322670364980fb7488873d54 1 parent 87ddd5c
@ericgj ericgj authored
Showing with 121 additions and 0 deletions.
  1. +121 −0 middleware/rack_parser.md
View
121 middleware/rack_parser.md
@@ -0,0 +1,121 @@
+Using Rack::Parser to encapsulate parsing logic in web service applications
+===========================
+
+Sinatra is often used for web service applications. One common need these
+applications have is to parse incoming messages, typically JSON or XML.
+
+Often this is a two step process -- first parsing the message string into
+predictable data structures, then loading that into models. So a typical route
+might look like
+
+ post '/messages'
+ message = Message.from_hash( ::MultiJson.decode(request.body) )
+ message.save
+ halt 202, {'Location' => "/messages/#{message.id}"}, ''
+ end
+
+And your Message model would have an appropriate `#from_hash` method that
+grabs what it needs from the parsed message and throws it into a new instance.
+
+If your application has several different endpoints, all using the same
+content-type, you could save some repitition by moving it to a helper:
+
+ helpers do
+ def parsed_body
+ ::MultiJson.decode(request.body)
+ end
+ end
+
+ post '/orders'
+ order = Order.from_hash( parsed_body )
+ order.process
+ # ....
+ end
+
+This works fine for simple scenarios. But if you have multiple content-types,
+need to validate the message format, or do other pre-processing on it, moving
+the parsing logic out of your application itself starts to become attractive.
+
+One option is to move it into a module that your app extends.
+
+But another option is to do the parsing in a middleware. That way, your app is
+not resposible for doing the basic parsing at all -- it's done and exposed to
+your app by the time the request arrives.
+
+The idea is *to mimic what Rack does to process form data*, which is to populate
+the `env[rack.request.form_hash]` with a hash, and then expose that through the
+`params` hash. But instead of parsing url parameters or multipart form data, it
+will parse the XML or JSON body.
+
+**`Rack::Parser`** is a Rack middleware that does just that, and lets you
+configure custom parsing routines for any given content-type.
+
+So using Rack::Parser, the example above would be changed to
+
+ # in config.ru
+ use Rack::Parser, :content_types => {
+ 'application/json' => Proc.new { |body| ::MultiJson.decode body }
+ }
+
+ # in application
+ post '/orders'
+ order = Order.from_hash( params['order'] )
+ order.process
+ # ....
+ end
+
+This is quite convenient if your app accepts either form data or JSON for a
+given endpoint -- since the code is then identical whether a user submits a
+form directly or some web-service client submits a JSON request.
+
+## Custom parsing
+
+Note that the content-type handlers are just procs (or anything that responds to
+`#call`) that take a single parameter -- the request body.
+
+This makes it quite easy to set up custom parsing, validation, or other pre-
+processing.
+
+Rack::Parser also has recently added support for error handling, so that errors
+in parsing can be mapped into a suitable http response, and logged. This is
+quite useful for handling validation errors.
+
+For instance, say you have set up vendor-specific json message formats, which
+you want to validate using a `validate_input` class method on your models.
+
+ class MyJsonParser
+
+ def initialize(validator_class)
+ @klass = validator_class
+ end
+
+ def call(body)
+ json = ::MultiJson.decode(body)
+ if @klass.respond_to?(:validate_input)
+ @klass.validate_input(json)
+ end
+ json
+ end
+
+ end
+
+ use Rack::Parser, :content_types => {
+ 'application/vnd-my-message+json' => MyJsonParser.new(Message),
+ 'application/vnd-my-order+json' => MyJsonParser.new(Order)
+ }
+
+
+Then any parsing error raised in `MulitJson.decode` or validation error in
+`validate_input` will result in a 400 (bad request) response *in the same
+content-type as the request* -- like
+
+ {"errors": "Validation error in input - cannot set 'private_info'"}
+
+This is the default; you can also completely customize how errors are converted
+into responses, per content-type.
+
+
+### For more info
+
+[Rack::Parser](https://github.com/achiu/rack-parser)
+
Please sign in to comment.
Something went wrong with that request. Please try again.