BSON for JRuby

This commit is contained in:
Kyle Banker 2010-09-30 09:43:17 -04:00
parent 4141331f79
commit 2a7b089a9b
17 changed files with 1810 additions and 30 deletions

View File

@ -13,6 +13,29 @@ require 'rbconfig'
include Config include Config
ENV['TEST_MODE'] = 'TRUE' ENV['TEST_MODE'] = 'TRUE'
task :java do
Rake::Task['build:java'].invoke
Rake::Task['test:ruby'].invoke
end
namespace :build do
desc "Build the java extensions."
task :java do
puts "Building Java extensions..."
java_dir = File.join(File.dirname(__FILE__), 'ext', 'java')
jar_dir = File.join(java_dir, 'jar')
jruby_jar = File.join(jar_dir, 'jruby.jar')
mongo_jar = File.join(jar_dir, 'mongo.jar')
bson_jar = File.join(jar_dir, 'bson.jar')
src_base = File.join(java_dir, 'src')
system("javac -Xlint:unchecked -classpath #{jruby_jar}:#{mongo_jar}:#{bson_jar} #{File.join(src_base, 'org', 'jbson', '*.java')}")
system("cd #{src_base} && jar cf #{File.join(jar_dir, 'jbson.jar')} #{File.join('.', 'org', 'jbson', '*.class')}")
end
end
desc "Test the MongoDB Ruby driver." desc "Test the MongoDB Ruby driver."
task :test do task :test do
puts "\nThis option has changed." puts "\nThis option has changed."

View File

@ -14,6 +14,7 @@ Gem::Specification.new do |s|
s.files = ['Rakefile', 'bson.gemspec', 'LICENSE.txt'] s.files = ['Rakefile', 'bson.gemspec', 'LICENSE.txt']
s.files += ['lib/bson.rb'] + Dir['lib/bson/**/*.rb'] s.files += ['lib/bson.rb'] + Dir['lib/bson/**/*.rb']
s.files += ['bin/b2json', 'bin/j2bson'] s.files += ['bin/b2json', 'bin/j2bson']
s.files += Dir['ext/java/jar/**/*.jar']
s.test_files = Dir['test/mongo_bson/*.rb'] s.test_files = Dir['test/mongo_bson/*.rb']
s.executables = ['b2json', 'j2bson'] s.executables = ['b2json', 'j2bson']

Binary file not shown.

View File

