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 }