Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 649f237b20501b8e879fa6ca59192e9c87a20c9e 0 parents
Vicente Mundim vicentemundim authored
2  .bundle/config
@@ -0,0 +1,2 @@
+---
+BUNDLE_WITHOUT: ""
3  .rspec
@@ -0,0 +1,3 @@
+--colour
+--format documentation
+
17 Gemfile
@@ -0,0 +1,17 @@
+source 'http://rubygems.org'
+
+RSPEC_VERSION = '~> 2.0.0.beta.18'
+MONGOID_VERSION = '~> 2.0.0.beta9'
+
+gem 'bson_ext', '1.0.4'
+gem 'mongoid', MONGOID_VERSION
+
+group(:test) do
+ gem 'rspec', RSPEC_VERSION
+ gem 'rspec-core', RSPEC_VERSION, :require => 'rspec/core'
+ gem 'rspec-expectations', RSPEC_VERSION, :require => 'rspec/expectations'
+ gem 'rspec-mocks', RSPEC_VERSION, :require => 'rspec/mocks'
+
+ gem 'database_cleaner'
+end
+
20 MIT_LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2010 Vicente Mundim
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
83 README.rdoc
@@ -0,0 +1,83 @@
+= Overview
+
+== About Mongoid::QueryStringInterace
+
+Gives a method that can parse query string parameters into a set of criterias
+that Mongoid can use to perform actual queries in MongoDB databases for a given
+model.
+
+== Repository
+
+http://github.com/vicentemundim/mongoid-query-string-interface
+
+= Installing
+
+This is a gem, so you can install it by:
+
+ sudo gem install mongoid_query_string_interface
+
+Or, if you are using rails, put this in your Gemfile:
+
+ gem 'mongoid_query_string_interface'
+
+= Usage
+
+To use it, just extend Mongoid::QueryStringInterface in your document model:
+
+ class Document
+ include Mongoid::Document
+ extend Mongoid::QueryInterfaceString
+
+ # ... add fields here
+ end
+
+Then, in your controllers put:
+
+ def index
+ @documents = Document.filter_by(params)
+ # ... do something like render a HTML template or a XML/JSON view of documents
+ end
+
+That's it! Now you can do something like this:
+
+ http://myhost.com/documents?tags.all=ruby|rails|mongodb&tags.nin=sql|java&updated_at.gt=2010-01-01&created_at.desc&per_page=10&page=3
+
+This would get all documents which have the tags 'ruby', 'rails' and 'mongo',
+and that don't have both 'sql' and 'java' tags, and that were updated after
+2010-01-01, ordered by descending created_at. It will return 10 elements per
+page, and the 3rd page of results.
+
+You could even query for embedded documents, and use any of the Mongodb
+conditional operators:
+
+ http://myhost.com/documents?comments.author=Shrek
+
+Which would get all documents that have been commented by 'Shrek'. Basically,
+any valid path that you can use in a Mongoid::Criteria can be used here, and
+you can also append any of the Mongodb conditional operators.
+
+You can sort results like this:
+
+ http://myhost.com/documents?order_by=created_at.desc
+
+or
+
+ http://myhost.com/documents?order_by=created_at
+
+Which is the same as:
+
+ http://myhost.com/documents?order_by=created_at.asc
+
+To order by more than one field:
+
+ http://myhost.com/documents?created_at.desc&updated_at.desc
+
+Or even:
+
+ http://myhost.com/documents?created_at=desc&updated_at=desc
+
+Check the specs for more use cases.
+
+= Credits
+
+Vicente Mundim: vicente.mundim at gmail dot com
36 Rakefile
@@ -0,0 +1,36 @@
+require "rake"
+require "rake/rdoctask"
+require "rspec"
+require "rspec/core/rake_task"
+
+require File.expand_path('lib/version.rb', File.dirname(__FILE__))
+
+task :build do
+ system "gem build mongoid_query_string_interface.gemspec"
+end
+
+task :install => :build do
+ system "sudo gem install mongoid_query_string_interface-#{Mongoid::QueryStringInterface::VERSION}.gem"
+end
+
+task :release => :build do
+ puts "Tagging #{Mongoid::QueryStringInterface::VERSION}..."
+ system "git tag -a #{Mongoid::QueryStringInterface::VERSION} -m 'Bumping to version #{Mongoid::QueryStringInterface::VERSION}'"
+ puts "Pushing to Github..."
+ system "git push --tags"
+ puts "Pushing to Rubygems..."
+ system "gem push mongoid_query_string_interface-#{Mongoid::QueryStringInterface::VERSION}.gem"
+end
+
+Rspec::Core::RakeTask.new(:spec) do |spec|
+ spec.pattern = "spec/**/*_spec.rb"
+end
+
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = "rdoc"
+ rdoc.title = "Mongoid Query String Interface #{Mongoid::QueryStringInterface::VERSION}"
+ rdoc.rdoc_files.include("README*")
+ rdoc.rdoc_files.include("lib/**/*.rb")
+end
+
+task :default => ["spec"]
175 lib/mongoid_query_string_interface.rb
@@ -0,0 +1,175 @@
+module Mongoid
+ module QueryStringInterface
+ CONDITIONAL_OPERATORS = [:all, :exists, :gte, :gt, :in, :lte, :lt, :ne, :nin, :size, :near, :within]
+ ARRAY_CONDITIONAL_OPERATORS = [:all, :in, :nin]
+ SORTING_OPERATORS = [:asc, :desc]
+
+ def filter_by(params={})
+ params = hash_with_indifferent_access(params)
+ where(filtering_options(params)).order_by(*sorting_options(params)).paginate(pagination_options(params))
+ end
+
+ def paginated_collection_with_filter_by(params={})
+ params = hash_with_indifferent_access(params)
+
+ pagination = pagination_options(params)
+ pager = WillPaginate::Collection.new pagination[:page], pagination[:per_page], where(filtering_options(params)).count
+
+ [:total_entries, :total_pages, :per_page, :offset, :previous_page, :current_page, :next_page].inject({}) do |result, attr|
+ result[attr] = pager.send(attr)
+ result
+ end
+ end
+
+ def default_filtering_options
+ {}
+ end
+
+ def default_sorting_options
+ []
+ end
+
+ private
+ def pagination_options(options)
+ options.reverse_merge :per_page => 12, :page => 1
+ end
+
+ def filtering_options(options)
+ default_filtering_options.merge(parse_operators(only_filtering(options)))
+ end
+
+ def sorting_options(options)
+ options = only_sorting(options)
+
+ sorting_options = []
+ sorting_options.concat(parse_order_by(options))
+ sorting_options.concat(parse_sorting(options))
+
+ sorting_options.empty? ? default_sorting_options : sorting_options
+ end
+
+ def parse_operators(options)
+ options.inject({}) do |result, item|
+ key, value = item
+
+ attribute = attribute_from(key)
+ operator = operator_from(key)
+ value = parse_value(value, operator)
+
+ if operator
+ filter = { operator => value }
+
+ if result.has_key?(attribute)
+ result[attribute].merge!(filter)
+ else
+ result[attribute] = filter
+ end
+ else
+ result[attribute] = value
+ end
+
+ result
+ end
+ end
+
+ def attribute_from(key)
+ if match = key.match(/(.*)\.(#{(CONDITIONAL_OPERATORS + SORTING_OPERATORS).join('|')})/)
+ match[1].to_sym
+ else
+ key.to_sym
+ end
+ end
+
+ def operator_from(key)
+ if match = key.match(/.*\.(#{CONDITIONAL_OPERATORS.join('|')})/)
+ "$#{match[1]}".to_sym
+ end
+ end
+
+ def parse_value(value, operator)
+ parse_date(value) or parse_integer(value) or parse_array(value, operator) or value
+ end
+
+ def parse_date(date)
+ date.to_time and Time.parse(date)
+ rescue Exception
+ nil
+ end
+
+ def parse_integer(integer)
+ if match = integer.match(/\d+/)
+ match[0].to_i
+ end
+ end
+
+ def parse_float(float)
+ if match = float.match(/^(\d+)(\.?\d*)$/)
+ match[0].to_f
+ end
+ end
+
+ def parse_array(value, operator)
+ split_and_strip(value) if array_operator?(operator)
+ end
+
+ def array_operator?(operator)
+ ARRAY_CONDITIONAL_OPERATORS.map { |op| "$#{op}" }.include?(operator.to_s)
+ end
+
+ def split_and_strip(values)
+ values.split('|').map(&:strip)
+ end
+
+ def hash_with_indifferent_access(params)
+ params.is_a?(HashWithIndifferentAccess) ? params : HashWithIndifferentAccess.new(params)
+ end
+
+ def only_filtering(options)
+ options.except(*only_sorting(options).keys).except(:per_page, :page, :action, :controller, :format, :order_by)
+ end
+
+ def only_sorting(options)
+ options.inject({}) do |result, item|
+ key, value = item
+ result[key] = value if sorting_parameter?(key, value)
+ result
+ end
+ end
+
+ def sorting_parameter?(key, value)
+ key.to_s == 'order_by' or key.match(/(.*)\.(#{SORTING_OPERATORS.join('|')})/) or SORTING_OPERATORS.include?(value.to_sym)
+ end
+
+ def parse_order_by(options)
+ sorting_options = []
+
+ if order_by = options.delete('order_by')
+ if match = order_by.match(/(.*)\.(#{SORTING_OPERATORS.join('|')})/)
+ sorting_options << match[1].to_sym.send(match[2])
+ else
+ sorting_options << order_by.to_sym.asc
+ end
+ end
+
+ sorting_options
+ end
+
+ def parse_sorting(options)
+ options.inject([]) do |result, item|
+ key, value = item
+
+ attribute = attribute_from(key)
+ sorting_operator = sorting_operator_from(key)
+
+ result << attribute.send(sorting_operator || value)
+ result
+ end
+ end
+
+ def sorting_operator_from(key)
+ if match = key.match(/.*\.(#{SORTING_OPERATORS.join('|')})/)
+ match[1].to_sym
+ end
+ end
+ end
+end
6 lib/version.rb
@@ -0,0 +1,6 @@
+# encoding: utf-8
+module Mongoid #:nodoc
+ module QueryStringInterface #:nodoc
+ VERSION = "0.1.0"
+ end
+end
24 mongoid_query_string_interface.gemspec
@@ -0,0 +1,24 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('lib/version.rb', File.dirname(__FILE__))
+
+Gem::Specification.new do |s|
+ s.name = "mongoid_query_string_interface"
+ s.version = Mongoid::QueryStringInterface::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Vicente Mundim"]
+ s.email = ["vicente.mundim@gmail.com"]
+ s.homepage = "http://github.com/vicentemundim/mongoid_query_string_interface"
+ s.summary = "An interface for performing queries in MongoDB using query string parameters."
+ s.description = "Gives a method that can parse query string parameters into a set of criterias that Mongoid can use to perform actual queries in MongoDB databases for a given model."
+
+ s.required_rubygems_version = ">= 1.3.6"
+ s.rubyforge_project = "mongoid_query_string_interface"
+
+ s.add_runtime_dependency("mongoid", ["~>2.0.0.beta"])
+
+ s.add_development_dependency(%q<rspec>, ["~> 2.0.0.beta"])
+
+ s.files = Dir.glob("lib/**/*") + %w(MIT_LICENSE README.rdoc)
+ s.require_path = 'lib'
+end
+
234 spec/mongoid_query_string_interface_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+class Document
+ include Mongoid::Document
+ extend Mongoid::QueryStringInterface
+
+ field :title
+ field :some_integer, :type => Integer
+ field :some_float, :type => Float
+ field :created_at, :type => Time
+ field :tags, :type => Array
+ field :status
+
+ embeds_one :embedded_document
+
+ def self.default_filtering_options
+ { :status => 'published' }
+ end
+
+ def self.default_sorting_options
+ [:created_at.desc]
+ end
+end
+
+class EmbeddedDocument
+ include Mongoid::Document
+
+ field :name
+ field :tags, :type => Array
+
+ embedded_in :document, :inverse_of => :embedded_document
+end
+
+describe Mongoid::QueryStringInterface do
+ let :document do
+ Document.create :title => 'Some Title', :some_integer => 1, :some_float => 1.1, :status => 'published',
+ :created_at => 5.days.ago.to_time, :tags => ['esportes', 'basquete', 'flamengo'],
+ :embedded_document => { :name => 'embedded document',
+ :tags => ['bar', 'foo', 'yeah'] }
+ end
+
+ let :other_document do
+ Document.create :title => 'Some Other Title', :some_integer => 2, :some_float => 2.2, :status => 'published',
+ :created_at => 2.days.ago.to_time, :tags => ['esportes', 'futebol', 'jabulani', 'flamengo'],
+ :embedded_document => { :name => 'other embedded document',
+ :tags => ['yup', 'uhu', 'yeah'] }
+ end
+
+ before :each do
+ # creates the document and other document
+ document and other_document
+ end
+
+ context 'with default filtering options' do
+ it 'should use the default filtering options' do
+ Document.create :status => 'not published' # this should not be retrieved
+ Document.filter_by.should == [other_document, document]
+ end
+ end
+
+ context 'with default sorting options' do
+ it 'should use the default sorting options if no sorting option is given' do
+ Document.filter_by.should == [other_document, document]
+ end
+
+ it 'should use the given sorting options and ignore the default sorting options' do
+ Document.should_not_receive(:default_sorting_options)
+ Document.filter_by('created_at.asc' => nil).should == [document, other_document]
+ end
+ end
+
+ context 'with pagination' do
+ before :each do
+ @context = mock('context')
+ Document.stub!(:where).and_return(@context)
+ @context.stub!(:order_by).and_return(@context)
+ end
+
+ it 'should paginate the result by default' do
+ @context.should_receive(:paginate).with('page' => 1, 'per_page' => 12)
+ Document.filter_by
+ end
+
+ it 'should use the page and per_page parameters if they are given' do
+ @context.should_receive(:paginate).with('page' => 3, 'per_page' => 20)
+ Document.filter_by 'page' => 3, 'per_page' => 20
+ end
+ end
+
+ context 'with sorting' do
+ it 'should use order_by parameter to sort' do
+ Document.filter_by('order_by' => 'created_at.desc').should == [other_document, document]
+ end
+
+ it 'should use asc as default if only the attribute name is given' do
+ Document.filter_by('order_by' => 'created_at').should == [document, other_document]
+ end
+
+ it 'should use parameters with .desc modifiers to add sort options' do
+ Document.filter_by('created_at.desc' => nil).should == [other_document, document]
+ end
+
+ it 'should use parameters with desc value to add sort options' do
+ Document.filter_by('created_at' => 'desc').should == [other_document, document]
+ end
+
+ it 'should use parameters with .asc modifiers to add sort options' do
+ Document.filter_by('created_at.asc' => nil).should == [document, other_document]
+ end
+
+ it 'should use parameters with asc value to add sort options' do
+ Document.filter_by('created_at' => 'asc').should == [document, other_document]
+ end
+ end
+
+ context 'with filtering' do
+ it 'should use a simple filter on a document attribute' do
+ Document.filter_by('title' => document.title).should == [document]
+ end
+
+ it 'should use a complex filter in an embedded document attribute' do
+ Document.filter_by('embedded_document.name' => document.embedded_document.name).should == [document]
+ end
+
+ it 'should ignore pagination parameters' do
+ Document.filter_by('title' => document.title, 'page' => 1, 'per_page' => 20).should == [document]
+ end
+
+ it 'should ignore order_by parameters' do
+ Document.filter_by('title' => document.title, 'order_by' => 'created_at').should == [document]
+ end
+
+ it 'should ignore parameters with .asc' do
+ Document.filter_by('title' => document.title, 'created_at.asc' => nil).should == [document]
+ end
+
+ it 'should ignore parameters with .desc' do
+ Document.filter_by('title' => document.title, 'created_at.desc' => nil).should == [document]
+ end
+
+ it 'should ignore parameters with asc value' do
+ Document.filter_by('title' => document.title, 'created_at' => 'asc').should == [document]
+ end
+
+ it 'should ignore parameters with desc value' do
+ Document.filter_by('title' => document.title, 'created_at' => 'desc').should == [document]
+ end
+
+ it 'should ignore controller, action and format parameters' do
+ Document.filter_by('title' => document.title, 'controller' => 'documents', 'action' => 'index', 'format' => 'json').should == [document]
+ end
+
+ context 'with conditional operators' do
+ it 'should use it when given as the last portion of attribute name' do
+ Document.filter_by('title.ne' => 'Some Other Title').should == [document]
+ end
+
+ it 'should accept different conditional operators for the same attribute' do
+ Document.filter_by('created_at.gt' => 6.days.ago.to_s, 'created_at.lt' => 4.days.ago.to_s).should == [document]
+ end
+
+ context 'with date values' do
+ it 'should parse a date correctly' do
+ Document.filter_by('created_at' => 5.days.ago.to_s).should == [document]
+ end
+ end
+
+ context 'with number values' do
+ it 'should parse a integer correctly' do
+ Document.filter_by('some_integer.lt' => '2').should == [document]
+ end
+
+ it 'should parse a float correctly' do
+ Document.filter_by('some_float.lt' => '2.1').should == [document]
+ end
+ end
+
+ context 'with array values' do
+ let :document_with_similar_tags do
+ Document.create :title => 'Some Title', :some_number => 1, :status => 'published',
+ :created_at => 5.days.ago.to_time, :tags => ['esportes', 'basquete', 'flamengo', 'rede globo', 'esporte espetacular']
+ end
+
+ it 'should convert values into arrays for operator $all' do
+ Document.filter_by('tags.all' => document.tags.join('|')).should == [document]
+ end
+
+ it 'should convert values into arrays for operator $in' do
+ Document.filter_by('tags.in' => 'basquete|futebol').should == [other_document, document]
+ end
+
+ it 'should convert values into arrays for operator $nin' do
+ Document.create :tags => ['futebol', 'esportes'], :status => 'published' # should not be retrieved
+ Document.filter_by('tags.nin' => 'jabulani|futebol').should == [document]
+ end
+
+ it 'should convert single values into arrays for operator $all' do
+ Document.filter_by('tags.all' => 'basquete').should == [document]
+ end
+
+ it 'should convert single values into arrays for operator $in' do
+ Document.filter_by('tags.in' => 'basquete').should == [document]
+ end
+
+ it 'should convert single values into arrays for operator $nin' do
+ Document.filter_by('tags.nin' => 'jabulani').should == [document]
+ end
+
+ it 'should accept different conditional operators for the same attribute' do
+ document_with_similar_tags
+ Document.filter_by('tags.all' => 'esportes|basquete', 'tags.nin' => 'rede globo|esporte espetacular').should == [document]
+ end
+ end
+ end
+ end
+
+ describe 'when returning paginated collection' do
+ it 'should return a paginated collection' do
+ Document.paginated_collection_with_filter_by.should == {:total_entries => 2, :total_pages => 1, :per_page => 12, :offset => 0, :previous_page => nil, :current_page => 1, :next_page => nil}
+ end
+
+ it 'should accept filtering options' do
+ context = mock('context', :count => 1)
+ Document.should_receive(:where).with({:status => 'published', :title => document.title}).and_return(context)
+ Document.paginated_collection_with_filter_by(:title => document.title).should == {:total_entries => 1, :total_pages => 1, :per_page => 12, :offset => 0, :previous_page => nil, :current_page => 1, :next_page => nil}
+ end
+
+ it 'should use pagination options' do
+ context = mock('context', :count => 100)
+ Document.should_receive(:where).with({:status => 'published'}).and_return(context)
+ Document.paginated_collection_with_filter_by(:page => 3, :per_page => 20).should == {:total_entries => 100, :total_pages => 5, :per_page => 20, :offset => 40, :previous_page => 2, :current_page => 3, :next_page => 4}
+ end
+ end
+end
42 spec/spec_helper.rb
@@ -0,0 +1,42 @@
+begin
+ require 'bundler'
+ Bundler.setup
+ Bundler.require(:default, :test)
+rescue LoadError
+ puts 'Bundler is not installed, you need to gem install it in order to run the specs.'
+ exit 1
+end
+
+# Requires supporting files with custom matchers and macros, etc,
+# in ./support/ and its subdirectories.
+Dir[File.expand_path('support/**/*.rb', File.dirname(__FILE__))].each { |f| require f }
+
+# Requires lib.
+Dir[File.expand_path('../lib/**/*.rb', File.dirname(__FILE__))].each { |f| require f }
+
+# Setup Mongoid.
+Mongoid.configure do |config|
+ name = "query_string_interface_test"
+ config.master = Mongo::Connection.new.db(name)
+ config.allow_dynamic_fields = true
+ config.use_object_ids = true
+end
+
+RSpec.configure do |config|
+ # == Mock Framework
+ #
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
+ #
+ # config.mock_with :mocha
+ # config.mock_with :flexmock
+ # config.mock_with :rr
+ config.mock_with :rspec
+
+ config.before(:suite) do
+ DatabaseCleaner.strategy = :truncation
+ end
+
+ config.before(:each) do
+ DatabaseCleaner.clean
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.