@ -0,0 +1,382 @@
// BSON Callback
// RubyBSONCallback.java
package org.jbson;
import org.jruby.*;
import org.jruby.util.ByteList;
import org.jruby.RubyString;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallType;
import org.jruby.runtime.callsite.CacheEntry;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.javasupport.JavaUtil;
import org.jruby.parser.ReOptions;
import org.jruby.RubyArray;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import org.bson.*;
import org.bson.types.*;
public class RubyBSONCallback implements BSONCallback {
private RubyHash _root;
private RubyModule _rbclsOrderedHash;
private RubyModule _rbclsObjectId;
private RubyModule _rbclsBinary;
private RubyModule _rbclsMinKey;
private RubyModule _rbclsMaxKey;
private RubyModule _rbclsDBRef;
private RubyModule _rbclsCode;
private final LinkedList<RubyObject> _stack = new LinkedList<RubyObject>();
private final LinkedList<String> _nameStack = new LinkedList<String>();
private Ruby _runtime;
static final HashMap<Ruby, HashMap> _runtimeCache = new HashMap<Ruby, HashMap>();
public RubyBSONCallback(Ruby runtime) {
_runtime = runtime;
_rbclsOrderedHash = _lookupConstant( _runtime, "BSON::OrderedHash" );
_rbclsBinary = _lookupConstant( _runtime, "BSON::Binary" );
_rbclsDBRef = _lookupConstant( _runtime, "BSON::DBRef" );
_rbclsCode = _lookupConstant( _runtime, "BSON::Code" );
_rbclsMinKey = _lookupConstant( _runtime, "BSON::MinKey" );
_rbclsMaxKey = _lookupConstant( _runtime, "BSON::MaxKey" );
_rbclsObjectId = _lookupConstant( _runtime, "BSON::ObjectId");
}
public BSONCallback createBSONCallback(){
return new RubyBSONCallback(_runtime);
}
public void reset(){
_root = null;
_stack.clear();
_nameStack.clear();
}
public RubyHash createHash() {
RubyHash h = (RubyHash)JavaEmbedUtils.invokeMethod(_runtime, _rbclsOrderedHash, "new",
new Object[] { }, Object.class);
return h;
}
public RubyArray createArray() {
return RubyArray.newArray(_runtime);
}
public RubyObject create( boolean array , List<String> path ){
if ( array )
return createArray();
return createHash();
}
public void objectStart(){
if ( _stack.size() > 0 ) {
throw new IllegalStateException( "something is wrong" );
}
_root = createHash();
_stack.add(_root);
}
public void objectStart(boolean f) {
objectStart();
}
public void objectStart(String key){
RubyHash hash = createHash();
_nameStack.addLast( key );
RubyObject lastObject = _stack.getLast();
// Yes, this is a bit hacky.
if(lastObject instanceof RubyHash) {
writeRubyHash(key, (RubyHash)lastObject, (IRubyObject)hash);
}
else {
writeRubyArray(key, (RubyArray)lastObject, (IRubyObject)hash);
}
_stack.addLast( (RubyObject)hash );
}
public void writeRubyHash(String key, RubyHash hash, IRubyObject obj) {
RubyString rkey = _runtime.newString(key);
JavaEmbedUtils.invokeMethod(_runtime, hash, "[]=",
new Object[] { (IRubyObject)rkey, obj }, Object.class);
}
public void writeRubyArray(String key, RubyArray array, IRubyObject obj) {
Long rkey = Long.parseLong(key);
RubyFixnum index = new RubyFixnum(_runtime, rkey);
array.aset((IRubyObject)index, obj);
}
public void arrayStart(String key){
RubyArray array = createArray();
RubyObject lastObject = _stack.getLast();
_nameStack.addLast( key );
if(lastObject instanceof RubyHash) {
writeRubyHash(key, (RubyHash)lastObject, (IRubyObject)array);
}
else {
writeRubyArray(key, (RubyArray)lastObject, (IRubyObject)array);
}
_stack.addLast( (RubyObject)array );
}
public RubyObject objectDone(){
RubyObject o =_stack.removeLast();
if ( _nameStack.size() > 0 )
_nameStack.removeLast();
else if ( _stack.size() > 0 ) {
throw new IllegalStateException( "something is wrong" );
}
return o;
}
// Not used by Ruby decoder
public void arrayStart(){
}
public RubyObject arrayDone(){
return objectDone();
}
public void gotNull( String name ){
_put(name, (RubyObject)_runtime.getNil());
}
// Undefined should be represented as a lack of key / value.
public void gotUndefined( String name ){
}
// TODO: Handle this
public void gotUUID( String name , long part1, long part2) {
//_put( name , new UUID(part1, part2) );
}
public void gotCode( String name , String code ){
RubyString code_string = _runtime.newString( code );
Object rb_code_obj = JavaEmbedUtils.invokeMethod(_runtime, _rbclsCode,
"new", new Object[] { code_string }, Object.class);
_put( name , (RubyObject)rb_code_obj );
}
public void gotCodeWScope( String name , String code , Object scope ){
RubyString code_string = _runtime.newString( code );
Object rb_code_obj = JavaEmbedUtils.invokeMethod(_runtime, _rbclsCode,
"new", new Object[] { code_string, (RubyHash)scope }, Object.class);
_put( name , (RubyObject)rb_code_obj );
}
public void gotMinKey( String name ){
Object minkey = JavaEmbedUtils.invokeMethod(_runtime, _rbclsMinKey, "new", new Object[] {}, Object.class);
_put( name, (RubyObject)minkey);
}
public void gotMaxKey( String name ){
Object maxkey = JavaEmbedUtils.invokeMethod(_runtime, _rbclsMaxKey, "new", new Object[] {}, Object.class);
_put( name, (RubyObject)maxkey);
}
public void gotBoolean( String name , boolean v ){
RubyBoolean b = RubyBoolean.newBoolean( _runtime, v );
_put(name , b);
}
public void gotDouble( String name , double v ){
RubyFloat f = new RubyFloat( _runtime, v );
_put(name , (RubyObject)f);
}
public void gotInt( String name , int v ){
RubyFixnum f = new RubyFixnum( _runtime, v );
_put(name , (RubyObject)f);
}
public void gotLong( String name , long v ){
RubyFixnum f = new RubyFixnum( _runtime, v );
_put(name , (RubyObject)f);
}
public void gotDate( String name , long millis ){
RubyTime time = RubyTime.newTime(_runtime, millis).gmtime();
_put( name , time );
}
public void gotRegex( String name , String pattern , String flags ){
int f = 0;
ByteList b = new ByteList(pattern.getBytes());
if(flags.contains("i")) {
f = f | ReOptions.RE_OPTION_IGNORECASE;
}
if(flags.contains("m")) {
f = f | ReOptions.RE_OPTION_MULTILINE;
}
if(flags.contains("x")) {
f = f | ReOptions.RE_OPTION_EXTENDED;
}
_put( name , RubyRegexp.newRegexp(_runtime, b, f) );
}
public void gotString( String name , String v ){
RubyString str = RubyString.newString(_runtime, v);
_put( name , str );
}
public void gotSymbol( String name , String v ){
ByteList bytes = new ByteList(v.getBytes());
RubySymbol symbol = _runtime.getSymbolTable().getSymbol(bytes);
_put( name , symbol );
}
// Timestamp is currently rendered in Ruby as a two-element array.
public void gotTimestamp( String name , int time , int inc ){
RubyFixnum rtime = RubyFixnum.newFixnum( _runtime, time );
RubyFixnum rinc = RubyFixnum.newFixnum( _runtime, inc );
RubyObject[] args = new RubyObject[2];
args[0] = rinc;
args[1] = rtime;
RubyArray result = RubyArray.newArray( _runtime, args );
_put ( name , result );
}
public void gotObjectId( String name , ObjectId id ){
IRubyObject arg = (IRubyObject)RubyString.newString(_runtime, id.toString());
Object[] args = new Object[] { arg };
Object result = JavaEmbedUtils.invokeMethod(_runtime, _rbclsObjectId, "from_string", args, Object.class);
_put( name, (RubyObject)result );
}
// TODO: Incredibly annoying to deserialize to a Ruby DBRef. Might just
// stop supporting this altogether in the driver.
public void gotDBRef( String name , String ns , ObjectId id ){
// _put( name , new BasicBSONObject( "$ns" , ns ).append( "$id" , id ) );
}
// TODO: I know that this is horrible. To be optimized.
private RubyArray ja2ra( byte[] b ) {
RubyArray result = RubyArray.newArray( _runtime, b.length );
for ( int i=0; i<b.length; i++ ) {
result.aset( RubyNumeric.dbl2num( _runtime, (double)i ), RubyNumeric.dbl2num( _runtime, (double)b[i] ) );
}
return result;
}
public void gotBinaryArray( String name , byte[] b ) {
RubyArray a = ja2ra( b );
Object[] args = new Object[] { a, 2 };
Object result = JavaEmbedUtils.invokeMethod(_runtime, _rbclsBinary, "new", args, Object.class);
_put( name, (RubyObject)result );
}
// TODO: fix abs stuff here. some kind of bad type issue
public void gotBinary( String name , byte type , byte[] data ){
RubyArray a = ja2ra( data );
Object[] args = new Object[] { a, RubyFixnum.newFixnum(_runtime, Math.abs( type )) };
Object result = JavaEmbedUtils.invokeMethod(_runtime, _rbclsBinary, "new", args, Object.class);
_put( name, (RubyObject)result );
}
protected void _put( String name , RubyObject o ){
RubyObject current = cur();
if(current instanceof RubyArray) {
RubyArray a = (RubyArray)current;
Long n = Long.parseLong(name);
RubyFixnum index = new RubyFixnum(_runtime, n);
a.aset((IRubyObject)index, (IRubyObject)o);
}
else {
RubyString rkey = RubyString.newString(_runtime, name);
JavaEmbedUtils.invokeMethod(_runtime, current, "[]=",
new Object[] { (IRubyObject)rkey, o }, Object.class);
}
}
protected RubyObject cur(){
return _stack.getLast();
}
public Object get(){
return _root;
}
protected void setRoot(RubyHash o) {
_root = o;
}
protected boolean isStackEmpty() {
return _stack.size() < 1;
}
// Helper method for checking whether a Ruby hash has a certain key.
private boolean _rbHashHasKey(RubyHash hash, String key) {
RubyBoolean b = hash.has_key_p( _runtime.newString( key ) );
return b == _runtime.getTrue();
}
// Helper method for getting a value from a Ruby hash.
private IRubyObject _rbHashGet(RubyHash hash, Object key) {
if (key instanceof String) {
return hash.op_aref( _runtime.getCurrentContext(), _runtime.newString((String)key) );
}
else {
return hash.op_aref( _runtime.getCurrentContext(), (RubyObject)key );
}
}
static final HashMap<String, Object> _getRuntimeCache(Ruby runtime) {
// each JRuby runtime may have different objects for these constants,
// so cache them separately for each runtime
@SuppressWarnings("unchecked") // aargh! Java!
HashMap<String, Object> cache = _runtimeCache.get( runtime );
if(cache == null) {
cache = new HashMap<String, Object>();
_runtimeCache.put( runtime, cache );
}
return cache;
}
static final RubyModule _lookupConstant(Ruby runtime, String name)
{
HashMap<String, Object> cache = _getRuntimeCache( runtime );
RubyModule module = (RubyModule) cache.get( name );
if(module == null && !cache.containsKey( name )) {
module = runtime.getClassFromPath( name );
cache.put( (String)name, (Object)module );
}
return module;
}
}

Binary file not shown.

View File

@ -0,0 +1,27 @@
// RubyBSONDecoder.java
package org.jbson;
import static org.bson.BSON.*;
import java.io.*;
import org.jruby.*;
import org.bson.*;
import org.bson.io.*;
import org.bson.types.*;
public class RubyBSONDecoder extends BSONDecoder {
// public int decode( RubyString s , BSONCallback callback ){
// byte[] b = s.getBytes();
// try {
// return decode( new Input( new ByteArrayInputStream(b) ) , callback );
// }
// catch ( IOException ioe ){
// throw new RuntimeException( "should be impossible" , ioe );
// }
// }
}

Binary file not shown.

View File

