mongoid-scroll 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format=documentation
3
+
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ services:
2
+ - mongodb
3
+
4
+ rvm:
5
+ - 1.9.3
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ 0.1.0 (TBD)
2
+ ===========
3
+
4
+ * Initial public release - [@dblock](https://github.com/dblock).
5
+
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem "rake"
7
+ gem "bundler"
8
+ gem "rspec"
9
+ end
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2012 Daniel Doubrovkine, Artsy Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ Mongoid::Scroll [![Build Status](https://travis-ci.org/dblock/mongoid-scroll.png?branch=master)](https://travis-ci.org/dblock/mongoid-scroll)
2
+ ===============
3
+
4
+ Mongoid extension that enable infinite scrolling.
5
+
6
+ The Problem
7
+ -----------
8
+
9
+ Traditional pagination does not work when data changes between paginated requests, which makes it unsuitable for infinite scroll behaviors.
10
+
11
+ * If a record is inserted before the current page limit, the collection will shift to the right, and the returned result will include a duplicate from a previous page.
12
+ * If a record is removed before the current page limit, the collection will shift to the left, and the returned result will be missing a record.
13
+
14
+ The solution implemented by the `scroll` extension paginates data using a cursor, giving you the ability to restart pagination where you left it off. This is a non-trivial problem when combined with sorting over non-unique record fields, such as timestamps.
15
+
16
+ Usage
17
+ -----
18
+
19
+ Add `mongoid-scroll` to Gemfile.
20
+
21
+ ```ruby
22
+ gem 'mongoid-scroll'
23
+ ```
24
+
25
+ A sample model.
26
+
27
+ ```ruby
28
+ module Feed
29
+ class Item
30
+ include Mongoid::Document
31
+ field :content, type: String
32
+ field :created_at, type: DateTime
33
+ end
34
+ end
35
+ ```
36
+
37
+ Scroll and save a cursor to the last item.
38
+
39
+ ```ruby
40
+ saved_cursor = nil
41
+ Feed::Item.desc(:created_at).limit(5).scroll do |record, next_cursor|
42
+ # each record, one-by-one
43
+ saved_cursor = next_cursor
44
+ end
45
+ ```
46
+
47
+ Resume iterating using the previously saved cursor.
48
+
49
+ ```ruby
50
+ Feed::Item.desc(:created_at).limit(5).scroll(saved_cursor) do |record, next_cursor|
51
+ # each record, one-by-one
52
+ saved_cursor = next_cursor
53
+ end
54
+ ```
55
+
56
+ The iteration finishes when no more records are available. You can also finish iterating over the remaining records by omitting the query limit.
57
+
58
+ ```ruby
59
+ Feed::Item.desc(:created_at).scroll(saved_cursor) do |record, next_cursor|
60
+ # each record, one-by-one
61
+ end
62
+ ```
63
+
64
+ Cursors
65
+ -------
66
+
67
+ You can use `Mongoid::Scroll::Cursor.from_record` to generate a cursor. This can be useful when you just want to return a collection of results and the cursor pointing to after the last item.
68
+
69
+ ```ruby
70
+ record = Feed::Item.desc(:created_at).limit(3).last
71
+ cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["created_at"] })
72
+ # cursor or cursor.to_s can be returned to a client and passed into .scroll(cursor)
73
+ ```
74
+
75
+ Note that unlike MongoDB cursors, `Mongoid::Scroll::Cursor` values don't expire.
76
+
77
+ Contributing
78
+ ------------
79
+
80
+ Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
81
+
82
+ Copyright and License
83
+ ---------------------
84
+
85
+ MIT License, see [LICENSE](http://github.com/dblock/mongoid-scroll/raw/master/LICENSE.md) for details.
86
+
87
+ (c) 2013 [Daniel Doubrovkine](http://github.com/dblock), based on code by [Frank Macreery](http://github.com/macreery), [Artsy Inc.](http://artsy.net)
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require File.expand_path('../lib/mongoid/scroll/version', __FILE__)
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'rake'
15
+
16
+ require 'rspec/core'
17
+ require 'rspec/core/rake_task'
18
+
19
+ RSpec::Core::RakeTask.new(:spec) do |spec|
20
+ spec.pattern = FileList['spec/**/*_spec.rb']
21
+ end
22
+
23
+ task :default => :spec
24
+
@@ -0,0 +1,22 @@
1
+ en:
2
+ mongoid:
3
+ scroll:
4
+ errors:
5
+ messages:
6
+ multiple_sort_fields:
7
+ message: "Scrolling over a criteria with multiple fields is not supported."
8
+ summary: "You're attempting to scroll over data with a sort order that includes multiple fields: %{sort}."
9
+ resolution: "Simplify the sort order to a single field."
10
+ invalid_cursor:
11
+ message: "The cursor supplied is invalid."
12
+ summary: "The cursor supplied is invalid: %{cursor}."
13
+ resolution: "Cursors must be in the form 'value:tiebreak_id'."
14
+ no_such_field:
15
+ message: "Invalid field."
16
+ summary: "The field supplied in the cursor does not exist: %{field}."
17
+ resolution: "Has the model changed or are you not sorting the criteria by the right field."
18
+ unsupported_field_type:
19
+ message: "Unsupported field type."
20
+ summary: "The type of the field '%{field}' is not supported: %{type}."
21
+ resolution: "Please open a feature request in https://github.com/dblock/mongoid-scroll."
22
+
@@ -0,0 +1,33 @@
1
+ module Mongoid
2
+ module Criterion
3
+ module Scrollable
4
+
5
+ def scroll(cursor = nil, &block)
6
+ c = self
7
+ # we don't support scrolling over a criteria with multiple fields
8
+ if c.options[:sort] && c.options[:sort].keys.count != 1
9
+ sort = c.options[:sort].keys.join(", ")
10
+ raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: sort)
11
+ end
12
+ # introduce a default sort order if there's none
13
+ c = c.asc(:_id) if (! c.options[:sort]) || c.options[:sort].empty?
14
+ # scroll field and direction
15
+ scroll_field = c.options[:sort].keys.first
16
+ scroll_direction = c.options[:sort].values.first.to_i == 1 ? '$gt' : '$lt'
17
+ # scroll cursor from the parameter, with value and tiebreak_id
18
+ field = c.klass.fields[scroll_field.to_s]
19
+ cursor_options = { field: field, direction: scroll_direction }
20
+ cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
21
+ # scroll
22
+ if block_given?
23
+ c.where(cursor.criteria).each do |record|
24
+ yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
25
+ end
26
+ else
27
+ c
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,78 @@
1
+ module Mongoid
2
+ module Scroll
3
+ class Cursor
4
+
5
+ attr_accessor :value, :tiebreak_id, :field, :direction
6
+
7
+ def initialize(value = nil, options = {})
8
+ unless options && (@field = options[:field])
9
+ raise ArgumentError.new "Missing options[:field]."
10
+ end
11
+ @direction = options[:direction] || '$gt'
12
+ @value, @tiebreak_id = Mongoid::Scroll::Cursor.parse(value, options)
13
+ end
14
+
15
+ def criteria
16
+ cursor_criteria = { field.name => { direction => value } } if value
17
+ tiebreak_criteria = { field.name => value, :_id => { '$gt' => tiebreak_id } } if value && tiebreak_id
18
+ (cursor_criteria || tiebreak_criteria) ? { '$or' => [ cursor_criteria, tiebreak_criteria].compact } : {}
19
+ end
20
+
21
+ class << self
22
+ def from_record(record, options)
23
+ unless options && (field = options[:field])
24
+ raise ArgumentError.new "Missing options[:field]."
25
+ end
26
+ Mongoid::Scroll::Cursor.new("#{transform_field_value(field, record.send(field.name))}:#{record.id}", options)
27
+ end
28
+ end
29
+
30
+ def to_s
31
+ tiebreak_id ? [ Mongoid::Scroll::Cursor.transform_field_value(field, value), tiebreak_id ].join(":") : nil
32
+ end
33
+
34
+ private
35
+
36
+ class << self
37
+
38
+ def parse(value, options)
39
+ return [ nil, nil ] unless value
40
+ parts = value.split(":")
41
+ unless parts.length >= 2
42
+ raise Mongoid::Scroll::Errors::InvalidCursorError.new({ cursor: value })
43
+ end
44
+ id = parts[-1]
45
+ value = parts[0...-1].join(":")
46
+ [ parse_field_value(options[:field], value), Moped::BSON::ObjectId(id) ]
47
+ end
48
+
49
+ def parse_field_value(field, value)
50
+ case field.type.to_s
51
+ when "String" then value.to_s
52
+ when "DateTime" then Time.at(value.to_i).to_datetime
53
+ when "Time" then Time.at(value.to_i)
54
+ when "Date" then Time.at(value.to_i).utc.to_date
55
+ when "Float" then value.to_f
56
+ when "Integer" then value.to_i
57
+ else
58
+ raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field.name, type: field.type)
59
+ end
60
+ end
61
+
62
+ def transform_field_value(field, value)
63
+ case field.type.to_s
64
+ when "String" then value.to_s
65
+ when "Date" then value.to_time(:utc).to_i
66
+ when "DateTime", "Time" then value.to_i
67
+ when "Float" then value.to_f
68
+ when "Integer" then value.to_i
69
+ else
70
+ raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field.name, type: field.type)
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,81 @@
1
+ module Mongoid
2
+ module Scroll
3
+ module Errors
4
+ class Base < StandardError
5
+
6
+ # Problem occurred.
7
+ attr_reader :problem
8
+
9
+ # Summary of the problem.
10
+ attr_reader :summary
11
+
12
+ # Suggested problem resolution.
13
+ attr_reader :resolution
14
+
15
+ # Compose the message.
16
+ # === Parameters
17
+ # [key] Lookup key in the translation table.
18
+ # [attributes] The objects to pass to create the message.
19
+ def compose_message(key, attributes = {})
20
+ @problem = create_problem(key, attributes)
21
+ @summary = create_summary(key, attributes)
22
+ @resolution = create_resolution(key, attributes)
23
+
24
+ "\nProblem:\n #{@problem}"+
25
+ "\nSummary:\n #{@summary}"+
26
+ "\nResolution:\n #{@resolution}"
27
+ end
28
+
29
+ private
30
+
31
+ BASE_KEY = "mongoid.scroll.errors.messages" #:nodoc:
32
+
33
+ # Given the key of the specific error and the options hash, translate the
34
+ # message.
35
+ #
36
+ # === Parameters
37
+ # [key] The key of the error in the locales.
38
+ # [options] The objects to pass to create the message.
39
+ #
40
+ # Returns a localized error message string.
41
+ def translate(key, options)
42
+ ::I18n.translate("#{BASE_KEY}.#{key}", { :locale => :en }.merge(options)).strip
43
+ end
44
+
45
+ # Create the problem.
46
+ #
47
+ # === Parameters
48
+ # [key] The error key.
49
+ # [attributes] The attributes to interpolate.
50
+ #
51
+ # Returns the problem.
52
+ def create_problem(key, attributes)
53
+ translate("#{key}.message", attributes)
54
+ end
55
+
56
+ # Create the summary.
57
+ #
58
+ # === Parameters
59
+ # [key] The error key.
60
+ # [attributes] The attributes to interpolate.
61
+ #
62
+ # Returns the summary.
63
+ def create_summary(key, attributes)
64
+ translate("#{key}.summary", attributes)
65
+ end
66
+
67
+ # Create the resolution.
68
+ #
69
+ # === Parameters
70
+ # [key] The error key.
71
+ # [attributes] The attributes to interpolate.
72
+ #
73
+ # Returns the resolution.
74
+ def create_resolution(key, attributes)
75
+ translate("#{key}.resolution", attributes)
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,13 @@
1
+ module Mongoid
2
+ module Scroll
3
+ module Errors
4
+ class InvalidCursorError < Mongoid::Scroll::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("invalid_cursor", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Mongoid
2
+ module Scroll
3
+ module Errors
4
+ class MultipleSortFieldsError < Mongoid::Scroll::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("multiple_sort_fields", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Mongoid
2
+ module Scroll
3
+ module Errors
4
+ class NoSuchFieldError < Mongoid::Scroll::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("no_such_field", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Mongoid
2
+ module Scroll
3
+ module Errors
4
+ class UnsupportedFieldTypeError < Mongoid::Scroll::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("unsupported_field_type", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ require 'mongoid/scroll/errors/base'
2
+ require 'mongoid/scroll/errors/multiple_sort_fields_error'
3
+ require 'mongoid/scroll/errors/invalid_cursor_error'
4
+ require 'mongoid/scroll/errors/no_such_field_error'
5
+ require 'mongoid/scroll/errors/unsupported_field_type_error'
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Scroll
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require 'i18n'
2
+
3
+ I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")
4
+
5
+ require 'mongoid'
6
+ require 'mongoid/scroll/version'
7
+ require 'mongoid/scroll/errors'
8
+ require 'mongoid/scroll/cursor'
9
+ require 'mongoid/criterion/scrollable'
10
+
11
+ Mongoid::Criteria.send(:include, Mongoid::Criterion::Scrollable)
@@ -0,0 +1,2 @@
1
+ require 'mongoid-scroll'
2
+
@@ -0,0 +1,20 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "mongoid/scroll/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mongoid-scroll"
6
+ s.version = Mongoid::Scroll::VERSION
7
+ s.authors = [ "Daniel Doubrovkine", "Frank Macreery" ]
8
+ s.email = "dblock@dblock.org"
9
+ s.platform = Gem::Platform::RUBY
10
+ s.required_rubygems_version = '>= 1.3.6'
11
+ s.files = `git ls-files`.split("\n")
12
+ s.require_paths = [ "lib" ]
13
+ s.homepage = "http://github.com/dblock/mongoid-scroll"
14
+ s.licenses = [ "MIT" ]
15
+ s.summary = "Mongoid extensions to enable infinite scroll."
16
+ s.add_dependency "mongoid", ">= 3.0"
17
+ s.add_dependency "i18n"
18
+ end
19
+
20
+
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::Criteria do
4
+ context "scrollable" do
5
+ subject do
6
+ Feed::Item
7
+ end
8
+ it ":scroll" do
9
+ subject.should.respond_to? :scroll
10
+ end
11
+ end
12
+ context "with multiple sort fields" do
13
+ subject do
14
+ Feed::Item.desc(:name).asc(:value)
15
+ end
16
+ it "raises Mongoid::Scroll::Errors::MultipleSortFieldsError" do
17
+ expect { subject.scroll }.to raise_error Mongoid::Scroll::Errors::MultipleSortFieldsError,
18
+ /You're attempting to scroll over data with a sort order that includes multiple fields: name, value./
19
+ end
20
+ end
21
+ context "with no sort" do
22
+ subject do
23
+ Feed::Item.all
24
+ end
25
+ it "adds a default sort by _id" do
26
+ subject.scroll.options[:sort].should == { "_id" => 1 }
27
+ end
28
+ end
29
+ context "with data" do
30
+ before :each do
31
+ 10.times do |i|
32
+ Feed::Item.create!(
33
+ a_string: i.to_s,
34
+ a_integer: i,
35
+ a_datetime: DateTime.new(2013, i + 1, 21, 1, 42, 3)
36
+ )
37
+ end
38
+ end
39
+ context "integer" do
40
+ it "scrolls all with a block" do
41
+ records = []
42
+ Feed::Item.asc(:a_integer).scroll do |record, next_cursor|
43
+ records << record
44
+ end
45
+ records.size.should == 10
46
+ records.should eq Feed::Item.all.to_a
47
+ end
48
+ it "scrolls all with a break" do
49
+ records = []
50
+ cursor = nil
51
+ Feed::Item.asc(:a_integer).limit(5).scroll do |record, next_cursor|
52
+ records << record
53
+ cursor = next_cursor
54
+ end
55
+ records.size.should == 5
56
+ Feed::Item.asc(:a_integer).scroll(cursor) do |record, next_cursor|
57
+ records << record
58
+ cursor = next_cursor
59
+ end
60
+ records.size.should == 10
61
+ records.should eq Feed::Item.all.to_a
62
+ end
63
+ it "scrolls in descending order" do
64
+ records = []
65
+ Feed::Item.desc(:a_integer).limit(3).scroll do |record, next_cursor|
66
+ records << record
67
+ end
68
+ records.size.should == 3
69
+ records.should eq Feed::Item.desc(:a_integer).limit(3).to_a
70
+ end
71
+ it "map" do
72
+ record = Feed::Item.desc(:a_integer).limit(3).scroll.map { |record, cursor| record }.last
73
+ cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["a_integer"] })
74
+ cursor.should_not be_nil
75
+ cursor.to_s.split(":").should == [ record.a_integer.to_s, record.id.to_s ]
76
+ end
77
+ end
78
+ end
79
+ context "with overlapping data" do
80
+ before :each do
81
+ 3.times { Feed::Item.create! a_integer: 5 }
82
+ end
83
+ it "scrolls" do
84
+ records = []
85
+ cursor = nil
86
+ Feed::Item.desc(:a_integer).limit(2).scroll do |record, next_cursor|
87
+ records << record
88
+ cursor = next_cursor
89
+ end
90
+ records.size.should == 2
91
+ Feed::Item.desc(:a_integer).scroll(cursor) do |record, next_cursor|
92
+ records << record
93
+ end
94
+ records.size.should == 3
95
+ records.should eq Feed::Item.all.to_a
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::Scroll::Cursor do
4
+ context "an empty cursor" do
5
+ subject do
6
+ Mongoid::Scroll::Cursor.new(nil, { field: Feed::Item.fields["a_string"]})
7
+ end
8
+ its(:tiebreak_id) { should be_nil }
9
+ its(:value) { should be_nil }
10
+ its(:criteria) { should eq({}) }
11
+ end
12
+ context "an invalid cursor" do
13
+ it "raises InvalidCursorError" do
14
+ expect { Mongoid::Scroll::Cursor.new "invalid", field: Feed::Item.fields["a_string"] }.to raise_error Mongoid::Scroll::Errors::InvalidCursorError,
15
+ /The cursor supplied is invalid: invalid./
16
+ end
17
+ end
18
+ context "a string field cursor" do
19
+ let(:feed_item) { Feed::Item.create!(a_string: "astring") }
20
+ subject do
21
+ Mongoid::Scroll::Cursor.new "#{feed_item.a_string}:#{feed_item.id}", field: Feed::Item.fields["a_string"]
22
+ end
23
+ its(:value) { should eq feed_item.a_string }
24
+ its(:tiebreak_id) { should eq feed_item.id }
25
+ its(:criteria) {
26
+ should eq({ "$or" => [
27
+ { "a_string" => { "$gt" => feed_item.a_string }},
28
+ { "a_string" => feed_item.a_string, :_id => { "$gt" => feed_item.id }}
29
+ ]})
30
+ }
31
+ end
32
+ context "an integer field cursor" do
33
+ let(:feed_item) { Feed::Item.create!(a_integer: 10) }
34
+ subject do
35
+ Mongoid::Scroll::Cursor.new "#{feed_item.a_integer}:#{feed_item.id}", field: Feed::Item.fields["a_integer"]
36
+ end
37
+ its(:value) { should eq feed_item.a_integer }
38
+ its(:tiebreak_id) { should eq feed_item.id }
39
+ its(:criteria) {
40
+ should eq({ "$or" => [
41
+ { "a_integer" => { "$gt" => feed_item.a_integer }},
42
+ { "a_integer" => feed_item.a_integer, :_id => { "$gt" => feed_item.id }}
43
+ ]})
44
+ }
45
+ end
46
+ context "a date/time field cursor" do
47
+ let(:feed_item) { Feed::Item.create!(a_datetime: DateTime.new(2013, 12, 21, 1, 42, 3)) }
48
+ subject do
49
+ Mongoid::Scroll::Cursor.new "#{feed_item.a_datetime.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_datetime"]
50
+ end
51
+ its(:value) { should eq feed_item.a_datetime }
52
+ its(:tiebreak_id) { should eq feed_item.id }
53
+ its(:to_s) { should eq "#{feed_item.a_datetime.to_i}:#{feed_item.id}" }
54
+ its(:criteria) {
55
+ should eq({ "$or" => [
56
+ { "a_datetime" => { "$gt" => feed_item.a_datetime }},
57
+ { "a_datetime" => feed_item.a_datetime, :_id => { "$gt" => feed_item.id }}
58
+ ]})
59
+ }
60
+ end
61
+ context "a date field cursor" do
62
+ let(:feed_item) { Feed::Item.create!(a_date: Date.new(2013, 12, 21)) }
63
+ subject do
64
+ Mongoid::Scroll::Cursor.new "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_date"]
65
+ end
66
+ its(:value) { should eq feed_item.a_date }
67
+ its(:tiebreak_id) { should eq feed_item.id }
68
+ its(:to_s) { should eq "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}" }
69
+ its(:criteria) {
70
+ should eq({ "$or" => [
71
+ { "a_date" => { "$gt" => feed_item.a_date.to_datetime }},
72
+ { "a_date" => feed_item.a_date.to_datetime, :_id => { "$gt" => feed_item.id }}
73
+ ]})
74
+ }
75
+ end
76
+ context "a time field cursor" do
77
+ let(:feed_item) { Feed::Item.create!(a_time: Time.new(2013, 12, 21, 1, 2, 3)) }
78
+ subject do
79
+ Mongoid::Scroll::Cursor.new "#{feed_item.a_time.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_time"]
80
+ end
81
+ its(:value) { should eq feed_item.a_time }
82
+ its(:tiebreak_id) { should eq feed_item.id }
83
+ its(:to_s) { should eq "#{feed_item.a_time.to_i}:#{feed_item.id}" }
84
+ its(:criteria) {
85
+ should eq({ "$or" => [
86
+ { "a_time" => { "$gt" => feed_item.a_time }},
87
+ { "a_time" => feed_item.a_time, :_id => { "$gt" => feed_item.id }}
88
+ ]})
89
+ }
90
+ end
91
+ context "an array field cursor" do
92
+ let(:feed_item) { Feed::Item.create!(a_array: [ "x", "y" ]) }
93
+ it "is not supported" do
94
+ expect {
95
+ Mongoid::Scroll::Cursor.from_record(feed_item, field: Feed::Item.fields["a_array"])
96
+ }.to raise_error Mongoid::Scroll::Errors::UnsupportedFieldTypeError, /The type of the field 'a_array' is not supported: Array./
97
+ end
98
+ end
99
+ context "an invalid field cursor" do
100
+ it "raises ArgumentError" do
101
+ expect {
102
+ Mongoid::Scroll::Cursor.new "invalid:whatever", {}
103
+ }.to raise_error ArgumentError
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::Scroll::VERSION do
4
+ it { should_not be_nil }
5
+ end
6
+
@@ -0,0 +1,21 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rubygems'
5
+ require 'rspec'
6
+ require 'mongoid-scroll'
7
+
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each do |f|
9
+ require f
10
+ end
11
+
12
+ Mongoid.configure do |config|
13
+ config.connect_to 'mongoid_scroll_test'
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ config.before :each do
18
+ Mongoid.purge!
19
+ end
20
+ end
21
+
@@ -0,0 +1,13 @@
1
+ module Feed
2
+ class Item
3
+ include Mongoid::Document
4
+
5
+ field :a_field
6
+ field :a_integer, type: Integer
7
+ field :a_string, type: String
8
+ field :a_datetime, type: DateTime
9
+ field :a_date, type: Date
10
+ field :a_time, type: Time
11
+ field :a_array, type: Array
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-scroll
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Doubrovkine
9
+ - Frank Macreery
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-02-14 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mongoid
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '3.0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: i18n
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description:
48
+ email: dblock@dblock.org
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .rspec
55
+ - .travis.yml
56
+ - CHANGELOG.md
57
+ - Gemfile
58
+ - LICENSE.md
59
+ - README.md
60
+ - Rakefile
61
+ - lib/config/locales/en.yml
62
+ - lib/mongoid-scroll.rb
63
+ - lib/mongoid/criterion/scrollable.rb
64
+ - lib/mongoid/scroll/cursor.rb
65
+ - lib/mongoid/scroll/errors.rb
66
+ - lib/mongoid/scroll/errors/base.rb
67
+ - lib/mongoid/scroll/errors/invalid_cursor_error.rb
68
+ - lib/mongoid/scroll/errors/multiple_sort_fields_error.rb
69
+ - lib/mongoid/scroll/errors/no_such_field_error.rb
70
+ - lib/mongoid/scroll/errors/unsupported_field_type_error.rb
71
+ - lib/mongoid/scroll/version.rb
72
+ - lib/mongoid_scroll.rb
73
+ - mongoid-scroll.gemspec
74
+ - spec/mongoid/criteria_spec.rb
75
+ - spec/mongoid/scroll_cursor_spec.rb
76
+ - spec/mongoid/scroll_spec.rb
77
+ - spec/spec_helper.rb
78
+ - spec/support/feed/item.rb
79
+ homepage: http://github.com/dblock/mongoid-scroll
80
+ licenses:
81
+ - MIT
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ segments:
93
+ - 0
94
+ hash: 1370092332583330753
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: 1.3.6
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 1.8.25
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Mongoid extensions to enable infinite scroll.
107
+ test_files: []