diff --git a/README.md b/README.md index feabecf..99daab9 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,36 @@ appropriate `If-Modified-Since` or `If-None-Match` headers. [cg]: http://www.rubydoc.info/github/rack/rack/Rack/ConditionalGet +## Different Formats + +Although Granola out of the box only ships with JSON serialization support, it's +easy to extend and add support for different types of serialization in case your +API needs to provide multiple formats. For example, in order to add MsgPack +support (via the [msgpack-ruby][] library), you'd do this: + +``` ruby +require "msgpack" + +class BaseSerializer < Granola::Serializer + MIME_TYPES[:msgpack] = "application/x-msgpack".freeze + + def to_msgpack(*) + MsgPack.pack(serialized) + end +end +``` + +Now all serializers that inherit from `BaseSerializer` can be serialized into +MsgPack. In order to use this from our Rack helpers, you'd do: + +``` ruby +granola(object, as: :msgpack) +``` + +This will set the correct MIME type. + +[msgpack-ruby]: https://github.com/msgpack/msgpack-ruby + ## License This project is shared under the MIT license. See the attached LICENSE file for diff --git a/lib/granola.rb b/lib/granola.rb index f717be4..0e10361 100644 --- a/lib/granola.rb +++ b/lib/granola.rb @@ -27,6 +27,10 @@ class << self class Serializer attr_reader :object + # Public: Map of the default MIME type for each given type of serialization + # for this object. + MIME_TYPES = { json: "application/json".freeze } + # Public: Instantiates a list serializer that wraps around an iterable of # objects of the type expected by this serializer class. # @@ -70,9 +74,11 @@ def to_json(**options) # this will be `application/json`, but you can override in your serializers # if your API uses a different MIME type (e.g. `application/my-app+json`). # + # type - A Symbol describing the expected mime type. + # # Returns a String. - def mime_type - "application/json".freeze + def mime_type(type = :json) + MIME_TYPES.fetch(type) end end diff --git a/lib/granola/rack.rb b/lib/granola/rack.rb index bcde14e..96b1d09 100644 --- a/lib/granola/rack.rb +++ b/lib/granola/rack.rb @@ -23,19 +23,22 @@ def self.included(base) # object - An object to serialize into JSON. # # Keywords: - # with: A specific serializer class to use. If this is `nil`, - # `Helper.serializer_class_for` will be used to infer the - # serializer class. - # status: The HTTP status to return on stale responses. Defaults to - # `200`. - # headers: A Hash of default HTTP headers. Defaults to an empty Hash. - # **json_options: Any other keywords passed will be forwarded to the - # serializer's `#to_json` call. + # with: A specific serializer class to use. If this is `nil`, + # `Helper.serializer_class_for` will be used to infer the + # serializer class. + # as: A Symbol with the type of serialization desired. Defaults to + # `:json` (and it's the only one available with Granola by default) + # but could be expanded with plugins to provide serialization to, + # for example, MsgPack. + # status: The HTTP status to return on stale responses. Defaults to `200`. + # headers: A Hash of default HTTP headers. Defaults to an empty Hash. + # **opts: Any other keywords passed will be forwarded to the serializer's + # serialization backend call. # # Raises NameError if no specific serializer is provided and we fail to infer # one for this object. # Returns a Rack response tuple. - def granola(object, with: nil, status: 200, headers: {}, **json_options) + def granola(object, with: nil, status: 200, headers: {}, as: :json, **opts) serializer = serializer_for(object, with: with) if serializer.last_modified @@ -46,9 +49,9 @@ def granola(object, with: nil, status: 200, headers: {}, **json_options) headers["ETag".freeze] = Digest::MD5.hexdigest(serializer.cache_key) end - headers["Content-Type".freeze] = serializer.mime_type + headers["Content-Type".freeze] = serializer.mime_type(as) - body = Enumerator.new { |y| y << serializer.to_json(json_options) } + body = Enumerator.new { |y| y << serializer.public_send(:"to_#{as}", opts) } [status, headers, body] end diff --git a/test/different_format_test.rb b/test/different_format_test.rb new file mode 100644 index 0000000..8c18fc5 --- /dev/null +++ b/test/different_format_test.rb @@ -0,0 +1,40 @@ +require "yaml" +require "granola/rack" + +class BaseSerializer < Granola::Serializer + MIME_TYPES[:yaml] = "application/x-yaml".freeze + + def to_yaml(**opts) + YAML.dump(serialized) + end +end + +User = Struct.new(:name, :age) + +class UserSerializer < BaseSerializer + def serialized + { "name" => object.name, "age" => object.age } + end +end + +class Context + include Granola::Rack +end + +prepare do + @user = User.new("John Doe", 25) +end + +setup { Context.new } + +test "allows rendering as a different format" do |context| + status, headers, body = context.granola(@user, as: :yaml) + + assert_equal 200, status + assert_equal "application/x-yaml", headers["Content-Type"] + + assert_equal( + { "name" => "John Doe", "age" => 25 }, + YAML.load(body.to_a.first) + ) +end