@ -0,0 +1,764 @@
// BSONEncoder.java
package org.jbson;
import static org.bson.BSON.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.regex.*;
import java.io.*;
import org.jruby.util.Pack;
import java.math.BigInteger;
import org.bson.BSONEncoder;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.javasupport.JavaUtil;
import org.jruby.*;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.parser.ReOptions;
import org.jcodings.Encoding;
import org.bson.BSONObject;
import org.bson.io.*;
import org.bson.types.*;
import org.bson.BSON;
import org.jruby.exceptions.RaiseException;
import org.jruby.java.addons.ArrayJavaAddons;
/**
* this is meant to be pooled or cached
* there is some per instance memory for string conversion, etc...
*/
@SuppressWarnings("unchecked")
public class RubyBSONEncoder extends BSONEncoder {
static final boolean DEBUG = false;
static final Map _runtimeCache = new HashMap();
private Ruby _runtime;
private RubyModule _rbclsByteBuffer;
private RubyModule _rbclsDBRef;
private RubyModule _rbclsInvalidDocument;
private RubyModule _rbclsInvalidKeyName;
private RubyModule _rbclsRangeError;
private RubySymbol _idAsSym;
private RubyString _idAsString;
private RubyString _tfAsString;
private static final int BIT_SIZE = 64;
private static final long MAX = (1L << (BIT_SIZE - 1)) - 1;
private static final BigInteger LONG_MAX = BigInteger.valueOf(MAX);
private static final BigInteger LONG_MIN = BigInteger.valueOf(-MAX - 1);
public RubyBSONEncoder(Ruby runtime){
_runtime = runtime;
_rbclsByteBuffer = _lookupConstant( _runtime, "BSON::ByteBuffer" );
_rbclsDBRef = _lookupConstant( _runtime, "BSON::DBRef" );
_rbclsInvalidDocument = _lookupConstant( _runtime, "BSON::InvalidDocument" );
_rbclsInvalidKeyName = _lookupConstant( _runtime, "BSON::InvalidKeyName" );
_rbclsRangeError = _lookupConstant( _runtime, "RangeError" );
_idAsSym = _lookupSymbol( _runtime, "_id" );
_tfAsString = _lookupString( _runtime, "_transientFields" );
if(_idAsString == null) {
_idAsString = _runtime.newString( "_id" );
}
}
public RubyString encode( Object arg ) {
RubyHash o = (RubyHash)arg;
BasicOutputBuffer buf = new BasicOutputBuffer();
set( buf );
putObject( o );
done();
RubyString b = RubyString.newString(_runtime, buf.toByteArray());
return b;
}
public void set( OutputBuffer out ){
if ( _buf != null ) {
done();
throw new IllegalStateException( "in the middle of something" );
}
_buf = out;
}
public void done(){
_buf = null;
}
/**
* @return true if object was handled
*/
protected boolean handleSpecialObjects( String name , RubyObject o ){
return false;
}
/** Encodes a <code>BSONObject</code>.
* This is for the higher level api calls
* @param o the object to encode
* @return the number of characters in the encoding
*/
public int putObject( RubyObject o ) {
return putObject( null, o );
}
/**
* this is really for embedded objects
*/
int putObject( String name , RubyObject o ){
if ( o == null )
throw new NullPointerException( "can't save a null object" );
final int start = _buf.getPosition();
byte myType = OBJECT;
if ( o instanceof RubyArray )
myType = ARRAY;
if ( handleSpecialObjects( name , o ) )
return _buf.getPosition() - start;
if ( name != null ){
_put( myType , name );
}
final int sizePos = _buf.getPosition();
_buf.writeInt( 0 ); // leaving space for sthis. set it at the end
List transientFields = null;
boolean rewriteID = ( myType == OBJECT && name == null );
if ( myType == OBJECT ) {
if ( rewriteID ) {
if ( _rbHashHasKey( (RubyHash)o, "_id" ) ) {
_putObjectField( "_id" , _rbHashGet( (RubyHash)o, _idAsString ) );
}
else if ( ( _rbHashHasKey( (RubyHash)o, _idAsSym )) ) {
_putObjectField( "_id" , _rbHashGet( (RubyHash)o, _idAsSym ) );
}
RubyObject temp = (RubyObject)_rbHashGet( (RubyHash)o, _tfAsString );
if ( temp instanceof RubyArray )
transientFields = (RubyArray)temp;
}
// Not sure we should invoke this way. Depends on if we can access the OrderedHash.
RubyArray keys = (RubyArray)JavaEmbedUtils.invokeMethod( _runtime, o , "keys" , new Object[] {}
, Object.class);
for (Iterator<RubyObject> i = keys.iterator(); i.hasNext(); ) {
Object hashKey = i.next();
// Convert the key into a Java String
String str = "";
if( hashKey instanceof String) {
str = hashKey.toString();
}
else if (hashKey instanceof RubyString) {
str = ((RubyString)hashKey).asJavaString();
}
else if (hashKey instanceof RubySymbol) {
str = ((RubySymbol)hashKey).asJavaString();
}
testNull(str);
// If we're rewriting the _id, we can move on.
if ( rewriteID && str.equals( "_id" ) )
continue;
RubyObject val = (RubyObject)_rbHashGet( (RubyHash)o, hashKey );
_putObjectField( str , (Object)val );
}
}
// Make sure we're within the 4MB limit
if ( _buf.size() > 4 * 1024 * 1024 ) {
_rbRaise( (RubyClass)_rbclsInvalidDocument,
"Document is too large (" + _buf.size() + "). BSON documents are limited to 4MB (" +
4 * 1024 * 1024 + ").");
}
_buf.write( EOO );
_buf.writeInt( sizePos , _buf.getPosition() - sizePos );
return _buf.getPosition() - start;
}
protected void _putObjectField( String name , Object val ){
if ( name.equals( "_transientFields" ) )
return;
if ( DEBUG ) {
System.out.println( "\t put thing : " + name );
System.out.println( "\t class : " + val.getClass().getName() );
}
if ( name.equals( "$where") && val instanceof String ){
_put( CODE , name );
_putValueString( val.toString() );
return;
}
if ( val instanceof String )
putString(name, val.toString() );
// TODO: Clean up
else if ( val instanceof Number ) {
if ( val instanceof Double ) {
putNumber(name, (Number)val);
}
else {
long jval = ((Number)val).longValue();
if (jval > Integer.MIN_VALUE && jval < Integer.MAX_VALUE) {
putNumber(name, (int)jval );
}
else
putNumber(name, (Number)jval );
}
}
else if ( val instanceof Boolean )
putBoolean(name, (Boolean)val);
else if ( val instanceof Map )
putMap( name , (Map)val );
else if ( val instanceof Iterable)
putIterable( name , (Iterable)val );
else if ( val instanceof byte[] )
putBinary( name , (byte[])val );
else if ( val.getClass().isArray() )
putIterable( name , Arrays.asList( (Object[])val ) );
else if ( val instanceof RubyObject ) {
if ( val instanceof RubyString ) {
putRubyString(name, ((RubyString)val).getUnicodeValue() );
}
else if (val instanceof RubySymbol) {
putSymbol(name, new Symbol(val.toString()));
}
// TODO: Clean up
else if ( val instanceof RubyFixnum ) {
long jval = ((RubyFixnum)val).getLongValue();
if (jval >= Integer.MIN_VALUE && jval <= Integer.MAX_VALUE) {
putNumber(name, (int)jval );
}
else
putNumber(name, (Number)jval );
}
// TODO: Clean up
else if ( val instanceof RubyFloat ) {
double jval = ((RubyFloat)val).getValue();
putNumber(name, (Number)jval );
}
else if ( val instanceof RubyNil )
putNull(name);
else if ( val instanceof RubyTime ) {
putDate( name , ((RubyTime)val).getDateTime().getMillis() );
}
else if ( val instanceof RubyBoolean )
putBoolean(name, (Boolean)((RubyBoolean)val).toJava(Boolean.class));
else if ( val instanceof RubyRegexp )
putRubyRegexp(name, (RubyRegexp)val );
else if (val instanceof RubyBignum) {
BigInteger big = ((RubyBignum)val).getValue();
if( big.compareTo(LONG_MAX) > 0 || big.compareTo(LONG_MIN) < 0 ) {
_rbRaise( (RubyClass)_rbclsRangeError , "MongoDB can only handle 8-byte ints" );
}
else {
long jval = big.longValue();
putNumber(name, (Number)jval );
}
}
// This is where we handle special types defined in the Ruby BSON.
else {
String klass = JavaEmbedUtils.invokeMethod(_runtime, val,
"class", new Object[] {}, Object.class).toString();
if( klass.equals( "BSON::ObjectId" ) ) {
putRubyObjectId(name, (RubyObject)val );
}
else if( klass.equals( "BSON::ObjectID" ) ) {
putRubyObjectId(name, (RubyObject)val );
}
else if ( klass.equals( "BSON::Code" ) ) {
putRubyCodeWScope(name, (RubyObject)val );
}
else if ( klass.equals( "BSON::Binary" ) ) {
putRubyBinary( name , (RubyObject)val );
}
else if ( klass.equals("BSON::MinKey") ) {
_put( MINKEY, name );
}
else if ( klass.equals("BSON::MaxKey") ) {
_put( MAXKEY, name );
}
else if ( klass.equals("BSON::DBRef") ) {
RubyHash ref = (RubyHash)JavaEmbedUtils.invokeMethod(_runtime, val,
"to_hash", new Object[] {}, Object.class);
putMap( name , (Map)ref );
}
else if ( klass.equals("Date") || klass.equals("DateTime") ||
klass.equals("ActiveSupport::TimeWithZone") ) {
_rbRaise( (RubyClass)_rbclsInvalidDocument,
klass + " is not currently supported; use a UTC Time instance instead.");
}
else {
_rbRaise( (RubyClass)_rbclsInvalidDocument,
"Cannot serialize " + klass + " as a BSON type; " +
"it either isn't supported or won't translate to BSON.");
}
}
}
else {
String klass = JavaEmbedUtils.invokeMethod(_runtime, val,
"class", new Object[] {}, Object.class).toString();
_rbRaise( (RubyClass)_rbclsInvalidDocument,
"Cannot serialize " + klass + " as a BSON type; " +
"it either isn't supported or won't translate to BSON.");
}
}
private void testNull(String str) {
byte[] bytes = str.getBytes();
for(int j = 0; j < bytes.length; j++ ) {
if(bytes[j] == '\u0000') {
_rbRaise( (RubyClass)_rbclsInvalidDocument, "Null not allowed");
}
}
}
private void putIterable( String name , Iterable l ){
_put( ARRAY , name );
final int sizePos = _buf.getPosition();
_buf.writeInt( 0 );
int i=0;
for ( Object obj: l ) {
_putObjectField( String.valueOf( i ) , obj );
i++;
}
_buf.write( EOO );
_buf.writeInt( sizePos , _buf.getPosition() - sizePos );
}
private void putMap( String name , Map m ){
_put( OBJECT , name );
final int sizePos = _buf.getPosition();
_buf.writeInt( 0 );
for ( Map.Entry entry : (Set<Map.Entry>)m.entrySet() )
_putObjectField( entry.getKey().toString() , entry.getValue() );
_buf.write( EOO );
_buf.writeInt( sizePos , _buf.getPosition() - sizePos );
}
protected void putNull( String name ){
_put( NULL , name );
}
protected void putUndefined(String name){
_put(UNDEFINED, name);
}
protected void putTimestamp(String name, BSONTimestamp ts ){
_put( TIMESTAMP , name );
_buf.writeInt( ts.getInc() );
_buf.writeInt( ts.getTime() );
}
protected void putRubyCodeWScope( String name , RubyObject code ){
_put( CODE_W_SCOPE , name );
int temp = _buf.getPosition();
_buf.writeInt( 0 );
String code_string = (String)JavaEmbedUtils.invokeMethod(_runtime, code,
"code", new Object[] {}, Object.class);
_putValueString( code_string );
putObject( (RubyObject)JavaEmbedUtils.invokeMethod(_runtime, code, "scope", new Object[] {}, Object.class) );
_buf.writeInt( temp , _buf.getPosition() - temp );
}
protected void putCodeWScope( String name , CodeWScope code ){
_put( CODE_W_SCOPE , name );
int temp = _buf.getPosition();
_buf.writeInt( 0 );
_putValueString( code.getCode() );
_buf.writeInt( temp , _buf.getPosition() - temp );
}
protected void putBoolean( String name , Boolean b ){
_put( BOOLEAN , name );
_buf.write( b ? (byte)0x1 : (byte)0x0 );
}
protected void putDate( String name , long millis ){
_put( DATE , name );
_buf.writeLong( millis );
}
protected void putNumber( String name , Number n ){
if ( n instanceof Integer ||
n instanceof Short ||
n instanceof Byte ||
n instanceof AtomicInteger ){
_put( NUMBER_INT , name );
_buf.writeInt( n.intValue() );
}
else if ( n instanceof Long ||
n instanceof AtomicLong ) {
_put( NUMBER_LONG , name );
_buf.writeLong( n.longValue() );
}
else {
_put( NUMBER , name );
_buf.writeDouble( n.doubleValue() );
}
}
private void putRubyBinary( String name , RubyObject binary ) {
RubyArray rarray = (RubyArray)JavaEmbedUtils.invokeMethod(_runtime,
binary, "to_a", new Object[] {}, Object.class);
Long rbSubtype = (Long)JavaEmbedUtils.invokeMethod(_runtime,
binary, "subtype", new Object[] {}, Object.class);
long subtype = rbSubtype.longValue();
byte[] data = ra2ba( rarray );
if ( subtype == 2 ) {
putBinary( name, data );
}
else {
_put( BINARY , name );
_buf.writeInt( data.length );
_buf.write( (byte)subtype );
_buf.write( data );
}
}
protected void putBinary( String name , byte[] data ){
_put( BINARY , name );
_buf.writeInt( 4 + data.length );
_buf.write( B_BINARY );
_buf.writeInt( data.length );
int before = _buf.getPosition();
_buf.write( data );
int after = _buf.getPosition();
com.mongodb.util.MyAsserts.assertEquals( after - before , data.length );
}
protected void putBinary( String name , Binary val ){
_put( BINARY , name );
_buf.writeInt( val.length() );
_buf.write( val.getType() );
_buf.write( val.getData() );
}
protected void putUUID( String name , UUID val ){
_put( BINARY , name );
_buf.writeInt( 4 + 64*2);
_buf.write( 3 );// B_UUID );
_buf.writeLong( val.getMostSignificantBits());
_buf.writeLong( val.getLeastSignificantBits());
}
protected void putSymbol( String name , Symbol s ){
_putString(name, s.getSymbol(), SYMBOL);
}
protected void putRubyString( String name , String s ) {
_put( STRING , name );
_putValueString( s );
}
protected void putString(String name, String s) {
_putString(name, s, STRING);
}
private void _putString( String name , String s, byte type ){
_put( type , name );
_putValueString( s );
}
private void putRubyObjectId( String name, RubyObject oid ) {
_put( OID , name );
RubyArray roid = (RubyArray)JavaEmbedUtils.invokeMethod(_runtime, oid,
"to_a", new Object[] {}, Object.class);
byte[] joid = ra2ba( (RubyArray)roid );
_buf.writeInt( convertToInt(joid, 0) );
_buf.writeInt( convertToInt(joid, 4) );
_buf.writeInt( convertToInt(joid, 8) );
}
private void putRubyRegexp( String name, RubyRegexp r ) {
RubyString source = (RubyString)r.source();
testNull(source.toString());
_put( REGEX , name );
_put( (String)((RubyString)source).toJava(String.class) );
int regexOptions = (int)((RubyFixnum)r.options()).getLongValue();
String options = "";
if( (regexOptions & ReOptions.RE_OPTION_IGNORECASE) != 0 )
options = options.concat( "i" );
if( (regexOptions & ReOptions.RE_OPTION_MULTILINE) != 0 )
options = options.concat( "m" );
if( (regexOptions & ReOptions.RE_OPTION_EXTENDED) != 0 )
options = options.concat( "x" );
_put( options );
}
// ---------------------------------------------
// Ruby-based helper methods.
// Converts four bytes from a byte array to an int
private int convertToInt(byte[] b, int offset) {
int intVal = ((b[offset + 3] & 0xff) << 24) | ((b[offset + 2] & 0xff) << 16) | ((b[offset + 1] & 0xff) << 8) | ((b[offset] & 0xff));
return intVal;
}
// Ruby array to byte array
private byte[] ra2ba( RubyArray rArray ) {
int len = rArray.getLength();
byte[] b = new byte[len];
int n = 0;
for ( Iterator<Object> i = rArray.iterator(); i.hasNext(); ) {
Object value = i.next();
b[n] = (byte)((Long)value).intValue();
n++;
}
return b;
}
// Helper method for getting a value from a Ruby hash.
private IRubyObject _rbHashGet(RubyHash hash, Object key) {
if (key instanceof String) {
return hash.op_aref( _runtime.getCurrentContext(), _runtime.newString((String)key) );
}
else {
return hash.op_aref( _runtime.getCurrentContext(), (RubyObject)key );
}
}
// Helper method for checking whether a Ruby hash has a certain key.
private boolean _rbHashHasKey(RubyHash hash, String key) {
RubyBoolean b = hash.has_key_p( _runtime.newString( key ) );
return b == _runtime.getTrue();
}
private boolean _rbHashHasKey(RubyHash hash, RubySymbol sym) {
RubyBoolean b = hash.has_key_p( sym );
return b == _runtime.getTrue();
}
// Helper method for setting a value in a Ruby hash.
private IRubyObject _rbHashSet(RubyHash hash, String key, IRubyObject value) {
return hash.op_aset( _runtime.getCurrentContext(), _runtime.newString( key ), value );
}
// Helper method for returning all keys from a Ruby hash.
private RubyArray _rbHashKeys(RubyHash hash) {
return hash.keys();
}
// Helper for raising a Ruby exception and aborting serialization.
private RaiseException _rbRaise( RubyClass exceptionClass , String message ) {
done();
throw new RaiseException( _runtime, exceptionClass, message, true );
}
// ----------------------------------------------
/**
* Encodes the type and key.
*
*/
protected void _put( byte type , String name ){
_buf.write( type );
_put( name );
}
/**
* Encodes the type and key without checking the validity of the key.
*
*/
protected void _putWithoutCheck( byte type , String name ){
_buf.write( type );
_put( name );
}
protected void _putValueString( String s ){
int lenPos = _buf.getPosition();
_buf.writeInt( 0 ); // making space for size
int strLen = _put( s );
_buf.writeInt( lenPos , strLen );
}
void _reset( Buffer b ){
b.position(0);
b.limit( b.capacity() );
}
/**
* puts as utf-8 string
*/
protected int _put( String str ){
int total = 0;
final int len = str.length();
int pos = 0;
while ( pos < len ){
_reset( _stringC );
_reset( _stringB );
int toEncode = Math.min( _stringC.capacity() - 1, len - pos );
_stringC.put( str , pos , pos + toEncode );
_stringC.flip();
CoderResult cr = _encoder.encode( _stringC , _stringB , false );
if ( cr.isMalformed() || cr.isUnmappable() )
throw new IllegalArgumentException( "malforumed string" );
if ( cr.isOverflow() )
throw new RuntimeException( "overflor should be impossible" );
if ( cr.isError() )
throw new RuntimeException( "should never get here" );
if ( ! cr.isUnderflow() )
throw new RuntimeException( "this should always be true" );
total += _stringB.position();
_buf.write( _stringB.array() , 0 , _stringB.position() );
pos += toEncode;
}
_buf.write( (byte)0 );
total++;
return total;
}
public void writeInt( int x ){
_buf.writeInt( x );
}
public void writeLong( long x ){
_buf.writeLong( x );
}
public void writeCString( String s ){
_put( s );
}
protected OutputBuffer _buf;
private CharBuffer _stringC = CharBuffer.wrap( new char[256 + 1] );
private ByteBuffer _stringB = ByteBuffer.wrap( new byte[1024 + 1] );
private CharsetEncoder _encoder = Charset.forName( "UTF-8" ).newEncoder();
static final Map _getRuntimeCache(Ruby runtime) {
// each JRuby runtime may have different objects for these constants,
// so cache them separately for each runtime
Map cache = (Map) _runtimeCache.get( runtime );
if(cache == null) {
cache = new HashMap();
_runtimeCache.put( runtime, cache );
}
return cache;
}
static final RubyModule _lookupConstant(Ruby runtime, String name)
{
Map cache = (Map) _getRuntimeCache( runtime );
RubyModule module = (RubyModule) cache.get( name );
if(module == null && !cache.containsKey( name )) {
module = runtime.getClassFromPath( name );
cache.put( name, module );
}
return module;
}
static final RubySymbol _lookupSymbol(Ruby runtime, String name)
{
Map cache = (Map) _getRuntimeCache( runtime );
RubySymbol symbol = (RubySymbol) cache.get( name );
if(symbol == null && !cache.containsKey( name )) {
symbol = runtime.newSymbol( name );
cache.put( name, symbol );
}
return symbol;
}
static final RubyString _lookupString(Ruby runtime, String name)
{
Map cache = (Map) _getRuntimeCache( runtime );
RubyString string = (RubyString) cache.get( name );
if(string == null && !cache.containsKey( name )) {
string = runtime.newString( name );
cache.put( name, string );
}
return string;
}
}

