Skip to content

[WIP] Add base64 serialization #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 0.3.8 (Next)

* [#28](https://github.com/mongoid/mongoid-scroll/pull/28): Add cursor Base64 serialization - [@FabienChaynes](https://github.com/FabienChaynes).
* [#25](https://github.com/mongoid/mongoid-scroll/pull/25): Compatibility with Ruby 3 - [@leamotta](https://github.com/leamotta).
* [#25](https://github.com/mongoid/mongoid-scroll/pull/25): Replace Travis CI with GHA - [@leamotta](https://github.com/leamotta).
* Your contribution here.
5 changes: 4 additions & 1 deletion lib/config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -19,4 +19,7 @@ en:
message: "Unsupported field type."
summary: "The type of the field '%{field}' is not supported: %{type}."
resolution: "Please open a feature request in https://github.com/mongoid/mongoid-scroll."

invalid_base64:
message: "The Base64 string supplied is invalid."
summary: "The Base64 string supplied is invalid: %{str}."
resolution: "Base64 strings must follow RFC 4648."
6 changes: 3 additions & 3 deletions lib/mongo/scrollable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Mongo
module Scrollable
def scroll(cursor = nil, options = nil, &_block)
def scroll(cursor = nil, options = nil, cursor_class = Mongoid::Scroll::Cursor, &_block)
view = self
# we don't support scrolling over a view with multiple fields
raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: view.sort) if view.sort && view.sort.keys.size != 1
@@ -10,7 +10,7 @@ def scroll(cursor = nil, options = nil, &_block)
# scroll cursor from the parameter, with value and tiebreak_id
options = { field_type: BSON::ObjectId } unless options
cursor_options = { field_name: scroll_field, direction: scroll_direction }.merge(options)
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
cursor = cursor.is_a?(cursor_class) ? cursor : cursor_class.new(cursor, cursor_options)
# make a view
view = Mongo::Collection::View.new(
view.collection,
@@ -22,7 +22,7 @@ def scroll(cursor = nil, options = nil, &_block)
# scroll
if block_given?
view.each do |record|
yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
yield record, cursor_class.from_record(record, cursor_options)
end
else
view
2 changes: 2 additions & 0 deletions lib/mongoid-scroll.rb
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
require 'mongoid/scroll/version'
require 'mongoid/scroll/errors'
require 'mongoid/scroll/cursor'
require 'mongoid/scroll/base64_encoded_cursor'
require 'mongoid/scroll/signed_base64_encoded_cursor'
require 'moped/scrollable' if Object.const_defined?(:Moped)
require 'mongo/scrollable' if Object.const_defined?(:Mongo)
require 'mongoid/criteria/scrollable'
24 changes: 15 additions & 9 deletions lib/mongoid/criteria/scrollable.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
module Mongoid
class Criteria
module Scrollable
def scroll(cursor = nil, &_block)
def scroll(cursor = nil, cursor_class = Mongoid::Scroll::Cursor, &_block)
raise_multiple_sort_fields_error if multiple_sort_fields?
criteria = dup
criteria.merge!(default_sort) if no_sort_option?
cursor_options = build_cursor_options(criteria)
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : new_cursor(cursor, cursor_options)
cursor = if cursor.is_a?(cursor_class)
current_cursor_options = { field_type: cursor.field_type, field_name: cursor.field_name, direction: cursor.direction }
raise Exception.new("Cursor not following the original sort: #{[cursor_options, current_cursor_options]}") if cursor_options != current_cursor_options # TODO: Add custom exception
cursor
else
new_cursor(cursor, cursor_options, cursor_class)
end
cursor_criteria = build_cursor_criteria(criteria, cursor)
if block_given?
cursor_criteria.order_by(_id: scroll_direction(criteria)).each do |record|
yield record, cursor_from_record(record, cursor_options)
yield record, cursor_from_record(record, cursor_options, cursor_class)
end
else
cursor_criteria
@@ -51,8 +57,8 @@ def build_cursor_options(criteria)
}
end

def new_cursor(cursor, cursor_options)
Mongoid::Scroll::Cursor.new(cursor, cursor_options)
def new_cursor(cursor, cursor_options, cursor_class)
cursor_class.new(cursor, cursor_options)
end

def build_cursor_criteria(criteria, cursor)
@@ -61,18 +67,18 @@ def build_cursor_criteria(criteria, cursor)
cursor_criteria
end

def cursor_from_record(record, cursor_options)
Mongoid::Scroll::Cursor.from_record(record, cursor_options)
def cursor_from_record(record, cursor_options, cursor_class)
cursor_class.from_record(record, cursor_options)
end

def scroll_field_type(criteria)
scroll_field = scroll_field(criteria)
field = criteria.klass.fields[scroll_field.to_s]
field.foreign_key? && field.object_id_field? ? bson_type : field.type
field.foreign_key? && field.object_id_field? ? bson_type : field.type.to_s
end

def bson_type
Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId.to_s : BSON::ObjectId.to_s
end
end
end
22 changes: 22 additions & 0 deletions lib/mongoid/scroll/base64_encoded_cursor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'base64'
require 'json'

module Mongoid
module Scroll
# Allows to serialize/deserialize the cursor using RFC 4648
class Base64EncodedCursor < Cursor
def to_s
Base64.strict_encode64({ value: super, field_type: field_type, field_name: field_name, direction: direction }.to_json)
end

class << self
def deserialize(str)
config_hash = ::JSON.parse(::Base64.strict_decode64(str))
new(config_hash['value'], field_type: config_hash['field_type'], field_name: config_hash['field_name'], direction: config_hash['direction'])
rescue ArgumentError
raise Mongoid::Scroll::Errors::InvalidBase64Error.new(str: str)
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mongoid/scroll/cursor.rb
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ def criteria

class << self
def from_record(record, options)
cursor = Mongoid::Scroll::Cursor.new(nil, options)
cursor = new(nil, options)
value = record.respond_to?(cursor.field_name) ? record.send(cursor.field_name) : record[cursor.field_name]
cursor.value = Mongoid::Scroll::Cursor.parse_field_value(cursor.field_type, cursor.field_name, value)
cursor.tiebreak_id = record['_id']
1 change: 1 addition & 0 deletions lib/mongoid/scroll/errors.rb
Original file line number Diff line number Diff line change
@@ -3,3 +3,4 @@
require 'mongoid/scroll/errors/invalid_cursor_error'
require 'mongoid/scroll/errors/no_such_field_error'
require 'mongoid/scroll/errors/unsupported_field_type_error'
require 'mongoid/scroll/errors/invalid_base64_error'
12 changes: 12 additions & 0 deletions lib/mongoid/scroll/errors/invalid_base64_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Mongoid
module Scroll
module Errors
# Raised when a string expected to be encoded in Base64 (following RFC 4648) is invalid
class InvalidBase64Error < Mongoid::Scroll::Errors::Base
def initialize(opts = {})
super(compose_message('invalid_base64', opts))
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/mongoid/scroll/signed_base64_encoded_cursor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'base64'
require 'json'

module Mongoid
module Scroll
# Allows to serialize/deserialize the cursor using RFC 4648 and sign it to avoid tampering
class SignedBase64EncodedCursor < Cursor
def to_s(&_block)
config_hash = { value: super, field_type: field_type, field_name: field_name, direction: direction }
sign = yield(config_hash.to_json)
config_hash[:sign] = sign
Base64.strict_encode64(config_hash.to_json)
end

class << self
def deserialize(str, &_block)
config_hash = ::JSON.parse(::Base64.strict_decode64(str))
sign = config_hash.delete('sign')
raise ::Exception.new('Invalid signature') if sign != yield(config_hash.to_json) # TODO: Add custom exception
new(config_hash['value'], field_type: config_hash['field_type'], field_name: config_hash['field_name'], direction: config_hash['direction'])
rescue ArgumentError
raise Mongoid::Scroll::Errors::InvalidBase64Error.new(str: str)
end
end
end
end
end
6 changes: 3 additions & 3 deletions lib/moped/scrollable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Moped
module Scrollable
def scroll(cursor = nil, options = nil, &_block)
def scroll(cursor = nil, options = nil, cursor_class = Mongoid::Scroll::Cursor, &_block)
unless options
bson_type = Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
options = { field_type: bson_type }
@@ -20,13 +20,13 @@ def scroll(cursor = nil, options = nil, &_block)
scroll_direction = query.operation.selector['$orderby'].values.first.to_i
# scroll cursor from the parameter, with value and tiebreak_id
cursor_options = { field_name: scroll_field, field_type: options[:field_type], direction: scroll_direction }
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
cursor = cursor.is_a?(cursor_class) ? cursor : cursor_class.new(cursor, cursor_options)
query.operation.selector['$query'] = query.operation.selector['$query'].merge(cursor.criteria)
query.operation.selector['$orderby'] = query.operation.selector['$orderby'].merge(_id: scroll_direction)
# scroll
if block_given?
query.each do |record|
yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
yield record, cursor_class.from_record(record, cursor_options)
end
else
query
22 changes: 22 additions & 0 deletions spec/mongoid/scroll_base64_encoded_cursor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'

describe Mongoid::Scroll::Base64EncodedCursor do
context 'an empty cursor' do
subject do
Mongoid::Scroll::Base64EncodedCursor.new nil, field_name: 'a_string', field_type: String
end
its(:tiebreak_id) { should be_nil }
its(:value) { should be_nil }
its(:criteria) { should eq({}) }
describe 'base64' do
let(:base64_string) { 'eyJ2YWx1ZSI6bnVsbCwiZmllbGRfdHlwZSI6IlN0cmluZyIsImZpZWxkX25hbWUiOiJhX3N0cmluZyIsImRpcmVjdGlvbiI6MX0=' }
its(:to_s) { should eq(base64_string) }
it 'is properly decoded' do
cursor = Mongoid::Scroll::Base64EncodedCursor.deserialize(base64_string)
expect(cursor.tiebreak_id).to be_nil
expect(cursor.value).to be_nil
expect(cursor.criteria).to eq({})
end
end
end
end