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 }