View File

@ -32,28 +32,39 @@ module BSON
end end
end end
begin if RUBY_PLATFORM =~ /java/
# Need this for running test with and without c ext in Ruby 1.9. jar_dir = File.join(File.dirname(__FILE__), '..', 'ext', 'java', 'jar')
raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT'] require File.join(jar_dir, 'mongo.jar')
require 'bson_ext/cbson' require File.join(jar_dir, 'bson.jar')
raise LoadError unless defined?(CBson::VERSION) require File.join(jar_dir, 'jbson.jar')
if CBson::VERSION < MINIMUM_BSON_EXT_VERSION require 'bson/bson_java'
puts "Able to load bson_ext version #{CBson::VERSION}, but >= #{MINIMUM_BSON_EXT_VERSION} is required."
raise LoadError
end
require 'bson/bson_c'
module BSON module BSON
BSON_CODER = BSON_C BSON_CODER = BSON_JAVA
end end
rescue LoadError else
require 'bson/bson_ruby' begin
module BSON # Need this for running test with and without c ext in Ruby 1.9.
BSON_CODER = BSON_RUBY raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT']
require 'bson_ext/cbson'
raise LoadError unless defined?(CBson::VERSION)
if CBson::VERSION < MINIMUM_BSON_EXT_VERSION
puts "Able to load bson_ext version #{CBson::VERSION}, but >= #{MINIMUM_BSON_EXT_VERSION} is required."
raise LoadError
end
require 'bson/bson_c'
module BSON
BSON_CODER = BSON_C
end
rescue LoadError
require 'bson/bson_ruby'
module BSON
BSON_CODER = BSON_RUBY
end
warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance."
warn " You can install the extension as follows:\n gem install bson_ext\n"
warn " If you continue to receive this message after installing, make sure that the"
warn " bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.\n"
end end
warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance."
warn " You can install the extension as follows:\n gem install bson_ext\n"
warn " If you continue to receive this message after installing, make sure that the"
warn " bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.\n"
end end
require 'bson/types/binary' require 'bson/types/binary'

