1 module struct2mongo; 2 3 import mondo; 4 import bsond; 5 import std.traits, std.range; 6 7 enum MongoKeep; 8 enum MongoUpdated; 9 enum MongoCreated; 10 struct Col { 11 Collection collection; 12 13 auto trySeveralTimes (alias Op, Args...)(uint times, Args args) { 14 tryAgain: 15 try { 16 return Op (args); 17 } catch (MongoException e) { 18 if (e.domain == ErrorDomains.SERVER_SELECTION 19 || e.domain == ErrorDomains.STREAM) { 20 21 import std.stdio; 22 writeln (`Connection failed, trying again.`); 23 import core.thread; 24 Thread.sleep (1.dur!`seconds`); 25 times --; 26 if (times == 0) { 27 throw e; 28 } else { 29 goto tryAgain; // Yeah. 30 } 31 } 32 } 33 } 34 35 unittest { 36 import std.stdio; 37 struct TestStruct { 38 int foo; 39 } 40 41 Mongo mongo = new Mongo("mongodb://localhost"); 42 assert (mongo.connected, `Not connected to Mongo`); 43 auto collection = Col (mongo [`newBase`][`newCollection`]); 44 // Be careful, this deletes the Collection. 45 if (collection.exists) collection.drop; 46 47 return collection.trySeveralTimes!(insert)(1, TestStruct (3)); 48 } 49 50 // Version without ref for non-lvalues. 51 auto insert (S)(S val) { this.insert (val); } 52 auto insert (S)(ref S val) { 53 alias created = getSymbolsByUDA!(S, MongoCreated); 54 static assert ( 55 created.length < 2 56 , `Are you sure you want several MongoCreated` 57 ~ ` symbols in ` ~ S.stringof ~ `?` 58 ); 59 import std.datetime; 60 static if (created.length) { 61 static assert (is (typeof (created [0]) == long)); 62 mixin ( 63 `val.` ~ __traits (identifier, created [0]) 64 ~ ` = Clock.currTime (UTC ()).toUnixTime;` 65 ); 66 } 67 collection.insert (val.bson); 68 } 69 70 // Version without ref for non-lvalues. 71 void update (S)( 72 in BsonObject selector 73 , S update 74 , in UpdateFlags flags = UpdateFlags.NONE 75 , in WriteConcern writeConcern = null 76 ) { 77 this.update!S (selector, update, flags, writeConcern); 78 } 79 80 void update (S)( 81 in BsonObject selector 82 , ref S update 83 , in UpdateFlags flags = UpdateFlags.NONE 84 , in WriteConcern writeConcern = null 85 ) { 86 static if (is (S == BO)) { 87 // Mondo's 88 collection.update (selector, update, flags, writeConcern); 89 } else { 90 alias updated = getSymbolsByUDA!(S, MongoUpdated); 91 static assert ( 92 updated.length < 2 93 , `Are you sure you want several MongoUpdated` 94 ~ ` symbols in ` ~ S.stringof ~ `?` 95 ); 96 import std.datetime; 97 static if (updated.length) { 98 static assert (is (typeof (updated [0]) == long)); 99 mixin ( 100 `update.` ~ __traits (identifier, updated [0]) 101 ~ ` = Clock.currTime (UTC ()).toUnixTime;` 102 ); 103 } 104 collection.update (selector, update.bson, flags, writeConcern); 105 } 106 } 107 108 /// Same parameters as Collection.findOne (except the first one). 109 /// S (return type) needs to be specified. 110 S findOne (S)( 111 in Query query = Query.init 112 , in QueryFlags flags = QueryFlags.NONE 113 , in ReadPrefs readPrefs = null 114 ) { 115 return collection 116 .findOne!BO (query, flags, readPrefs) 117 .fromBO!S; 118 } 119 /// S (return type) needs to be specified. 120 auto find (S)( 121 in Query query = Query.init 122 , in QueryFlags flags = QueryFlags.NONE 123 , in ReadPrefs readPrefs = null 124 ) { 125 import std.algorithm : map; 126 return collection 127 .find!BO (query, flags, readPrefs) 128 .map!(a => a.fromBO!S); 129 } 130 131 auto aggregate (S = BO, K)( 132 in K aggregate 133 , in BsonObject options = BsonObject.init 134 , in QueryFlags flags = QueryFlags.NONE 135 , in ReadPrefs readPrefs = null 136 ) if (is(Unqual!K == BsonArray) || is(Unqual!K == BsonObject)) { 137 auto toReturn = 138 collection.aggregate (aggregate, options, flags, readPrefs); 139 static if (is (S == BO)) { 140 return toReturn; 141 } else { 142 return toReturn.map!(a => a.fromBO!S); 143 } 144 } 145 146 // From here: Just calls to Mondo's methods. 147 auto findOne ( 148 in Query query = Query.init 149 , in QueryFlags flags = QueryFlags.NONE 150 , in ReadPrefs readPrefs = null 151 ) { 152 return collection.findOne (query, flags, readPrefs); 153 } 154 auto find ( 155 in Query query = Query.init 156 , in QueryFlags flags = QueryFlags.NONE 157 , in ReadPrefs readPrefs = null 158 ) { 159 return collection.find (query, flags, readPrefs); 160 } 161 162 alias collection this; 163 } 164 unittest { 165 Mongo mongo = new Mongo("mongodb://localhost"); 166 assert (mongo.connected, `Not connected to Mongo`); 167 auto collection = Col (mongo [`newBase`][`newCollection`]); 168 // Be careful, this deletes the Collection. 169 if (collection.exists) collection.drop; 170 struct Foo {int a = 4;} 171 collection.insert (Foo ()); 172 assert (collection.findOne!Foo == Foo ()); 173 assert (collection.find!Foo.front == Foo ()); 174 collection.insert (Foo (8)); 175 assert ( 176 collection 177 .aggregate!Foo (BA ([BO(`$match`, BO (`a`, 8))])) 178 .front == Foo (8) 179 ); 180 181 struct UpdatedTest { 182 int val = 0; 183 @MongoUpdated long updateTime = 0; 184 @MongoCreated long createdTime = 0; 185 } 186 auto ut = UpdatedTest (1); 187 assert (ut.createdTime == 0); 188 collection.insert (ut); 189 assert (ut.createdTime != 0); 190 ut.val = 2; 191 assert (ut.updateTime == 0); 192 collection.update (BO (`val`, 1), ut); 193 assert (ut.updateTime != 0); 194 195 collection.remove (BO (`val`, 2)); 196 197 // Test Mondo's methods. 198 assert ( 199 collection 200 .aggregate (BA ([BO(`$match`, BO (`a`, 8))])) 201 .front [`a`] == 8 202 ); 203 assert (collection.find.array.length == 2); 204 assert ((`a` !in collection.findOne ()) || collection.findOne ()[`a`] == 8); 205 206 auto find16 = new Query; 207 find16.conditions = BO (`a`, 16); 208 collection.update (BO (`a`, 8), BO (`a`, 16)); 209 assert (! collection.find (find16).empty); 210 211 } 212 213 // A BsonObject converted to BO it's just itself. 214 auto bson (BO b) { return b;} 215 216 BO bson (Type)(Type instance) { 217 static assert (__traits (isPOD, Type) 218 , `bson (instance) is only implemented for POD structs`); 219 220 // If an empty BO constructor is used, it segfaults when appending. 221 // Already fixed in master, still not pushed to dub. 222 auto toReturn = BO (`a`, `b`); 223 toReturn.remove (`a`); 224 static foreach (field; FieldNameTuple!Type) { { 225 auto instanceField = __traits (getMember, instance, field); 226 // Save only the fields with non default values or the ones that 227 // have the @MongoKeep UDA. 228 if ( 229 instanceField != __traits(getMember, Type.init, field) 230 || hasUDA! (mixin (`Type.` ~ field), MongoKeep) 231 ) { 232 static if (field == `_id`) { 233 auto toInsert = ObjectId (instanceField); 234 } else { 235 auto toInsert = recursiveBsonArray (instanceField); 236 } 237 toReturn.append (field, toInsert); 238 } 239 } } 240 return toReturn; 241 } 242 243 unittest { 244 struct Foo { 245 string a = `Hello`; 246 int b = 3; 247 int [3] c = [2,3,4]; 248 @MongoKeep bool d = true; 249 } 250 // If the default values are used, nothing needs to be saved. 251 // Note: This one fails, because internally it hasn't been initted. 252 // Should be already fixed on Mondo's master. 253 //assert (bson (Foo ()) == BO()); 254 BO toCompare = BO (`d`, true); 255 assert (bson (Foo ()) == toCompare); 256 257 toCompare.append (`b`, 5); 258 assert (bson (Foo (`Hello`, 5)) == toCompare); 259 260 struct WithId { 261 string _id; 262 } 263 string customId = `dddddddddddddddddddddddd`; 264 auto withId = WithId (customId); 265 assert (bson (withId) == BO(`_id`, ObjectId(customId))); 266 267 } 268 269 /// Converts bo to Type by using the field names of Type and keys of bo. 270 auto fromBO (Type) (BO bo) { 271 static assert (__traits (isPOD, Type) 272 , "fromBO is made for POD structs.\n" 273 ~ ` Make sure it's okay to assign to ` ~ Type.stringof 274 ~ `'s fields and comment this warning.`); 275 alias TypeFields = FieldNameTuple!Type; 276 Type toReturn; 277 foreach (key, val; bo) { 278 outerSwitch: switch (key) { 279 static foreach (field; TypeFields) { 280 case field: 281 alias FieldType = typeof (mixin (`Type.` ~ field)); 282 FieldType toAssign; 283 static if (field == `_id` && is (FieldType == string)) { 284 // Slightly modified version of Mondo's ObjectId.toString () 285 // Allows casting the ObjectId back to a string. 286 static immutable char[] digits = "0123456789abcdef"; 287 auto app = appender!string; 288 foreach (b; bo [field].to!ObjectId._data) { 289 app.put (digits [b >> 4]); 290 app.put (digits [b & 0xF]); 291 } 292 toAssign = app.data; 293 } else { 294 toAssign = bo [field].recursiveArrayMap! (FieldType); 295 } 296 enum fieldToAssign = `toReturn.` ~ field; 297 mixin (fieldToAssign ~ ` = toAssign;`); 298 break outerSwitch; 299 } 300 default: 301 // _id is the only field that is allowed to be on the BO 302 // and not on the struct, if bo has some other field that the 303 // struct doesn't, an exception is thrown. 304 if (key != `_id`) 305 throw new Exception (`Found member of BO that is not in ` 306 ~ Type.stringof ~ ` : ` ~ key); 307 } 308 } 309 return toReturn; 310 } 311 312 unittest { 313 struct Test { 314 int a = 3; 315 int b = 5; 316 string c = `Foo`; 317 int d; 318 bool e = true; 319 int [] f = [6,5,4]; 320 } 321 struct WithId { 322 int s = 3; 323 string _id = "aaaaaaaaaaaaaaaaaaaaaaaa"; 324 } 325 struct WithObjectId { 326 ObjectId _id = "cccccccccccccccccccccccc"; 327 } 328 auto comparedTo = Test (3, 5, `Bar`); 329 assert (fromBO!Test (BO (`c`, `Bar`)) == comparedTo); 330 // Test that operations are the inverse of the other one. 331 assert (comparedTo.bson.fromBO!Test == comparedTo); 332 auto idCheck = WithId (3, `bbbbbbbbbbbbbbbbbbbbbbbb`); 333 auto boWithId = BO (`_id`, ObjectId (`bbbbbbbbbbbbbbbbbbbbbbbb`)); 334 assert (fromBO!WithId (boWithId) == idCheck); 335 auto objectIdCheck = WithObjectId (ObjectId(`bbbbbbbbbbbbbbbbbbbbbbbb`)); 336 assert (fromBO!WithObjectId (boWithId) == objectIdCheck); 337 338 // Using a BO with other fields should throw an exception: 339 auto extraFields = BO (`a`, 3, `g`, 8); 340 import std.exception; 341 assertThrown (fromBO!Test (extraFields)); 342 } 343 344 import std.algorithm : map; 345 346 /// Used to handle arrays because Mondo uses BsonArrays. 347 auto recursiveBsonArray (Type)(Type input) { 348 // One-dimensional arrays can avoid the need of a BsonArray. 349 static if (isArray!Type && !is (Type == string)) { 350 // Slice operator is used to allow static arrays. 351 return BsonArray (input [].map!(a => a.recursiveBsonArray).array); 352 } else { 353 return input; 354 } 355 } 356 357 unittest { 358 assert (recursiveBsonArray (5) == 5); 359 assert (recursiveBsonArray ([1,2,3]) == BA([1,2,3])); 360 import std.stdio; 361 assert (recursiveBsonArray ([[3,4,5], [1,2], []]) 362 == BA([BA(3,4,5), BA(1,2), BA()])); 363 assert (recursiveBsonArray (`Hello`) == `Hello`); 364 } 365 366 auto recursiveArrayMap (Type)(BsonValue input) { 367 static if (isArray!Type && !is (Type == string)) { 368 return input 369 .to!(BsonArray) 370 .map!(a => a.recursiveArrayMap!(ElementType!Type)) 371 .array; 372 } else { 373 import std.conv : to; 374 return input.to!Type; 375 } 376 } 377 378 unittest { 379 assert (recursiveArrayMap!int (BsonValue (5)) == 5); 380 assert (recursiveArrayMap! (int [])(BsonValue (BA ([1,2,3]))) == [1,2,3]); 381 assert (recursiveArrayMap! (string [])(BsonValue (BA ([`Foo`, `Bar`, ``]))) 382 == [`Foo`, `Bar`, ``]); 383 assert (recursiveArrayMap! (int [][])(BsonValue (BA ([BA([1,2]), BA([3,4])]))) 384 == [[1,2], [3,4]]); 385 }