From b9ec72bee73c4909d7e76eecf7deaa2ead74b199 Mon Sep 17 00:00:00 2001 From: Andrey Voronkov Date: Sun, 12 Feb 2012 16:15:06 +0400 Subject: [PATCH] BSON format support provided. Fixed pluralization (without requiring active_support/inflector it doesn't work in Rabl::Helpers). Covered by tests, results were compared with msgpack results and matches except for those that returns array - BSON.serialize can't accept arrays so it wraps it into a hash with first object name pluralization as a key. --- README.md | 35 +++- lib/rabl/configuration.rb | 20 +++ lib/rabl/engine.rb | 16 ++ lib/rabl/helpers.rb | 4 +- rabl.gemspec | 5 +- test/bson_engine_test.rb | 331 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 test/bson_engine_test.rb diff --git a/README.md b/README.md index a7dc3d3e..76bda9c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RABL # -RABL (Ruby API Builder Language) is a Rails and [Padrino](http://padrinorb.com) ruby templating system for generating JSON and XML. When using the ActiveRecord 'to_json' method, I tend to quickly find myself wanting a more expressive and powerful solution for generating APIs. +RABL (Ruby API Builder Language) is a Rails and [Padrino](http://padrinorb.com) ruby templating system for generating JSON, XML, MessagePack and BSON. When using the ActiveRecord 'to_json' method, I tend to quickly find myself wanting a more expressive and powerful solution for generating APIs. This is especially frustrating when the JSON representation is complex or doesn't match the exact schema defined in the database. I wanted a simple and flexible system for generating my APIs. In particular, I wanted to easily: @@ -100,8 +100,10 @@ Rabl.configure do |config| # Commented as these are the defaults # config.json_engine = nil # Any multi\_json engines # config.msgpack_engine = nil # Defaults to ::MessagePack + # config.bson_engine = nil # Defaults to ::BSON # config.include_json_root = true # config.include_msgpack_root = true + # config.include_bson_root = true # config.include_xml_root = false # config.enable_json_callbacks = false # config.xml_options = { :dasherize => true, :skip_types => false } @@ -145,6 +147,37 @@ end *NOTE*: Attempting to render the msgpack format without either including the msgpack gem or setting a `msgpack_engine` will cause an exception to be raised. +### BSON ### + +Rabl also includes optional support for [BSON](http://bsonspec.org/) serialization format using the [bson gem](https://rubygems.org/gems/bson). +To enable, include the bson gem in your project's Gemfile. Then use Rabl as normal with the `bson` format (akin to json and xml formats). + +```ruby +# Gemfile +gem 'bson', '~> 1.5.2' +``` +To use it with Rails just register bson mime type format. +```ruby +# config/initializers/mime_types.rb +Mime::Type.register "application/bson", :bson +``` + +One can additionally use a custom BSON implementation by setting the Rabl `bson_engine` configuration attribute. This custom BSON engine must conform to the BSON#serialize method signature. + +```ruby +class CustomEncodeEngine + def self.serialize string + # Custom Encoding by your own engine. + end +end + +Rabl.configure do |config| + config.bson_engine = CustomEncodeEngine +end +``` + +*NOTE*: Attempting to render the bson format without either including the bson gem or setting a `bson_engine` will cause an exception to be raised. + ## Usage ## ### Object Assignment ### diff --git a/lib/rabl/configuration.rb b/lib/rabl/configuration.rb index 10bd6bd1..c9ddab81 100644 --- a/lib/rabl/configuration.rb +++ b/lib/rabl/configuration.rb @@ -4,6 +4,12 @@ rescue LoadError end +# We load the bson library if it is available. +begin + require 'bson' + rescue LoadError +end + # Load MultiJSON require 'multi_json' @@ -13,8 +19,12 @@ class Configuration attr_accessor :include_json_root attr_accessor :include_msgpack_root attr_accessor :include_xml_root + attr_accessor :include_bson_root attr_accessor :enable_json_callbacks + attr_accessor :bson_check_keys + attr_accessor :bson_move_id attr_writer :msgpack_engine + attr_writer :bson_engine attr_writer :xml_options DEFAULT_XML_OPTIONS = { :dasherize => true, :skip_types => false } @@ -23,9 +33,13 @@ def initialize @include_json_root = true @include_msgpack_root = true @include_xml_root = false + @include_bson_root = true @enable_json_callbacks = false + @bson_check_keys = false + @bson_move_id = false @json_engine = nil @msgpack_engine = nil + @bson_engine = nil @xml_options = {} end @@ -47,6 +61,12 @@ def msgpack_engine @msgpack_engine || ::MessagePack end + ## + # @return the Bson encoder/engine to use. + def bson_engine + @bson_engine || ::BSON + end + # Allows config options to be read like a hash # # @param [Symbol] option Key for a given attribute diff --git a/lib/rabl/engine.rb b/lib/rabl/engine.rb index 54639328..af337fd8 100644 --- a/lib/rabl/engine.rb +++ b/lib/rabl/engine.rb @@ -72,6 +72,21 @@ def to_xml(options={}) to_hash(options).to_xml(xml_options) end + # Returns a bson representation of the data object + # to_bson(:root => true) + def to_bson(options={}) + include_root = Rabl.configuration.include_bson_root + options = options.reverse_merge(:root => include_root, :child_root => include_root) + result = if defined?(@_collection_name) + { @_collection_name => to_hash(options) } + elsif defined?(@_bson_collection_name) + { @_bson_collection_name => to_hash(options) } + else + to_hash(options) + end + Rabl.configuration.bson_engine.serialize(result).to_s + end + # Sets the object to be used as the data source for this template # object(@user) # object @user => :person @@ -85,6 +100,7 @@ def object(data) # collection @users => :people def collection(data) @_collection_name = data.values.first if data.respond_to?(:each_pair) + @_bson_collection_name = data_name(data) if is_collection?(data) self.object(data_object(data).to_a) if data end diff --git a/lib/rabl/helpers.rb b/lib/rabl/helpers.rb index 62159fc7..5e037c46 100644 --- a/lib/rabl/helpers.rb +++ b/lib/rabl/helpers.rb @@ -1,3 +1,5 @@ +require 'active_support/inflector' # for the sake of pluralizing + module Rabl module Helpers # data_object(data) => @@ -18,7 +20,7 @@ def data_name(data) return data.values.first if data.is_a?(Hash) # @user => :user data = @_object.send(data) if data.is_a?(Symbol) && @_object # :address if data.respond_to?(:first) - data_name(data.first).pluralize if data.first.present? + data_name(data.first).to_s.pluralize if data.first.present? else # actual data object object_name = @_collection_name.to_s.singularize if defined? @_collection_name object_name ||= data.class.respond_to?(:model_name) ? data.class.model_name.element : data.class.to_s.downcase diff --git a/rabl.gemspec b/rabl.gemspec index 813f3887..730a43b2 100644 --- a/rabl.gemspec +++ b/rabl.gemspec @@ -9,8 +9,8 @@ Gem::Specification.new do |s| s.authors = ["Nathan Esquenazi"] s.email = ["nesquena@gmail.com"] s.homepage = "https://github.com/nesquena/rabl" - s.summary = %q{General ruby templating for json or xml} - s.description = %q{General ruby templating for json or xml} + s.summary = %q{General ruby templating with json, bson, xml and msgpack support} + s.description = %q{General ruby templating with json, bson, xml and msgpack support} s.rubyforge_project = "rabl" @@ -28,4 +28,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'tilt' s.add_development_dependency 'yajl-ruby' s.add_development_dependency 'msgpack', '~> 0.4.5' + s.add_development_dependency 'bson', '~> 1.5.2' end diff --git a/test/bson_engine_test.rb b/test/bson_engine_test.rb new file mode 100644 index 00000000..73b8e372 --- /dev/null +++ b/test/bson_engine_test.rb @@ -0,0 +1,331 @@ +require File.expand_path('../teststrap', __FILE__) +require File.expand_path('../../lib/rabl', __FILE__) +require File.expand_path('../../lib/rabl/template', __FILE__) +require File.expand_path('../models/user', __FILE__) + +context "Rabl::Engine" do + + helper(:rabl) { |t| RablTemplate.new("code", :format => 'bson') { t } } + + context "with bson defaults" do + setup do + Rabl.configure do |config| + # Comment this line out because include_bson_root is default. + #config.include_bson_root = true + end + end + + context "#object" do + + asserts "that it sets data source" do + template = rabl %q{ + object @user + } + scope = Object.new + scope.instance_variable_set :@user, User.new + template.render(scope) + end.matches "\x10\x00\x00\x00\x03user\x00\x05\x00\x00\x00\x00\x00" + + asserts "that it can set root node" do + template = rabl %q{ + object @user => :person + } + scope = Object.new + scope.instance_variable_set :@user, User.new + template.render(scope).split("").sort + end.equals "\x12\x00\x00\x00\x03person\x00\x05\x00\x00\x00\x00\x00".split("").sort + end + + context "#collection" do + + asserts "that it sets object to be casted as a simple array" do + template = rabl %{ + collection @users + } + scope = Object.new + scope.instance_variable_set :@users, [User.new, User.new] + template.render(scope).split("").sort + end.equals "7\x00\x00\x00\x04users\x00+\x00\x00\x00\x030\x00\x10\x00\x00\x00\x03user\x00\x05\x00\x00\x00\x00\x00\x031\x00\x10\x00\x00\x00\x03user\x00\x05\x00\x00\x00\x00\x00\x00\x00".split("").sort + + asserts "that it sets root node for objects" do + template = rabl %{ + collection @users => :people + } + scope = Object.new + scope.instance_variable_set :@users, [User.new, User.new] + template.render(scope).split("").sort + end.equals "<\x00\x00\x00\x04people\x00/\x00\x00\x00\x030\x00\x12\x00\x00\x00\x03person\x00\x05\x00\x00\x00\x00\x00\x031\x00\x12\x00\x00\x00\x03person\x00\x05\x00\x00\x00\x00\x00\x00\x00".split("").sort + + end + + context "#attribute" do + + asserts "that it adds an attribute or method to be included in output" do + template = rabl %{ + object @user + attribute :name + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "!\x00\x00\x00\x03user\x00\x16\x00\x00\x00\x02name\x00\a\x00\x00\x00irvine\x00\x00\x00".split("").sort + + asserts "that it can add attribute under a different key name through :as" do + template = rabl %{ + object @user + attribute :name, :as => 'city' + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "!\x00\x00\x00\x03user\x00\x16\x00\x00\x00\x02city\x00\a\x00\x00\x00irvine\x00\x00\x00".split("").sort + + asserts "that it can add attribute under a different key name through hash" do + template = rabl %{ + object @user + attribute :name => :city + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "!\x00\x00\x00\x03user\x00\x16\x00\x00\x00\x02city\x00\a\x00\x00\x00irvine\x00\x00\x00".split("").sort + + end + + context "#code" do + + asserts "that it can create an arbitraty code node" do + template = rabl %{ + code(:foo) { 'bar' } + } + template.render(Object.new).split("").sort + end.equals "\x12\x00\x00\x00\x02foo\x00\x04\x00\x00\x00bar\x00\x00".split("").sort + + asserts "that it can be passed conditionals" do + template = rabl %{ + code(:foo, :if => lambda { |i| false }) { 'bar' } + } + template.render(Object.new).split("").sort + end.equals "\x05\x00\x00\x00\x00".split("").sort + + end + + context "#child" do + + asserts "that it can create a child node" do + template = rabl %{ + object @user + attribute :name + child(@user) { attribute :city } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA') + template.render(scope).split("").sort + end.equals "6\x00\x00\x00\x03user\x00+\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x03user\x00\x12\x00\x00\x00\x02city\x00\x03\x00\x00\x00LA\x00\x00\x00\x00".split("").sort + + asserts "that it can create a child node with different key" do + template = rabl %{ + object @user + attribute :name + child(@user => :person) { attribute :city } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA') + template.render(scope).split("").sort + end.equals "8\x00\x00\x00\x03user\x00-\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x03person\x00\x12\x00\x00\x00\x02city\x00\x03\x00\x00\x00LA\x00\x00\x00\x00".split("").sort + end + + context "#glue" do + + asserts "that it glues data from a child node" do + template = rabl %{ + object @user + attribute :name + glue(@user) { attribute :city } + glue(@user) { attribute :age } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA', :age => 12) + template.render(scope).split("").sort + end.equals "4\x00\x00\x00\x03user\x00)\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x02city\x00\x03\x00\x00\x00LA\x00\x10age\x00\f\x00\x00\x00\x00\x00".split("").sort + end + + teardown do + Rabl.reset_configuration! + end + end + + context "with bson_engine" do + setup do + class CustomEncodeEngine + def self.serialize string + 42 + end + end + + Rabl.configure do |config| + config.bson_engine = CustomEncodeEngine + end + end + + asserts 'that it returns process by custom to_json' do + template = rabl %q{ + object @user + } + scope = Object.new + scope.instance_variable_set :@user, User.new + template.render(scope) + end.equals "42" + + teardown do + Rabl.reset_configuration! + end + end + + context "without bson root" do + setup do + Rabl.configure do |config| + config.include_bson_root = false + end + end + + context "#object" do + + asserts "that it sets data source" do + template = rabl %q{ + object @user + } + scope = Object.new + scope.instance_variable_set :@user, User.new + template.render(scope) + end.matches "\x05\x00\x00\x00\x00" + + asserts "that it can set root node" do + template = rabl %q{ + object @user => :person + } + scope = Object.new + scope.instance_variable_set :@user, User.new + template.render(scope) + end.equals "\x05\x00\x00\x00\x00" + end + + context "#collection" do + + asserts "that it sets object to be casted as a simple array" do + template = rabl %{ + collection @users + } + scope = Object.new + scope.instance_variable_set :@users, [User.new, User.new] + template.render(scope).split("").sort + end.equals "!\x00\x00\x00\x04users\x00\x15\x00\x00\x00\x030\x00\x05\x00\x00\x00\x00\x031\x00\x05\x00\x00\x00\x00\x00\x00".split("").sort + + asserts "that it sets root node for objects" do + template = rabl %{ + collection @users => :person + } + scope = Object.new + scope.instance_variable_set :@users, [User.new, User.new] + template.render(scope).split("").sort + end.equals "\"\x00\x00\x00\x04person\x00\x15\x00\x00\x00\x030\x00\x05\x00\x00\x00\x00\x031\x00\x05\x00\x00\x00\x00\x00\x00".split("").sort + + end + + context "#attribute" do + + asserts "that it adds an attribute or method to be included in output" do + template = rabl %{ + object @user + attribute :name + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "\x16\x00\x00\x00\x02name\x00\a\x00\x00\x00irvine\x00\x00".split("").sort + + asserts "that it can add attribute under a different key name through :as" do + template = rabl %{ + object @user + attribute :name, :as => 'city' + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "\x16\x00\x00\x00\x02city\x00\a\x00\x00\x00irvine\x00\x00".split("").sort + + asserts "that it can add attribute under a different key name through hash" do + template = rabl %{ + object @user + attribute :name => :city + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'irvine') + template.render(scope).split("").sort + end.equals "\x16\x00\x00\x00\x02city\x00\a\x00\x00\x00irvine\x00\x00".split("").sort + + end + + context "#code" do + + asserts "that it can create an arbitraty code node" do + template = rabl %{ + code(:foo) { 'bar' } + } + template.render(Object.new).split("").sort + end.equals "\x12\x00\x00\x00\x02foo\x00\x04\x00\x00\x00bar\x00\x00".split("").sort + + asserts "that it can be passed conditionals" do + template = rabl %{ + code(:foo, :if => lambda { |i| false }) { 'bar' } + } + template.render(Object.new).split("").sort + end.equals "\x05\x00\x00\x00\x00".split("").sort + + end + + context "#child" do + + asserts "that it can create a child node" do + template = rabl %{ + object @user + attribute :name + child(@user) { attribute :city } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA') + template.render(scope).split("").sort + end.equals "+\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x03user\x00\x12\x00\x00\x00\x02city\x00\x03\x00\x00\x00LA\x00\x00\x00".split("").sort + + asserts "that it can create a child node with different key" do + template = rabl %{ + object @user + attribute :name + child(@user => :person) { attribute :city } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA') + template.render(scope).split("").sort + end.equals "-\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x03person\x00\x12\x00\x00\x00\x02city\x00\x03\x00\x00\x00LA\x00\x00\x00".split("").sort + end + + context "#glue" do + + asserts "that it glues data from a child node" do + template = rabl %{ + object @user + attribute :name + glue(@user) { attribute :city } + glue(@user) { attribute :age } + } + scope = Object.new + scope.instance_variable_set :@user, User.new(:name => 'leo', :city => 'LA', :age => 12) + template.render(scope).split("").sort + end.equals ")\x00\x00\x00\x02name\x00\x04\x00\x00\x00leo\x00\x02city\x00\x03\x00\x00\x00LA\x00\x10age\x00\f\x00\x00\x00\x00".split("").sort + end + + teardown do + Rabl.reset_configuration! + end + end +end