30
lib/bson/bson_java.rb Normal file
View File

@ -0,0 +1,30 @@
include Java
module BSON
class BSON_JAVA
def self.serialize(obj, check_keys=false, move_id=false)
raise InvalidDocument, "BSON_JAVA.serialize takes a Hash" unless obj.is_a?(Hash)
enc = get_encoder# Java::OrgJbson::RubyBSONEncoder.new(JRuby.runtime)
ByteBuffer.new(enc.encode(obj))
end
def self.get_encoder
@@enc ||= Java::OrgJbson::RubyBSONEncoder.new(JRuby.runtime)
end
def self.get_decoder
@@dec ||= Java::OrgBson::BSONDecoder.new
end
def self.deserialize(buf)
if buf.is_a? String
buf = ByteBuffer.new(buf) if buf
end
dec = get_decoder
callback = Java::OrgJbson::RubyBSONCallback.new(JRuby.runtime)
dec.decode(buf.to_s.to_java_bytes, callback)
callback.get
end
end
end

View File

@ -44,6 +44,7 @@ module BSON
def initialize def initialize
@buf = ByteBuffer.new @buf = ByteBuffer.new
@encoder = BSON_RUBY
end end
if RUBY_VERSION >= '1.9' if RUBY_VERSION >= '1.9'
@ -303,7 +304,7 @@ module BSON
def deserialize_object_data(buf) def deserialize_object_data(buf)
size = buf.get_int size = buf.get_int
buf.position -= 4 buf.position -= 4
object = BSON_CODER.new().deserialize(buf.get(size)) object = @encoder.new().deserialize(buf.get(size))
if object.has_key? "$ref" if object.has_key? "$ref"
DBRef.new(object["$ref"], object["$id"]) DBRef.new(object["$ref"], object["$id"])
else else
@ -358,7 +359,7 @@ module BSON
scope_size = buf.get_int scope_size = buf.get_int
buf.position -= 4 buf.position -= 4
scope = BSON_CODER.new().deserialize(buf.get(scope_size)) scope = @encoder.new().deserialize(buf.get(scope_size))
Code.new(encoded_str(code), scope) Code.new(encoded_str(code), scope)
end end
@ -452,7 +453,7 @@ module BSON
def serialize_object_element(buf, key, val, check_keys, opcode=OBJECT) def serialize_object_element(buf, key, val, check_keys, opcode=OBJECT)
buf.put(opcode) buf.put(opcode)
self.class.serialize_key(buf, key) self.class.serialize_key(buf, key)
buf.put_array(BSON_CODER.new.serialize(val, check_keys).to_a) buf.put_array(@encoder.new.serialize(val, check_keys).to_a)
end end
def serialize_array_element(buf, key, val, check_keys) def serialize_array_element(buf, key, val, check_keys)
@ -527,9 +528,9 @@ module BSON
len_pos = buf.position len_pos = buf.position
buf.put_int(0) buf.put_int(0)
buf.put_int(val.length + 1) buf.put_int(val.code.length + 1)
self.class.serialize_cstr(buf, val) self.class.serialize_cstr(buf, val.code)
buf.put_array(BSON_CODER.new.serialize(val.scope).to_a) buf.put_array(@encoder.new.serialize(val.scope).to_a)
end_pos = buf.position end_pos = buf.position
buf.put_int(end_pos - len_pos, len_pos) buf.put_int(end_pos - len_pos, len_pos)

