mongoid-scroll 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []