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