View File

@ -19,10 +19,10 @@
module BSON module BSON
# JavaScript code to be evaluated by MongoDB. # JavaScript code to be evaluated by MongoDB.
class Code < String class Code
# Hash mapping identifiers to their values # Hash mapping identifiers to their values
attr_accessor :scope attr_accessor :scope, :code
# Wrap code to be evaluated by MongoDB. # Wrap code to be evaluated by MongoDB.
# #
@ -30,9 +30,22 @@ module BSON
# @param [Hash] a document mapping identifiers to values, which # @param [Hash] a document mapping identifiers to values, which
# represent the scope in which the code is to be executed. # represent the scope in which the code is to be executed.
def initialize(code, scope={}) def initialize(code, scope={})
super(code) @code = code
@scope = scope @scope = scope
end end
def length
@code.length
end
def ==(other)
self.class == other.class &&
@code == other.code && @scope == other.scope
end
def inspect
"<BSON::Code:#{object_id} @data=\"#{@code}\" @scope=\"#{@scope.inspect}\">"
end
end end
end end

View File

@ -38,5 +38,9 @@ module BSON
"ns: #{namespace}, id: #{object_id}" "ns: #{namespace}, id: #{object_id}"
end end
def to_hash
{"$ns" => @namespace, "$id" => @object_id }
end
end end
end end

View File

@ -477,7 +477,7 @@ class TestCollection < Test::Unit::TestCase
def test_saving_dates_pre_epoch def test_saving_dates_pre_epoch
begin begin
@@test.save({'date' => Time.utc(1600)}) @@test.save({'date' => Time.utc(1600)})
assert_in_delta Time.utc(1600), @@test.find_one()["date"], 0.001 assert_in_delta Time.utc(1600), @@test.find_one()["date"], 2
rescue ArgumentError rescue ArgumentError
# See note in test_date_before_epoch (BSONTest) # See note in test_date_before_epoch (BSONTest)
end end

View File

@ -631,6 +631,7 @@ class DBAPITest < Test::Unit::TestCase
assert_equal("mike", @@coll.find_one()["hello"]) assert_equal("mike", @@coll.find_one()["hello"])
end end
if !RUBY_PLATFORM =~ /java/
def test_invalid_key_names def test_invalid_key_names
@@coll.remove @@coll.remove
@ -640,6 +641,7 @@ class DBAPITest < Test::Unit::TestCase
assert_raise BSON::InvalidKeyName do assert_raise BSON::InvalidKeyName do
@@coll.insert({"$hello" => "world"}) @@coll.insert({"$hello" => "world"})
end end
assert_raise BSON::InvalidKeyName do assert_raise BSON::InvalidKeyName do
@@coll.insert({"hello" => {"$hello" => "world"}}) @@coll.insert({"hello" => {"$hello" => "world"}})
end end
@ -666,6 +668,7 @@ class DBAPITest < Test::Unit::TestCase
@@coll.insert({"hello" => {"hel.lo" => "world"}}) @@coll.insert({"hello" => {"hel.lo" => "world"}})
end end
end end
end
def test_collection_names def test_collection_names
assert_raise TypeError do assert_raise TypeError do

View File

@ -322,7 +322,7 @@ class BSONTest < Test::Unit::TestCase
assert_equal Binary::SUBTYPE_USER_DEFINED, bin2.subtype assert_equal Binary::SUBTYPE_USER_DEFINED, bin2.subtype
end end
def test_binary_binary_subtype_0 def test_binary_subtype_0
bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_SIMPLE) bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_SIMPLE)
doc = {'bin' => bin} doc = {'bin' => bin}

View File

@ -0,0 +1,521 @@
# encoding:utf-8
require './test/test_helper'
require 'complex'
require 'bigdecimal'
require 'rational'
begin
require 'active_support/core_ext'
Time.zone = "Pacific Time (US & Canada)"
Zone = Time.zone.now
rescue LoadError
warn 'Mocking time with zone'
module ActiveSupport
class TimeWithZone
end
end
Zone = ActiveSupport::TimeWithZone.new
end
class BSONTest < Test::Unit::TestCase
include BSON
def setup
@encoder = BSON::BSON_JAVA
@decoder = BSON::BSON_RUBY#BSON::BSON_JAVA
end
def assert_doc_pass(doc, options={})
bson = @encoder.serialize(doc)
if options[:debug]
puts "DEBUGGIN DOC:"
p bson.to_a
puts "DESERIALIZES TO:"
p @decoder.deserialize(bson)
end
assert_equal @decoder.serialize(doc).to_a, bson.to_a
assert_equal doc, @decoder.deserialize(bson)
end
def test_require_hash
assert_raise_error InvalidDocument, "takes a Hash" do
BSON.serialize('foo')
end
assert_raise_error InvalidDocument, "takes a Hash" do
BSON.serialize(Object.new)
end
assert_raise_error InvalidDocument, "takes a Hash" do
BSON.serialize(Set.new)
end
end
def test_string
doc = {'doc' => 'hello, world'}
assert_doc_pass(doc)
end
def test_valid_utf8_string
doc = {'doc' => 'aé'}
assert_doc_pass(doc)
end
def test_valid_utf8_key
doc = {'aé' => 'hello'}
assert_doc_pass(doc)
end
def test_document_length
doc = {'name' => 'a' * 5 * 1024 * 1024}
assert_raise InvalidDocument do
assert @encoder.serialize(doc)
end
end
# In 1.8 we test that other string encodings raise an exception.
# In 1.9 we test that they get auto-converted.
if RUBY_VERSION < '1.9'
require 'iconv'
def test_invalid_string
string = Iconv.conv('iso-8859-1', 'utf-8', 'aé')
doc = {'doc' => string}
assert_raise InvalidStringEncoding do
@encoder.serialize(doc)
end
end
def test_invalid_key
key = Iconv.conv('iso-8859-1', 'utf-8', 'aé')
doc = {key => 'hello'}
assert_raise InvalidStringEncoding do
@encoder.serialize(doc)
end
end
else
def test_non_utf8_string
bson = BSON::BSON_CODER.serialize({'str' => 'aé'.encode('iso-8859-1')})
result = BSON::BSON_CODER.deserialize(bson)['str']
assert_equal 'aé', result
assert_equal 'UTF-8', result.encoding.name
end
def test_non_utf8_key
bson = BSON::BSON_CODER.serialize({'aé'.encode('iso-8859-1') => 'hello'})
assert_equal 'hello', BSON::BSON_CODER.deserialize(bson)['aé']
end
# Based on a test from sqlite3-ruby
def test_default_internal_is_honored
before_enc = Encoding.default_internal
str = "壁に耳あり、障子に目あり"
bson = BSON::BSON_CODER.serialize("x" => str)
Encoding.default_internal = 'EUC-JP'
out = BSON::BSON_CODER.deserialize(bson)["x"]
assert_equal Encoding.default_internal, out.encoding
assert_equal str.encode('EUC-JP'), out
assert_equal str, out.encode(str.encoding)
ensure
Encoding.default_internal = before_enc
end
end
def test_code
doc = {'$where' => Code.new('this.a.b < this.b')}
assert_doc_pass(doc)
end
def test_code_with_scope
doc = {'$where' => Code.new('this.a.b < this.b', {'foo' => 1})}
assert_doc_pass(doc)
end
def test_double
doc = {'doc' => 41.25}
assert_doc_pass(doc)
end
def test_int
doc = {'doc' => 42}
assert_doc_pass(doc)
doc = {"doc" => -5600}
assert_doc_pass(doc)
doc = {"doc" => 2147483647}
assert_doc_pass(doc)
doc = {"doc" => -2147483648}
assert_doc_pass(doc)
end
def test_ordered_hash
doc = BSON::OrderedHash.new
doc["b"] = 1
doc["a"] = 2
doc["c"] = 3
doc["d"] = 4
assert_doc_pass(doc)
end
def test_object
doc = {'doc' => {'age' => 42, 'name' => 'Spongebob', 'shoe_size' => 9.5}}
assert_doc_pass(doc)
bson = BSON::BSON_CODER.serialize(doc)
end
def test_oid
doc = {'doc' => ObjectId.new}
assert_doc_pass(doc)
end
def test_array
doc = {'doc' => [1, 2, 'a', 'b']}
assert_doc_pass(doc)
end
def test_regex
doc = {'doc' => /foobar/i}
assert_doc_pass(doc)
end
def test_boolean
doc = {'doc' => true}
assert_doc_pass(doc)
end
def test_date
doc = {'date' => Time.now}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
# Mongo only stores up to the millisecond
assert_in_delta doc['date'], doc2['date'], 0.001
end
def test_date_returns_as_utc
doc = {'date' => Time.now}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
assert doc2['date'].utc?
end
def test_date_before_epoch
begin
doc = {'date' => Time.utc(1600)}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
# Mongo only stores up to the millisecond
assert_in_delta doc['date'], doc2['date'], 2
rescue ArgumentError
# some versions of Ruby won't let you create pre-epoch Time instances
#
# TODO figure out how that will work if somebady has saved data
# w/ early dates already and is just querying for it.
end
end
def test_exeption_on_using_unsupported_date_class
[DateTime.now, Date.today, Zone].each do |invalid_date|
doc = {:date => invalid_date}
begin
bson = BSON::BSON_CODER.serialize(doc)
rescue => e
ensure
if !invalid_date.is_a? Time
assert_equal InvalidDocument, e.class
assert_match /UTC Time/, e.message
end
end
end
end
def test_dbref
oid = ObjectId.new
doc = {}
doc['dbref'] = DBRef.new('namespace', oid)
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
if RUBY_PLATFORM =~ /java/
assert_equal 'namespace', doc2['dbref']['$ns']
assert_equal oid, doc2['dbref']['$id']
else
assert_equal 'namespace', doc2['dbref'].namespace
assert_equal oid, doc2['dbref'].object_id
end
end
def test_symbol
doc = {'sym' => :foo}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
assert_equal :foo, doc2['sym']
end
def test_binary
bin = Binary.new
'binstring'.each_byte { |b| bin.put(b) }
doc = {'bin' => bin}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
bin2 = doc2['bin']
assert_kind_of Binary, bin2
assert_equal 'binstring', bin2.to_s
assert_equal Binary::SUBTYPE_BYTES, bin2.subtype
end
def test_binary_with_string
b = Binary.new('somebinarystring')
doc = {'bin' => b}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
bin2 = doc2['bin']
assert_kind_of Binary, bin2
assert_equal 'somebinarystring', bin2.to_s
assert_equal Binary::SUBTYPE_BYTES, bin2.subtype
end
def test_binary_type
bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_USER_DEFINED)
doc = {'bin' => bin}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
bin2 = doc2['bin']
assert_kind_of Binary, bin2
assert_equal [1, 2, 3, 4, 5], bin2.to_a
assert_equal Binary::SUBTYPE_USER_DEFINED, bin2.subtype
end
# Java doesn't support this yet
if !(RUBY_PLATFORM =~ /java/)
def test_binary_subtype_0
bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_SIMPLE)
doc = {'bin' => bin}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
bin2 = doc2['bin']
assert_kind_of Binary, bin2
assert_equal [1, 2, 3, 4, 5], bin2.to_a
assert_equal Binary::SUBTYPE_SIMPLE, bin2.subtype
end
end
def test_binary_byte_buffer
bb = Binary.new
5.times { |i| bb.put(i + 1) }
doc = {'bin' => bb}
bson = @encoder.serialize(doc)
doc2 = @decoder.deserialize(bson)
bin2 = doc2['bin']
assert_kind_of Binary, bin2
assert_equal [1, 2, 3, 4, 5], bin2.to_a
assert_equal Binary::SUBTYPE_BYTES, bin2.subtype
end
def test_put_id_first
val = BSON::OrderedHash.new
val['not_id'] = 1
val['_id'] = 2
roundtrip = @decoder.deserialize(@encoder.serialize(val, false, true).to_s)
assert_kind_of BSON::OrderedHash, roundtrip
assert_equal '_id', roundtrip.keys.first
val = {'a' => 'foo', 'b' => 'bar', :_id => 42, 'z' => 'hello'}
roundtrip = @decoder.deserialize(@encoder.serialize(val, false, true).to_s)
assert_kind_of BSON::OrderedHash, roundtrip
assert_equal '_id', roundtrip.keys.first
end
def test_nil_id
doc = {"_id" => nil}
assert_doc_pass(doc)
end
def test_timestamp
val = {"test" => [4, 20]}
assert_equal val, @decoder.deserialize([0x13, 0x00, 0x00, 0x00,
0x11, 0x74, 0x65, 0x73,
0x74, 0x00, 0x04, 0x00,
0x00, 0x00, 0x14, 0x00,
0x00, 0x00, 0x00])
end
def test_overflow
doc = {"x" => 2**75}
assert_raise RangeError do
bson = @encoder.serialize(doc)
end
doc = {"x" => 9223372036854775}
assert_doc_pass(doc)
doc = {"x" => 9223372036854775807}
assert_doc_pass(doc)
doc["x"] = doc["x"] + 1
assert_raise RangeError do
bson = @encoder.serialize(doc)
end
doc = {"x" => -9223372036854775}
assert_doc_pass(doc)
doc = {"x" => -9223372036854775808}
assert_doc_pass(doc)
doc["x"] = doc["x"] - 1
assert_raise RangeError do
bson = BSON::BSON_CODER.serialize(doc)
end
end
def test_invalid_numeric_types
[BigDecimal.new("1.0"), Complex(0, 1), Rational(2, 3)].each do |type|
doc = {"x" => type}
begin
@encoder.serialize(doc)
rescue => e
ensure
assert_equal InvalidDocument, e.class
assert_match /Cannot serialize/, e.message
end
end
end
def test_do_not_change_original_object
val = BSON::OrderedHash.new
val['not_id'] = 1
val['_id'] = 2
assert val.keys.include?('_id')
@encoder.serialize(val)
assert val.keys.include?('_id')
val = {'a' => 'foo', 'b' => 'bar', :_id => 42, 'z' => 'hello'}
assert val.keys.include?(:_id)
@encoder.serialize(val)
assert val.keys.include?(:_id)
end
# note we only test for _id here because in the general case we will
# write duplicates for :key and "key". _id is a special case because
# we call has_key? to check for it's existence rather than just iterating
# over it like we do for the rest of the keys. thus, things like
# HashWithIndifferentAccess can cause problems for _id but not for other
# keys. rather than require rails to test with HWIA directly, we do this
# somewhat hacky test.
def test_no_duplicate_id
dup = {"_id" => "foo", :_id => "foo"}
one = {"_id" => "foo"}
assert_equal @encoder.serialize(one).to_a, @encoder.serialize(dup).to_a
end
def test_no_duplicate_id_when_moving_id
dup = {"_id" => "foo", :_id => "foo"}
one = {:_id => "foo"}
assert_equal @encoder.serialize(one, false, true).to_s, @encoder.serialize(dup, false, true).to_s
end
def test_null_character
doc = {"a" => "\x00"}
assert_doc_pass(doc)
assert_raise InvalidDocument do
@encoder.serialize({"\x00" => "a"})
end
assert_raise InvalidDocument do
@encoder.serialize({"a" => (Regexp.compile "ab\x00c")})
end
end
def test_max_key
doc = {"a" => MaxKey.new}
assert_doc_pass(doc)
end
def test_min_key
doc = {"a" => MinKey.new}
assert_doc_pass(doc)
end
def test_invalid_object
o = Object.new
assert_raise InvalidDocument do
@encoder.serialize({:foo => o})
end
assert_raise InvalidDocument do
@encoder.serialize({:foo => Date.today})
end
end
def test_move_id
a = BSON::OrderedHash.new
a['text'] = 'abc'
a['key'] = 'abc'
a['_id'] = 1
assert_equal ")\000\000\000\020_id\000\001\000\000\000\002text" +
"\000\004\000\000\000abc\000\002key\000\004\000\000\000abc\000\000",
@encoder.serialize(a, false, true).to_s
# Java doesn't support this. Isn't actually necessary.
if !(RUBY_PLATFORM =~ /java/)
assert_equal ")\000\000\000\002text\000\004\000\000\000abc\000\002key" +
"\000\004\000\000\000abc\000\020_id\000\001\000\000\000\000",
@encoder.serialize(a, false, false).to_s
end
end
def test_move_id_with_nested_doc
b = BSON::OrderedHash.new
b['text'] = 'abc'
b['_id'] = 2
c = BSON::OrderedHash.new
c['text'] = 'abc'
c['hash'] = b
c['_id'] = 3
assert_equal ">\000\000\000\020_id\000\003\000\000\000\002text" +
"\000\004\000\000\000abc\000\003hash\000\034\000\000" +
"\000\002text\000\004\000\000\000abc\000\020_id\000\002\000\000\000\000\000",
@encoder.serialize(c, false, true).to_s
# Java doesn't support this. Isn't actually necessary.
if !(RUBY_PLATFORM =~ /java/)
assert_equal ">\000\000\000\002text\000\004\000\000\000abc\000\003hash" +
"\000\034\000\000\000\002text\000\004\000\000\000abc\000\020_id" +
"\000\002\000\000\000\000\020_id\000\003\000\000\000\000",
@encoder.serialize(c, false, false).to_s
end
end
# Mocking this class for testing
class ::HashWithIndifferentAccess < Hash; end
def test_keep_id_with_hash_with_indifferent_access
doc = HashWithIndifferentAccess.new
embedded = HashWithIndifferentAccess.new
embedded['_id'] = ObjectId.new
doc['_id'] = ObjectId.new
doc['embedded'] = [embedded]
@encoder.serialize(doc, false, true).to_a
assert doc.has_key?("_id")
assert doc['embedded'][0].has_key?("_id")
doc['_id'] = ObjectId.new
@encoder.serialize(doc, false, true).to_a
assert doc.has_key?("_id")
end
end