├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── d-linter.ini ├── dub.json └── source └── mongoschema ├── aliases.d ├── date.d ├── db.d ├── package.d ├── query.d └── variant.d /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | dcompiler: [dmd-latest, dmd-beta, dmd-master, dmd-2.094.2, ldc-latest, ldc-beta, ldc-master] 12 | os: [ubuntu-latest, windows-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install ${{ matrix.dcompiler }} 17 | uses: dlang-community/setup-dlang@v1 18 | with: 19 | compiler: ${{ matrix.dcompiler }} 20 | - name: Build 21 | run: dub build 22 | #- name: Start MongoDB 23 | # run: mongod --bind_ip 127.0.0.1 --fork 24 | #- name: Run tests 25 | # run: dub test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | *.o 5 | *.obj 6 | __test__library__ 7 | libmongoschemad.a 8 | dub.selections.json 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongoSchemaD 2 | 3 | A simple library for vibe.d adding support for structured Bson 4 | data using structs/classes and functions to simplify saving, 5 | updating and finding Mongo documents. 6 | 7 | Can also be used without MongoDB for Bson (de)serialization. 8 | 9 | ## Example 10 | 11 | ```d 12 | import vibe.db.mongo.mongo; 13 | import mongoschema; 14 | import mongoschema.aliases : name, ignore, unique, binary; 15 | 16 | struct Permission 17 | { 18 | string name; 19 | int priority; 20 | } 21 | 22 | struct User 23 | { 24 | mixin MongoSchema; // Adds save, update, etc. 25 | 26 | @unique 27 | string username; 28 | 29 | @binary() 30 | ubyte[] hash; 31 | @binary() 32 | ubyte[] salt; 33 | 34 | @name("profile-picture") 35 | string profilePicture = "default.png"; 36 | 37 | Permission[] permissions; 38 | 39 | @ignore: 40 | int sessionID; 41 | } 42 | 43 | // implement these for this example 44 | // if you use static arrays in the functions (like using the std.digest methods) 45 | // you need to call .idup on your arrays to copy them to GC memory, otherwise 46 | // they would corrupt when leaving the stack frame. (returning the function) 47 | ubyte[] generateSalt(); 48 | ubyte[] complicatedHashFunction(); 49 | 50 | User registerNewUser(string name, string password) 51 | { 52 | User user; 53 | user.username = name; 54 | user.salt = generateSalt(); 55 | user.hash = complicatedHashFunction(password, user.salt); 56 | user.permissions ~= Permission("forum.access", 1); 57 | // Automatically serializes and puts the object in the registered database 58 | // If save was already called or the object got retrieved from the 59 | // collection `save()` will just update the existing object. 60 | user.save(); 61 | // -> 62 | // { 63 | // username: name, 64 | // hash: , 65 | // salt: , 66 | // profile-picture: "default.png", 67 | // permissions: [{ 68 | // name: "forum.access", 69 | // priority: 1 70 | // }] 71 | // } 72 | return user; 73 | } 74 | 75 | // convenience method, could also put this in the User struct 76 | User find(string name) 77 | { 78 | // throws if not found, can also use `tryFindOne` to get a Nullable instead 79 | // of throwing an exception. 80 | return User.findOne(["username": name]); 81 | } 82 | 83 | void main() 84 | { 85 | // connect as usual 86 | auto client = connectMongoDB("localhost"); 87 | 88 | // before accessing any MongoSchemaD functions on a struct, register the 89 | // structs using the `MongoCollection.register!T` function globally. 90 | 91 | // Links the `test.users` collection to the `User` struct. 92 | client.getCollection("test.users").register!User; 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /d-linter.ini: -------------------------------------------------------------------------------- 1 | ; Configurue which static analysis checks are enabled 2 | [analysis.config.StaticAnalysisConfig] 3 | ; Check variable, class, struct, interface, union, and function names against t 4 | ; he Phobos style guide 5 | style_check="false" 6 | ; Check for array literals that cause unnecessary allocation 7 | enum_array_literal_check="true" 8 | ; Check for poor exception handling practices 9 | exception_check="true" 10 | ; Check for use of the deprecated 'delete' keyword 11 | delete_check="true" 12 | ; Check for use of the deprecated floating point operators 13 | float_operator_check="true" 14 | ; Check number literals for readability 15 | number_style_check="true" 16 | ; Checks that opEquals, opCmp, toHash, and toString are either const, immutable 17 | ; , or inout. 18 | object_const_check="true" 19 | ; Checks for .. expressions where the left side is larger than the right. 20 | backwards_range_check="true" 21 | ; Checks for if statements whose 'then' block is the same as the 'else' block 22 | if_else_same_check="true" 23 | ; Checks for some problems with constructors 24 | constructor_check="true" 25 | ; Checks for unused variables and function parameters 26 | unused_variable_check="false" 27 | ; Checks for unused labels 28 | unused_label_check="true" 29 | ; Checks for duplicate attributes 30 | duplicate_attribute="true" 31 | ; Checks that opEquals and toHash are both defined or neither are defined 32 | opequals_tohash_check="true" 33 | ; Checks for subtraction from .length properties 34 | length_subtraction_check="true" 35 | ; Checks for methods or properties whose names conflict with built-in propertie 36 | ; s 37 | builtin_property_names_check="true" 38 | ; Checks for confusing code in inline asm statements 39 | asm_style_check="true" 40 | ; Checks for confusing logical operator precedence 41 | logical_precedence_check="true" 42 | ; Checks for undocumented public declarations 43 | undocumented_declaration_check="true" 44 | ; Checks for poor placement of function attributes 45 | function_attribute_check="true" 46 | ; Checks for use of the comma operator 47 | comma_expression_check="true" 48 | ; Checks for local imports that are too broad 49 | local_import_check="false" 50 | ; Checks for variables that could be declared immutable 51 | could_be_immutable_check="false" 52 | ; Checks for redundant expressions in if statements 53 | redundant_if_check="true" 54 | ; Checks for redundant parenthesis 55 | redundant_parens_check="true" 56 | ; Checks for labels with the same name as variables 57 | label_var_same_name_check="true" 58 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoschemad", 3 | "description": "MongoDB Schema support", 4 | "copyright": "Copyright © 2017-2024, webfreak", 5 | "authors": ["webfreak"], 6 | "license": "MIT", 7 | "dependencies": 8 | { 9 | "vibe-d:mongodb": ">=0.9.6", 10 | "vibe-d:data": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/mongoschema/aliases.d: -------------------------------------------------------------------------------- 1 | module mongoschema.aliases; 2 | 3 | import mongoschema; 4 | 5 | public: 6 | 7 | /// 8 | alias ignore = schemaIgnore; 9 | /// 10 | alias binary = binaryType; 11 | /// 12 | alias name = schemaName; 13 | 14 | /// 15 | alias background = mongoBackground; 16 | /// 17 | alias dropDuplicates = mongoDropDuplicates; 18 | /// 19 | alias expireAfterSeconds = mongoExpire; 20 | /// 21 | alias expires = mongoExpire; 22 | /// 23 | alias sparse = mongoSparse; 24 | /// 25 | alias unique = mongoUnique; 26 | -------------------------------------------------------------------------------- /source/mongoschema/date.d: -------------------------------------------------------------------------------- 1 | /// This module provides Date serialization with an extra added magic value to serialize the current date at serialization time. 2 | module mongoschema.date; 3 | 4 | import std.datetime.systime; 5 | import std.traits : isSomeString; 6 | 7 | import vibe.data.bson; 8 | 9 | /// Class serializing to a bson date containing a special `now` value that gets translated to the current time when converting to bson. 10 | final struct SchemaDate 11 | { 12 | public @safe: 13 | /// 14 | this(BsonDate date) 15 | { 16 | _time = date.value; 17 | } 18 | 19 | /// 20 | this(long time) 21 | { 22 | _time = time; 23 | } 24 | 25 | /// 26 | @property auto time() const 27 | { 28 | return _time; 29 | } 30 | 31 | /// 32 | static Bson toBson(SchemaDate date) 33 | { 34 | if (date._time == -1) 35 | { 36 | return Bson(BsonDate.fromStdTime(Clock.currStdTime())); 37 | } 38 | else 39 | { 40 | return Bson(BsonDate(date._time)); 41 | } 42 | } 43 | 44 | /// 45 | static SchemaDate fromBson(Bson bson) 46 | { 47 | return SchemaDate(bson.get!BsonDate.value); 48 | } 49 | 50 | /// 51 | static SchemaDate fromSysTime(SysTime stime) 52 | { 53 | return SchemaDate(BsonDate(stime).value); 54 | } 55 | 56 | /// Magic value setting the date to the current time stamp when serializing. 57 | static SchemaDate now() 58 | { 59 | return SchemaDate(-1); 60 | } 61 | 62 | /// Converts this SchemaDate to a std.datetime.SysTime object. 63 | SysTime toSysTime() const 64 | { 65 | if (_time == -1) 66 | return Clock.currTime; 67 | return BsonDate(_time).toSysTime(); 68 | } 69 | 70 | /// Converts this SchemaDate to a vibed BsonDate object. 71 | BsonDate toBsonDate() const 72 | { 73 | return BsonDate(_time); 74 | } 75 | 76 | /// 77 | string toISOExtString() const 78 | { 79 | return toSysTime.toISOExtString; 80 | } 81 | 82 | /// 83 | static SchemaDate fromISOExtString(S)(in S s) if (isSomeString!S) 84 | { 85 | return SchemaDate.fromSysTime(SysTime.fromISOExtString(s)); 86 | } 87 | 88 | private: 89 | long _time; 90 | } 91 | 92 | static assert (isISOExtStringSerializable!SchemaDate); 93 | -------------------------------------------------------------------------------- /source/mongoschema/db.d: -------------------------------------------------------------------------------- 1 | /// This module provides the database utility tools which make the whole project useful. 2 | module mongoschema.db; 3 | 4 | import core.time; 5 | 6 | import mongoschema; 7 | 8 | import std.traits; 9 | import std.typecons : BitFlags, tuple, Tuple; 10 | 11 | /// Range for iterating over a collection using a Schema. 12 | struct DocumentRange(Schema) 13 | { 14 | alias Cursor = MongoCursor!Bson; 15 | 16 | private Cursor _cursor; 17 | 18 | public this(Cursor cursor) 19 | { 20 | _cursor = cursor; 21 | } 22 | 23 | /** 24 | Returns true if there are no more documents for this cursor. 25 | 26 | Throws: An exception if there is a query or communication error. 27 | */ 28 | @property bool empty() 29 | { 30 | return _cursor.empty; 31 | } 32 | 33 | /** 34 | Returns the current document of the response. 35 | 36 | Use empty and popFront to iterate over the list of documents using an 37 | input range interface. Note that calling this function is only allowed 38 | if empty returns false. 39 | */ 40 | @property Schema front() 41 | { 42 | return fromSchemaBson!Schema(_cursor.front); 43 | } 44 | 45 | /** 46 | Controls the order in which the query returns matching documents. 47 | 48 | This method must be called before starting to iterate, or an exeption 49 | will be thrown. If multiple calls to $(D sort()) are issued, only 50 | the last one will have an effect. 51 | 52 | Params: 53 | order = A BSON object convertible value that defines the sort order 54 | of the result. This BSON object must be structured according to 55 | the MongoDB documentation (see below). 56 | 57 | Returns: Reference to the modified original curser instance. 58 | 59 | Throws: 60 | An exception if there is a query or communication error. 61 | Also throws if the method was called after beginning of iteration. 62 | 63 | See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort) 64 | */ 65 | auto sort(T)(T order) 66 | { 67 | _cursor.sort(serializeToBson(order)); 68 | return this; 69 | } 70 | 71 | /** 72 | Limits the number of documents that the cursor returns. 73 | 74 | This method must be called before beginnig iteration in order to have 75 | effect. If multiple calls to limit() are made, the one with the lowest 76 | limit will be chosen. 77 | 78 | Params: 79 | count = The maximum number number of documents to return. A value 80 | of zero means unlimited. 81 | 82 | Returns: the same cursor 83 | 84 | See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit) 85 | */ 86 | auto limit(size_t count) 87 | { 88 | _cursor.limit(count); 89 | return this; 90 | } 91 | 92 | /** 93 | Skips a given number of elements at the beginning of the cursor. 94 | 95 | This method must be called before beginnig iteration in order to have 96 | effect. If multiple calls to skip() are made, the one with the maximum 97 | number will be chosen. 98 | 99 | Params: 100 | count = The number of documents to skip. 101 | 102 | Returns: the same cursor 103 | 104 | See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip) 105 | */ 106 | auto skip(int count) 107 | { 108 | _cursor.skip(count); 109 | return this; 110 | } 111 | 112 | /** 113 | Advances the cursor to the next document of the response. 114 | 115 | Note that calling this function is only allowed if empty returns false. 116 | */ 117 | void popFront() 118 | { 119 | _cursor.popFront(); 120 | } 121 | 122 | /** 123 | Iterates over all remaining documents. 124 | 125 | Note that iteration is one-way - elements that have already been visited 126 | will not be visited again if another iteration is done. 127 | 128 | Throws: An exception if there is a query or communication error. 129 | */ 130 | int opApply(int delegate(Schema doc) del) 131 | { 132 | while (!_cursor.empty) 133 | { 134 | auto doc = _cursor.front; 135 | _cursor.popFront(); 136 | if (auto ret = del(fromSchemaBson!Schema(doc))) 137 | return ret; 138 | } 139 | return 0; 140 | } 141 | } 142 | 143 | /// Exception thrown if a document could not be found. 144 | class DocumentNotFoundException : Exception 145 | { 146 | /// 147 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe 148 | { 149 | super(msg, file, line); 150 | } 151 | } 152 | 153 | /// 154 | struct PipelineUnwindOperation 155 | { 156 | /// Field path to an array field. To specify a field path, prefix the field name with a dollar sign $. 157 | string path; 158 | /// Optional. The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $. 159 | string includeArrayIndex = null; 160 | } 161 | 162 | /// 163 | struct SchemaPipeline 164 | { 165 | @safe: 166 | this(MongoCollection collection) 167 | { 168 | _collection = collection; 169 | } 170 | 171 | /// Passes along the documents with only the specified fields to the next stage in the pipeline. The specified fields can be existing fields from the input documents or newly computed fields. 172 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project 173 | SchemaPipeline project(Bson specifications) 174 | { 175 | assert(!finalized); 176 | pipeline ~= Bson(["$project": specifications]); 177 | return this; 178 | } 179 | 180 | /// ditto 181 | SchemaPipeline project(T)(T specifications) 182 | { 183 | return project(serializeToBson(specifications)); 184 | } 185 | 186 | /// Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage. 187 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match 188 | SchemaPipeline match(Bson query) 189 | { 190 | assert(!finalized); 191 | pipeline ~= Bson(["$match": query]); 192 | return this; 193 | } 194 | 195 | /// ditto 196 | SchemaPipeline match(T)(Query!T query) 197 | { 198 | return match(query._query); 199 | } 200 | 201 | /// ditto 202 | SchemaPipeline match(T)(T[string] query) 203 | { 204 | return match(serializeToBson(query)); 205 | } 206 | 207 | /// Restricts the contents of the documents based on information stored in the documents themselves. 208 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact 209 | SchemaPipeline redact(Bson expression) 210 | { 211 | assert(!finalized); 212 | pipeline ~= Bson(["$redact": expression]); 213 | return this; 214 | } 215 | 216 | /// ditto 217 | SchemaPipeline redact(T)(T expression) 218 | { 219 | return redact(serializeToBson(expression)); 220 | } 221 | 222 | /// Limits the number of documents passed to the next stage in the pipeline. 223 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit 224 | SchemaPipeline limit(size_t count) 225 | { 226 | assert(!finalized); 227 | pipeline ~= Bson(["$limit": Bson(count)]); 228 | return this; 229 | } 230 | 231 | /// Skips over the specified number of documents that pass into the stage and passes the remaining documents to the next stage in the pipeline. 232 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip 233 | SchemaPipeline skip(size_t count) 234 | { 235 | assert(!finalized); 236 | pipeline ~= Bson(["$skip": Bson(count)]); 237 | return this; 238 | } 239 | 240 | /// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element. 241 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind 242 | SchemaPipeline unwind(string path) 243 | { 244 | assert(!finalized); 245 | pipeline ~= Bson(["$unwind": Bson(path)]); 246 | return this; 247 | } 248 | 249 | /// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element. 250 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind 251 | SchemaPipeline unwind(PipelineUnwindOperation op) 252 | { 253 | assert(!finalized); 254 | Bson opb = Bson(["path": Bson(op.path)]); 255 | if (op.includeArrayIndex !is null) 256 | opb["includeArrayIndex"] = Bson(op.includeArrayIndex); 257 | pipeline ~= Bson(["$unwind": opb]); 258 | return this; 259 | } 260 | 261 | /// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element. 262 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind 263 | SchemaPipeline unwind(PipelineUnwindOperation op, bool preserveNullAndEmptyArrays) 264 | { 265 | assert(!finalized); 266 | Bson opb = Bson([ 267 | "path": Bson(op.path), 268 | "preserveNullAndEmptyArrays": Bson(preserveNullAndEmptyArrays) 269 | ]); 270 | if (op.includeArrayIndex !is null) 271 | opb["includeArrayIndex"] = Bson(op.includeArrayIndex); 272 | pipeline ~= Bson(["$unwind": opb]); 273 | return this; 274 | } 275 | 276 | /// Groups documents by some specified expression and outputs to the next stage a document for each distinct grouping. The output documents contain an _id field which contains the distinct group by key. The output documents can also contain computed fields that hold the values of some accumulator expression grouped by the $group‘s _id field. $group does not order its output documents. 277 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group 278 | SchemaPipeline group(Bson id, Bson accumulators) 279 | { 280 | assert(!finalized); 281 | accumulators["_id"] = id; 282 | pipeline ~= Bson(["$group": accumulators]); 283 | return this; 284 | } 285 | 286 | /// ditto 287 | SchemaPipeline group(K, T)(K id, T[string] accumulators) 288 | { 289 | return group(serializeToBson(id), serializeToBson(accumulators)); 290 | } 291 | 292 | /// Groups all documents into one specified with the accumulators. Basically just runs group(null, accumulators) 293 | SchemaPipeline groupAll(Bson accumulators) 294 | { 295 | assert(!finalized); 296 | accumulators["_id"] = Bson(null); 297 | pipeline ~= Bson(["$group": accumulators]); 298 | return this; 299 | } 300 | 301 | /// ditto 302 | SchemaPipeline groupAll(T)(T[string] accumulators) 303 | { 304 | return groupAll(serializeToBson(accumulators)); 305 | } 306 | 307 | /// Randomly selects the specified number of documents from its input. 308 | /// Warning: $sample may output the same document more than once in its result set. 309 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sample/#pipe._S_sample 310 | SchemaPipeline sample(size_t count) 311 | { 312 | assert(!finalized); 313 | pipeline ~= Bson(["$sample": Bson(count)]); 314 | return this; 315 | } 316 | 317 | /// Sorts all input documents and returns them to the pipeline in sorted order. 318 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort 319 | SchemaPipeline sort(Bson sorter) 320 | { 321 | assert(!finalized); 322 | pipeline ~= Bson(["$sort": sorter]); 323 | return this; 324 | } 325 | 326 | /// ditto 327 | SchemaPipeline sort(T)(T sorter) 328 | { 329 | return sort(serializeToBson(sorter)); 330 | } 331 | 332 | /// Outputs documents in order of nearest to farthest from a specified point. 333 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear 334 | SchemaPipeline geoNear(Bson options) 335 | { 336 | assert(!finalized); 337 | pipeline ~= Bson(["$geoNear": options]); 338 | return this; 339 | } 340 | 341 | /// ditto 342 | SchemaPipeline geoNear(T)(T options) 343 | { 344 | return geoNear(serializeToBson(options)); 345 | } 346 | 347 | /// Performs a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing. The $lookup stage does an equality match between a field from the input documents with a field from the documents of the “joined” collection. 348 | /// To each input document, the $lookup stage adds a new array field whose elements are the matching documents from the “joined” collection. The $lookup stage passes these reshaped documents to the next stage. 349 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup 350 | SchemaPipeline lookup(Bson options) 351 | { 352 | assert(!finalized); 353 | pipeline ~= Bson(["$lookup": options]); 354 | return this; 355 | } 356 | 357 | /// ditto 358 | SchemaPipeline lookup(T)(T options) 359 | { 360 | return lookup(serializeToBson(options)); 361 | } 362 | 363 | /// Takes the documents returned by the aggregation pipeline and writes them to a specified collection. The $out operator must be the last stage in the pipeline. The $out operator lets the aggregation framework return result sets of any size. 364 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out 365 | SchemaPipeline outputTo(string outputCollection) 366 | { 367 | assert(!finalized); 368 | debug finalized = true; 369 | pipeline ~= Bson(["$out": Bson(outputCollection)]); 370 | return this; 371 | } 372 | 373 | /// Returns statistics regarding the use of each index for the collection. If running with access control, the user must have privileges that include indexStats action. 374 | /// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/#pipe._S_indexStats 375 | SchemaPipeline indexStats() 376 | { 377 | assert(!finalized); 378 | pipeline ~= Bson(["$indexStats": Bson.emptyObject]); 379 | return this; 380 | } 381 | 382 | Bson run() @trusted // workaround because old vibe.d versions mistagged aggregate safety 383 | { 384 | debug finalized = true; 385 | return _collection.aggregate(pipeline); 386 | } 387 | 388 | DocumentRange!T collect(T = Bson)(AggregateOptions options = AggregateOptions.init) 389 | { 390 | debug finalized = true; 391 | return _collection.aggregate!T(pipeline, options); 392 | } 393 | 394 | private: 395 | bool finalized = false; 396 | Bson[] pipeline; 397 | MongoCollection _collection; 398 | } 399 | 400 | /// Mixin for functions for interacting with Mongo collections. 401 | mixin template MongoSchema() 402 | { 403 | import vibe.db.mongo.collection; 404 | 405 | import std.typecons : Nullable; 406 | import std.range : isInputRange, ElementType; 407 | 408 | static MongoCollection _schema_collection_; 409 | private BsonObjectID _schema_object_id_; 410 | 411 | @property static MongoCollection collection() @safe 412 | { 413 | return _schema_collection_; 414 | } 415 | 416 | /// Returns: the _id value (if set by save or find) 417 | @property ref BsonObjectID bsonID() @safe return 418 | { 419 | return _schema_object_id_; 420 | } 421 | 422 | /// ditto 423 | @property const(BsonObjectID) bsonID() const @safe 424 | { 425 | return _schema_object_id_; 426 | } 427 | 428 | /// Inserts or updates an existing value. 429 | void save() 430 | { 431 | if (_schema_object_id_.valid) 432 | { 433 | UpdateOptions options; 434 | options.upsert = true; 435 | collection.replaceOne(Bson(["_id": Bson(_schema_object_id_)]), 436 | this.toSchemaBson(), options); 437 | } 438 | else 439 | { 440 | _schema_object_id_ = BsonObjectID.generate; 441 | auto bson = this.toSchemaBson(); 442 | collection.insertOne(bson); 443 | } 444 | } 445 | 446 | /// Inserts or merges into an existing value. 447 | void merge() 448 | { 449 | if (_schema_object_id_.valid) 450 | { 451 | UpdateOptions options; 452 | options.upsert = true; 453 | collection.updateOne(Bson(["_id": Bson(_schema_object_id_)]), 454 | Bson(["$set": this.toSchemaBson()]), options); 455 | } 456 | else 457 | { 458 | _schema_object_id_ = BsonObjectID.generate; 459 | auto bson = this.toSchemaBson(); 460 | collection.insertOne(bson); 461 | } 462 | } 463 | 464 | /// Removes this object from the collection. Returns false when _id of this is not set. 465 | bool remove() @trusted const 466 | { 467 | if (!_schema_object_id_.valid) 468 | return false; 469 | collection.deleteOne(Bson(["_id": Bson(_schema_object_id_)])); 470 | return true; 471 | } 472 | 473 | /// Tries to find one document in the collection. 474 | /// Throws: DocumentNotFoundException if not found 475 | static Bson findOneOrThrow(Query!(typeof(this)) query) 476 | { 477 | return findOneOrThrow(query._query); 478 | } 479 | 480 | /// ditto 481 | static Bson findOneOrThrow(T)(T query) 482 | { 483 | Bson found = collection.findOne(query); 484 | if (found.isNull) 485 | throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof); 486 | return found; 487 | } 488 | 489 | /// Finds one element with the object id `id`. 490 | /// Throws: DocumentNotFoundException if not found 491 | static typeof(this) findById(BsonObjectID id) 492 | { 493 | return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id": Bson(id)]))); 494 | } 495 | 496 | /// Finds one element with the hex id `id`. 497 | /// Throws: DocumentNotFoundException if not found 498 | static typeof(this) findById(string id) 499 | { 500 | return findById(BsonObjectID.fromString(id)); 501 | } 502 | 503 | /// Finds one element using a query. 504 | /// Throws: DocumentNotFoundException if not found 505 | static typeof(this) findOne(Query!(typeof(this)) query) 506 | { 507 | return fromSchemaBson!(typeof(this))(findOneOrThrow(query)); 508 | } 509 | 510 | /// ditto 511 | static typeof(this) findOne(T)(T query) 512 | { 513 | return fromSchemaBson!(typeof(this))(findOneOrThrow(query)); 514 | } 515 | 516 | /// Tries to find a document by the _id field and returns a Nullable which `isNull` if it could not be found. Otherwise it will be the document wrapped in the nullable. 517 | static Nullable!(typeof(this)) tryFindById(BsonObjectID id) 518 | { 519 | Bson found = collection.findOne(Bson(["_id": Bson(id)])); 520 | if (found.isNull) 521 | return Nullable!(typeof(this)).init; 522 | return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found)); 523 | } 524 | 525 | /// ditto 526 | static Nullable!(typeof(this)) tryFindById(string id) 527 | { 528 | return tryFindById(BsonObjectID.fromString(id)); 529 | } 530 | 531 | /// Tries to find a document in this collection. It will return a Nullable which `isNull` if the document could not be found. Otherwise it will be the document wrapped in the nullable. 532 | static Nullable!(typeof(this)) tryFindOne(Query!(typeof(this)) query) 533 | { 534 | return tryFindOne(query._query); 535 | } 536 | 537 | /// ditto 538 | static Nullable!(typeof(this)) tryFindOne(T)(T query) 539 | { 540 | Bson found = collection.findOne(query); 541 | if (found.isNull) 542 | return Nullable!(typeof(this)).init; 543 | return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found)); 544 | } 545 | 546 | /// Tries to find a document by the _id field and returns a default value if it could not be found. 547 | static typeof(this) tryFindById(BsonObjectID id, typeof(this) defaultValue) 548 | { 549 | Bson found = collection.findOne(Bson(["_id": Bson(id)])); 550 | if (found.isNull) 551 | return defaultValue; 552 | return fromSchemaBson!(typeof(this))(found); 553 | } 554 | 555 | /// ditto 556 | static typeof(this) tryFindById(string id, typeof(this) defaultValue) 557 | { 558 | return tryFindById(BsonObjectID.fromString(id), defaultValue); 559 | } 560 | 561 | /// Tries to find a document in this collection. It will return a default value if the document could not be found. 562 | static typeof(this) tryFindOne(Query!(typeof(this)) query, typeof(this) defaultValue) 563 | { 564 | return tryFindOne(query._query, defaultValue); 565 | } 566 | 567 | /// ditto 568 | static typeof(this) tryFindOne(T)(T query, typeof(this) defaultValue) 569 | { 570 | Bson found = collection.findOne(query); 571 | if (found.isNull) 572 | return defaultValue; 573 | return fromSchemaBson!(typeof(this))(found); 574 | } 575 | 576 | /// Finds one or more elements using a query. 577 | static typeof(this)[] find(Query!(typeof(this)) query, FindOptions options = FindOptions.init) 578 | { 579 | return find(query._query, options); 580 | } 581 | 582 | /// ditto 583 | static typeof(this)[] find(T)(T query, FindOptions options = FindOptions.init) 584 | { 585 | typeof(this)[] values; 586 | foreach (entry; collection.find(query, options)) 587 | { 588 | values ~= fromSchemaBson!(typeof(this))(entry); 589 | } 590 | return values; 591 | } 592 | 593 | /// Finds one or more elements using a query as range. 594 | static DocumentRange!(typeof(this)) findRange(Query!(typeof(this)) query, FindOptions options = FindOptions.init) 595 | { 596 | return findRange(query._query, options); 597 | } 598 | 599 | /// ditto 600 | static DocumentRange!(typeof(this)) findRange(T)(T query, FindOptions options = FindOptions.init) 601 | { 602 | return DocumentRange!(typeof(this))(collection.find(serializeToBson(query), options)); 603 | } 604 | 605 | /// Queries all elements from the collection as range. 606 | static DocumentRange!(typeof(this)) findAll() 607 | { 608 | return DocumentRange!(typeof(this))(collection.find()); 609 | } 610 | 611 | /// Inserts many documents at once. The resulting IDs of the symbols will be generated by the server and not known to the caller. 612 | static void insertMany(T)(T documents, InsertManyOptions options = InsertManyOptions.init) 613 | if (isInputRange!T && is(ElementType!T : typeof(this))) 614 | { 615 | import std.array : array; 616 | import std.algorithm : map; 617 | 618 | if (documents.empty) 619 | return; 620 | collection.insertMany(documents.map!((a) { 621 | a.bsonID = BsonObjectID.init; 622 | return a.toSchemaBson; 623 | }).array, options); // .array needed because of vibe-d issue #2185 624 | } 625 | 626 | /// Updates a document with `$dollarOperations`. 627 | static void updateOne(U)(Query!(typeof(this)) query, U update, UpdateOptions options = UpdateOptions.init) 628 | { 629 | updateOne(query._query, update, options); 630 | } 631 | 632 | /// ditto 633 | static void updateOne(T, U)(T query, U update, UpdateOptions options = UpdateOptions.init) 634 | { 635 | collection.updateOne(query, update, options); 636 | } 637 | 638 | /// Updates any amount of documents. 639 | static void updateMany(U)(Query!(typeof(this)) query, U update, UpdateOptions options = UpdateOptions.init) 640 | { 641 | updateMany(query._query, update, options); 642 | } 643 | 644 | /// ditto 645 | static void updateMany(T, U)(T query, U update, UpdateOptions options = UpdateOptions.init) 646 | { 647 | collection.updateMany(query, update, options); 648 | } 649 | 650 | /// Updates a document or inserts it when not existent. Calls `replaceOne` with `options.upsert` set to true. 651 | static void upsert(U)(Query!(typeof(this)) query, U upsert, UpdateOptions options = UpdateOptions.init) 652 | { 653 | options.upsert = true; 654 | upsert(query._query, upsert, options); 655 | } 656 | 657 | /// ditto 658 | static void upsert(T, U)(T query, U update, UpdateOptions options = UpdateOptions.init) 659 | { 660 | options.upsert = true; 661 | collection.replaceOne(query, update, options); 662 | } 663 | 664 | /// Deletes one or any amount of documents matching the selector based on the options. 665 | static void remove(Query!(typeof(this)) query, DeleteOptions options = DeleteOptions.init) 666 | { 667 | remove(query._query, options); 668 | } 669 | 670 | /// ditto 671 | static void remove(T)(T selector, DeleteOptions options = DeleteOptions.init) 672 | { 673 | collection.deleteMany(selector, options); 674 | } 675 | 676 | /// Removes all documents from this collection. 677 | static void removeAll(DeleteOptions options = DeleteOptions.init) 678 | { 679 | collection.deleteAll(options); 680 | } 681 | 682 | /// Drops the entire collection and all indices in the database. 683 | static void dropTable() 684 | { 685 | collection.drop(); 686 | } 687 | 688 | /// Returns the count of documents in this collection matching this query. 689 | static auto count(Query!(typeof(this)) query) 690 | { 691 | return count(query._query); 692 | } 693 | 694 | /// ditto 695 | static auto count(T)(T query) 696 | { 697 | return collection.countDocuments(query); 698 | } 699 | 700 | /// Returns the count of documents in this collection. 701 | static auto countAll() 702 | { 703 | import vibe.data.bson : Bson; 704 | 705 | return collection.countDocuments(Bson.emptyObject); 706 | } 707 | 708 | /// Start of an aggregation call. Returns a pipeline with typesafe functions for modifying the pipeline and running it at the end. 709 | /// Examples: 710 | /// -------------------- 711 | /// auto groupResults = Book.aggregate.groupAll([ 712 | /// "totalPrice": Bson([ 713 | /// "$sum": Bson([ 714 | /// "$multiply": Bson([Bson("$price"), Bson("$quantity")]) 715 | /// ]) 716 | /// ]), 717 | /// "averageQuantity": Bson([ 718 | /// "$avg": Bson("$quantity") 719 | /// ]), 720 | /// "count": Bson(["$sum": Bson(1)]) 721 | /// ]).run; 722 | /// -------------------- 723 | static SchemaPipeline aggregate() 724 | { 725 | return SchemaPipeline(collection); 726 | } 727 | } 728 | 729 | /// Binds a MongoCollection to a Schema. Can only be done once! 730 | void register(T)(MongoCollection collection) @safe 731 | { 732 | T obj = T.init; 733 | 734 | static if (hasMember!(T, "_schema_collection_")) 735 | { 736 | (() @trusted { 737 | assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!"); 738 | T._schema_collection_ = collection; 739 | })(); 740 | } 741 | 742 | static foreach (memberName; getSerializableMembers!obj) 743 | { 744 | { 745 | alias member = __traits(getMember, obj, memberName); 746 | 747 | string name = memberName; 748 | static if (hasUDA!(member, schemaName)) 749 | { 750 | static assert(getUDAs!(member, schemaName) 751 | .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 752 | name = getUDAs!(member, schemaName)[0].name; 753 | } 754 | 755 | ulong expires = 0LU; 756 | bool force; 757 | 758 | static if (hasUDA!(member, mongoForceIndex)) 759 | { 760 | force = true; 761 | } 762 | static if (hasUDA!(member, mongoExpire)) 763 | { 764 | static assert(getUDAs!(member, mongoExpire) 765 | .length == 1, "Member '" ~ memberName ~ "' can only have one expiry value!"); 766 | expires = getUDAs!(member, mongoExpire)[0].seconds; 767 | } 768 | 769 | 770 | IndexOptions indexOptions; 771 | static if (hasUDA!(member, mongoBackground)) 772 | { 773 | indexOptions.background = true; 774 | } 775 | static if (hasUDA!(member, mongoDropDuplicates)) 776 | { 777 | indexOptions.dropDups = true; 778 | } 779 | static if (hasUDA!(member, mongoSparse)) 780 | { 781 | indexOptions.sparse = true; 782 | } 783 | static if (hasUDA!(member, mongoUnique)) 784 | { 785 | indexOptions.unique = true; 786 | } 787 | static if (hasUDA!(member, mongoExpire)) 788 | { 789 | indexOptions.expireAfterSeconds = cast(int)expires; 790 | } 791 | 792 | if (indexOptions != indexOptions.init) 793 | collection.createIndex([name: 1], indexOptions); 794 | } 795 | } 796 | } 797 | 798 | unittest 799 | { 800 | struct C 801 | { 802 | int a = 4; 803 | } 804 | 805 | struct B 806 | { 807 | C cref; 808 | } 809 | 810 | struct A 811 | { 812 | B bref; 813 | } 814 | 815 | A a; 816 | a.bref.cref.a = 5; 817 | auto bson = a.toSchemaBson(); 818 | assert(bson["bref"]["cref"]["a"].get!int == 5); 819 | A b = bson.fromSchemaBson!A(); 820 | assert(b.bref.cref.a == 5); 821 | } 822 | 823 | unittest 824 | { 825 | static if (__VERSION__ >= 2076) 826 | import std.digest; 827 | else 828 | import std.digest.digest; 829 | import std.digest.sha; 830 | import std.datetime.systime; 831 | 832 | enum Activity 833 | { 834 | High, 835 | Medium, 836 | Low 837 | } 838 | 839 | enum Permission 840 | { 841 | A = 1, 842 | B = 2, 843 | C = 4 844 | } 845 | 846 | struct UserSchema 847 | { 848 | string username = "Unnamed"; 849 | @binaryType() 850 | string salt = "foobar"; 851 | @encode("encodePassword") 852 | @binaryType() 853 | string password; 854 | @schemaName("date-created") 855 | SchemaDate dateCreated = SchemaDate.now; 856 | SysTime otherDate = BsonDate(1000000000).toSysTime(); 857 | Activity activity = Activity.Medium; 858 | BitFlags!Permission permissions; 859 | Tuple!(string, string) name; 860 | ubyte x0; 861 | byte x1; 862 | ushort x2; 863 | short x3; 864 | uint x4; 865 | int x5; 866 | ulong x6; 867 | long x7; 868 | float x8; 869 | double x9; 870 | int[10] token; 871 | 872 | Bson encodePassword(UserSchema user) 873 | { 874 | // TODO: Replace with something more secure 875 | return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt))); 876 | } 877 | } 878 | 879 | auto user = UserSchema(); 880 | user.password = "12345"; 881 | user.username = "Bob"; 882 | user.permissions = Permission.A | Permission.C; 883 | user.name = tuple("Bob", "Bobby"); 884 | user.x0 = 7; 885 | user.x1 = 7; 886 | user.x2 = 7; 887 | user.x3 = 7; 888 | user.x4 = 7; 889 | user.x5 = 7; 890 | user.x6 = 7; 891 | user.x7 = 7; 892 | user.x8 = 7; 893 | user.x9 = 7; 894 | user.token[3] = 8; 895 | auto bson = user.toSchemaBson(); 896 | assert(bson["username"].get!string == "Bob"); 897 | assert(bson["date-created"].get!(BsonDate).value > 0); 898 | assert(bson["otherDate"].get!(BsonDate).value == 1000000000); 899 | assert(bson["activity"].get!(int) == cast(int) Activity.Medium); 900 | assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar"); 901 | assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt)); 902 | assert(bson["permissions"].get!(int) == 5); 903 | assert(bson["name"].get!(Bson[]).length == 2); 904 | assert(bson["x0"].get!int == 7); 905 | assert(bson["x1"].get!int == 7); 906 | assert(bson["x2"].get!int == 7); 907 | assert(bson["x3"].get!int == 7); 908 | assert(bson["x4"].get!int == 7); 909 | assert(bson["x5"].get!int == 7); 910 | assert(bson["x6"].get!long == 7); 911 | assert(bson["x7"].get!long == 7); 912 | assert(bson["x8"].get!double == 7); 913 | assert(bson["x9"].get!double == 7); 914 | assert(bson["token"].get!(Bson[]).length == 10); 915 | assert(bson["token"].get!(Bson[])[3].get!int == 8); 916 | 917 | auto user2 = bson.fromSchemaBson!UserSchema(); 918 | assert(user2.username == user.username); 919 | assert(user2.password != user.password); 920 | assert(user2.salt == user.salt); 921 | // dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time 922 | assert(user2.dateCreated != user.dateCreated); 923 | assert(user2.otherDate.stdTime == user.otherDate.stdTime); 924 | assert(user2.activity == user.activity); 925 | assert(user2.permissions == user.permissions); 926 | assert(user2.name == user.name); 927 | assert(user2.x0 == user.x0); 928 | assert(user2.x1 == user.x1); 929 | assert(user2.x2 == user.x2); 930 | assert(user2.x3 == user.x3); 931 | assert(user2.x4 == user.x4); 932 | assert(user2.x5 == user.x5); 933 | assert(user2.x6 == user.x6); 934 | assert(user2.x7 == user.x7); 935 | assert(user2.x8 == user.x8); 936 | assert(user2.x9 == user.x9); 937 | assert(user2.token == user.token); 938 | } 939 | 940 | unittest 941 | { 942 | struct Private 943 | { 944 | private: 945 | int x; 946 | } 947 | 948 | struct Something 949 | { 950 | Private p; 951 | } 952 | 953 | struct Public 954 | { 955 | int y; 956 | } 957 | 958 | struct Something2 959 | { 960 | Public p; 961 | } 962 | 963 | struct Something3 964 | { 965 | int y; 966 | @schemaIgnore Private p; 967 | } 968 | 969 | struct SerializablePrivate 970 | { 971 | static Bson toBson(SerializablePrivate p) 972 | { 973 | return Bson(1); 974 | } 975 | 976 | static SerializablePrivate fromBson(Bson bson) 977 | { 978 | return SerializablePrivate.init; 979 | } 980 | } 981 | 982 | struct Something4 983 | { 984 | SerializablePrivate p; 985 | } 986 | 987 | Something s; 988 | Something2 s2; 989 | Something3 s3; 990 | Something4 s4; 991 | static assert(__traits(compiles, { s2.toSchemaBson(); })); 992 | static assert(!__traits(compiles, { s.toSchemaBson(); }), 993 | "Private members are not serializable, so Private is empty and may not be used as type."); 994 | static assert(__traits(compiles, { s3.toSchemaBson(); }), 995 | "When adding schemaIgnore, empty structs must be ignored"); 996 | static assert(__traits(compiles, { s4.toSchemaBson(); }), 997 | "Empty structs with custom (de)serialization must be ignored"); 998 | } 999 | 1000 | unittest 1001 | { 1002 | import vibe.db.mongo.mongo; 1003 | import std.digest.sha; 1004 | import std.exception; 1005 | import std.array; 1006 | 1007 | auto client = connectMongoDB("127.0.0.1"); 1008 | auto database = client.getDatabase("test"); 1009 | MongoCollection users = database["users"]; 1010 | users.deleteAll(); // Clears collection 1011 | 1012 | struct User 1013 | { 1014 | mixin MongoSchema; 1015 | 1016 | @mongoExpire(30) @mongoUnique string username; 1017 | @binaryType() 1018 | ubyte[] hash; 1019 | @schemaName("profile-picture") 1020 | string profilePicture; 1021 | auto registered = SchemaDate.now; 1022 | } 1023 | 1024 | users.register!User; 1025 | 1026 | assert(User.findAll().array.length == 0); 1027 | 1028 | User user; 1029 | user.username = "Example"; 1030 | user.hash = sha512Of("password123").dup; 1031 | user.profilePicture = "example-avatar.png"; 1032 | 1033 | assertNotThrown(user.save()); 1034 | 1035 | User user2; 1036 | user2.username = "Bob"; 1037 | user2.hash = sha512Of("foobar").dup; 1038 | user2.profilePicture = "bob-avatar.png"; 1039 | 1040 | assertNotThrown(user2.save()); 1041 | 1042 | User faker; 1043 | faker.username = "Example"; 1044 | faker.hash = sha512Of("PASSWORD").dup; 1045 | faker.profilePicture = "faker-avatar.png"; 1046 | 1047 | // Unique username 1048 | assertThrown(faker.save()); 1049 | 1050 | faker.username = "Example_"; 1051 | assertNotThrown(faker.save()); 1052 | 1053 | user.username = "NewExample"; 1054 | user.save(); 1055 | 1056 | auto actualFakeID = faker.bsonID; 1057 | faker = User.findOne(["username": "NewExample"]); 1058 | 1059 | assert(actualFakeID != faker.bsonID); 1060 | 1061 | foreach (usr; User.findAll) 1062 | { 1063 | usr.profilePicture = "default.png"; // Reset all profile pictures 1064 | usr.save(); 1065 | } 1066 | user = User.findOne(["username": "NewExample"]); 1067 | user2 = User.findOne(["username": "Bob"]); 1068 | faker = User.findOne(["username": "Example_"]); 1069 | assert(user.profilePicture == user2.profilePicture 1070 | && user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png"); 1071 | 1072 | User user3; 1073 | user3.username = "User123"; 1074 | user3.hash = sha512Of("486951").dup; 1075 | user3.profilePicture = "new.png"; 1076 | User.upsert(["username": "User123"], user3.toSchemaBson); 1077 | user3 = User.findOne(["username": "User123"]); 1078 | assert(user3.hash == sha512Of("486951")); 1079 | assert(user3.profilePicture == "new.png"); 1080 | } 1081 | 1082 | unittest 1083 | { 1084 | import vibe.db.mongo.mongo; 1085 | import mongoschema.aliases : name, ignore, unique, binary; 1086 | import std.digest.sha; 1087 | import std.digest.md; 1088 | 1089 | auto client = connectMongoDB("127.0.0.1"); 1090 | 1091 | struct Permission 1092 | { 1093 | string name; 1094 | int priority; 1095 | } 1096 | 1097 | struct User 1098 | { 1099 | mixin MongoSchema; 1100 | 1101 | @unique string username; 1102 | 1103 | @binary() 1104 | ubyte[] hash; 1105 | @binary() 1106 | ubyte[] salt; 1107 | 1108 | @name("profile-picture") 1109 | string profilePicture = "default.png"; 1110 | 1111 | Permission[] permissions; 1112 | 1113 | @ignore: 1114 | int sessionID; 1115 | } 1116 | 1117 | auto coll = client.getCollection("test.users2"); 1118 | coll.deleteAll(); 1119 | coll.register!User; 1120 | 1121 | User register(string name, string password) 1122 | { 1123 | User user; 1124 | user.username = name; 1125 | user.salt = md5Of(name).dup; 1126 | user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup; 1127 | user.permissions ~= Permission("forum.access", 1); 1128 | user.save(); 1129 | return user; 1130 | } 1131 | 1132 | User find(string name) 1133 | { 1134 | return User.findOne(["username": name]); 1135 | } 1136 | 1137 | User a = register("foo", "bar"); 1138 | User b = find("foo"); 1139 | assert(a == b); 1140 | } 1141 | -------------------------------------------------------------------------------- /source/mongoschema/package.d: -------------------------------------------------------------------------------- 1 | module mongoschema; 2 | 3 | import core.time; 4 | import std.array : appender; 5 | import std.conv; 6 | import std.datetime.systime; 7 | import std.traits; 8 | import std.typecons : BitFlags, isTuple; 9 | public import vibe.data.bson; 10 | public import vibe.db.mongo.collection; 11 | public import vibe.db.mongo.connection; 12 | 13 | public import mongoschema.date; 14 | public import mongoschema.db; 15 | public import mongoschema.query; 16 | public import mongoschema.variant; 17 | 18 | // Bson Attributes 19 | 20 | /// Will ignore the variables and not encode/decode them. 21 | enum schemaIgnore; 22 | /// Custom encode function. `func` is the name of the function which must be present as child. 23 | struct encode 24 | { /++ Function name (needs to be member function) +/ string func; 25 | } 26 | /// Custom decode function. `func` is the name of the function which must be present as child. 27 | struct decode 28 | { /++ Function name (needs to be member function) +/ string func; 29 | } 30 | /// Encodes the value as binary value. Must be an array with one byte wide elements. 31 | struct binaryType 32 | { /++ Type to encode +/ BsonBinData.Type type = BsonBinData.Type.generic; 33 | } 34 | /// Custom name for special characters. 35 | struct schemaName 36 | { /++ Custom replacement name +/ string name; 37 | } 38 | 39 | // Mongo Attributes 40 | /// Will create an index with (by default) no flags. 41 | enum mongoForceIndex; 42 | /// Background index construction allows read and write operations to continue while building the index. 43 | enum mongoBackground; 44 | /// Drops duplicates in the database. Only for Mongo versions less than 3.0 45 | enum mongoDropDuplicates; 46 | /// Sparse indexes are like non-sparse indexes, except that they omit references to documents that do not include the indexed field. 47 | enum mongoSparse; 48 | /// MongoDB allows you to specify a unique constraint on an index. These constraints prevent applications from inserting documents that have duplicate values for the inserted fields. 49 | enum mongoUnique; 50 | /// TTL indexes expire documents after the specified number of seconds has passed since the indexed field value; i.e. the expiration threshold is the indexed field value plus the specified number of seconds. 51 | /// Field must be a SchemaDate/BsonDate. You must update the time using collMod. 52 | struct mongoExpire 53 | { 54 | /// 55 | this(int seconds) 56 | { 57 | this.seconds = cast(ulong) seconds; 58 | } 59 | /// 60 | this(long seconds) 61 | { 62 | this.seconds = cast(ulong) seconds; 63 | } 64 | /// 65 | this(ulong seconds) 66 | { 67 | this.seconds = seconds; 68 | } 69 | /// 70 | this(Duration time) 71 | { 72 | seconds = cast(ulong) time.total!"msecs"; 73 | } 74 | /// 75 | ulong seconds; 76 | } 77 | 78 | package template isVariable(alias T) 79 | { 80 | enum isVariable = !is(T) && is(typeof(T)) && !isCallable!T && !is(T == void) 81 | && !__traits(isStaticFunction, T) && !__traits(isOverrideFunction, T) && !__traits(isFinalFunction, 82 | T) && !__traits(isAbstractFunction, T) && !__traits(isVirtualFunction, 83 | T) && !__traits(isVirtualMethod, T) && !is(ReturnType!T); 84 | } 85 | 86 | package template isVariable(T) 87 | { 88 | enum isVariable = false; // Types are no variables 89 | } 90 | 91 | /// Converts any value to a bson value 92 | Bson memberToBson(T)(T member) 93 | { 94 | static if (__traits(hasMember, T, "toBson") && is(ReturnType!(typeof(T.toBson)) == Bson)) 95 | { 96 | // Custom defined toBson 97 | return T.toBson(member); 98 | } 99 | else static if (is(T == Json)) 100 | { 101 | return Bson.fromJson(member); 102 | } 103 | else static if (is(T == BsonBinData) || is(T == BsonObjectID) 104 | || is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex) || is(T == typeof(null))) 105 | { 106 | return Bson(member); 107 | } 108 | else static if (is(T == SysTime)) 109 | { 110 | return Bson(BsonDate(member)); 111 | } 112 | else static if (is(T == enum)) 113 | { // Enum value 114 | return Bson(cast(OriginalType!T) member); 115 | } 116 | else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe)) 117 | { // std.typecons.BitFlags 118 | return Bson(cast(OriginalType!Enum) member); 119 | } 120 | else static if (isArray!(T) && !isSomeString!T || isTuple!T) 121 | { // Arrays of anything except strings 122 | Bson[] values; 123 | foreach (val; member) 124 | values ~= memberToBson(val); 125 | return Bson(values); 126 | } 127 | else static if (isAssociativeArray!T) 128 | { // Associative Arrays (Objects) 129 | Bson[string] values; 130 | static assert(is(KeyType!T == string), "Associative arrays must have strings as keys"); 131 | foreach (string name, val; member) 132 | values[name] = memberToBson(val); 133 | return Bson(values); 134 | } 135 | else static if (is(T == Bson)) 136 | { // Already a Bson object 137 | return member; 138 | } 139 | else static if (__traits(compiles, { Bson(member); })) 140 | { // Check if this can be passed 141 | return Bson(member); 142 | } 143 | else static if (!isBasicType!T) 144 | { 145 | // Mixed in MongoSchema 146 | return member.toSchemaBson(); 147 | } 148 | else // Generic value 149 | { 150 | pragma(msg, "Warning falling back to serializeToBson for type " ~ T.stringof); 151 | return serializeToBson(member); 152 | } 153 | } 154 | 155 | /// Converts any bson value to a given type 156 | T bsonToMember(T)(auto ref T member, Bson value) 157 | { 158 | static if (__traits(hasMember, T, "fromBson") && is(ReturnType!(typeof(T.fromBson)) == T)) 159 | { 160 | // Custom defined toBson 161 | return T.fromBson(value); 162 | } 163 | else static if (is(T == Json)) 164 | { 165 | return Bson.fromJson(value); 166 | } 167 | else static if (is(T == BsonBinData) || is(T == BsonObjectID) 168 | || is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex)) 169 | { 170 | return value.get!T; 171 | } 172 | else static if (is(T == SysTime)) 173 | { 174 | return value.get!BsonDate.toSysTime(); 175 | } 176 | else static if (is(T == enum)) 177 | { // Enum value 178 | return cast(T) value.get!(OriginalType!T); 179 | } 180 | else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe)) 181 | { // std.typecons.BitFlags 182 | return cast(T) cast(Enum) value.get!(OriginalType!Enum); 183 | } 184 | else static if (isTuple!T) 185 | { // Tuples 186 | auto bsons = value.get!(Bson[]); 187 | T values; 188 | foreach (i, val; values) 189 | values[i] = bsonToMember!(typeof(val))(values[i], bsons[i]); 190 | return values; 191 | } 192 | else static if (isDynamicArray!T && !isSomeString!T) 193 | { // Arrays of anything except strings 194 | alias Type = typeof(member[0]); 195 | if (value.type != Bson.Type.array) 196 | throw new Exception("Cannot convert from BSON type " ~ value.type.toString ~ " to array"); 197 | auto arr = value.get!(Bson[]); 198 | auto ret = appender!T(); 199 | ret.reserve(arr.length); 200 | foreach (val; arr) 201 | ret.put(bsonToMember!Type(Type.init, val)); 202 | return ret.data; 203 | } 204 | else static if (isStaticArray!T) 205 | { // Arrays of anything except strings 206 | alias Type = typeof(member[0]); 207 | T values; 208 | if (value.type != Bson.Type.array) 209 | throw new Exception("Cannot convert from BSON type " ~ value.type.toString ~ " to array"); 210 | auto arr = value.get!(Bson[]); 211 | if (arr.length != values.length) 212 | throw new Exception("Cannot convert from BSON array of length " 213 | ~ arr.length.to!string ~ " to array of length " ~ arr.length.to!string); 214 | foreach (i, val; arr) 215 | values[i] = bsonToMember!Type(Type.init, val); 216 | return values; 217 | } 218 | else static if (isAssociativeArray!T) 219 | { // Associative Arrays (Objects) 220 | T values; 221 | static assert(is(KeyType!T == string), "Associative arrays must have strings as keys"); 222 | alias ValType = ValueType!T; 223 | foreach (string name, val; value) 224 | values[name] = bsonToMember!ValType(ValType.init, val); 225 | return values; 226 | } 227 | else static if (is(T == Bson)) 228 | { // Already a Bson object 229 | return value; 230 | } 231 | else static if (isNumeric!T) 232 | { 233 | if (value.type == Bson.Type.int_) 234 | return cast(T) value.get!int; 235 | else if (value.type == Bson.Type.long_) 236 | return cast(T) value.get!long; 237 | else if (value.type == Bson.Type.double_) 238 | return cast(T) value.get!double; 239 | else 240 | throw new Exception( 241 | "Cannot convert BSON from type " ~ value.type.toString ~ " to " ~ T.stringof); 242 | } 243 | else static if (__traits(compiles, { value.get!T(); })) 244 | { 245 | return value.get!T(); 246 | } 247 | else static if (!isBasicType!T) 248 | { 249 | // Mixed in MongoSchema 250 | return value.fromSchemaBson!T(); 251 | } 252 | else // Generic value 253 | { 254 | pragma(msg, "Warning falling back to deserializeBson for type " ~ T.stringof); 255 | return deserializeBson!T(value); 256 | } 257 | } 258 | 259 | // our own function instead of std.conv because it prints way too many deprecations 260 | private string toString(Bson.Type type) 261 | { 262 | final switch (type) 263 | { 264 | case Bson.Type.end: return "end"; 265 | case Bson.Type.double_: return "double"; 266 | case Bson.Type.string: return "string"; 267 | case Bson.Type.object: return "object"; 268 | case Bson.Type.array: return "array"; 269 | case Bson.Type.binData: return "binData"; 270 | case Bson.Type.undefined: return "undefined"; 271 | case Bson.Type.objectID: return "objectID"; 272 | case Bson.Type.bool_: return "bool"; 273 | case Bson.Type.date: return "date"; 274 | case Bson.Type.null_: return "null"; 275 | case Bson.Type.regex: return "regex"; 276 | case Bson.Type.dbRef: return "dbRef"; 277 | case Bson.Type.code: return "code"; 278 | case Bson.Type.symbol: return "symbol"; 279 | case Bson.Type.codeWScope: return "codeWScope"; 280 | case Bson.Type.int_: return "int"; 281 | case Bson.Type.timestamp: return "timestamp"; 282 | case Bson.Type.long_: return "long"; 283 | case Bson.Type.minKey: return "minKey"; 284 | case Bson.Type.maxKey: return "maxKey"; 285 | } 286 | } 287 | 288 | string[] getSerializableMembers(alias obj)() 289 | { 290 | alias T = typeof(obj); 291 | string[] ret; 292 | foreach (memberName; __traits(allMembers, T)) 293 | { 294 | static if (memberName == "_schema_object_id_") 295 | continue; 296 | else static if (__traits(compiles, { 297 | static s = isVariable!(__traits(getMember, obj, memberName)); 298 | }) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, { 299 | static s = __traits(getMember, T, memberName); 300 | }) // No static members 301 | && __traits(compiles, { 302 | typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, obj, memberName); 303 | })) 304 | { 305 | static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 306 | { 307 | string name = memberName; 308 | Bson value; 309 | static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 310 | { 311 | ret ~= memberName; 312 | } 313 | } 314 | } 315 | } 316 | return ret; 317 | } 318 | 319 | /// Generates a Bson document from a struct/class object 320 | Bson toSchemaBson(T)(T obj) 321 | { 322 | static if (__traits(compiles, cast(T) null) && __traits(compiles, { 323 | T foo = null; 324 | })) 325 | { 326 | if (obj is null) 327 | return Bson(null); 328 | } 329 | 330 | Bson data = Bson.emptyObject; 331 | 332 | enum members = getSerializableMembers!obj; 333 | 334 | static if (hasMember!(T, "_schema_object_id_")) 335 | { 336 | if (obj.bsonID.valid) 337 | data["_id"] = obj.bsonID; 338 | } 339 | else static if (members.length == 0) 340 | static assert(false, "Trying to MongoSchema serialize type " ~ T.stringof ~ " with no (accessible) members. Annotate member with @schemaIgnore if intended or provide a custom toBson and fromBson method."); 341 | 342 | static foreach (memberName; members) 343 | { 344 | { 345 | string name = memberName; 346 | Bson value; 347 | static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 348 | { 349 | static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 350 | .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 351 | name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 352 | } 353 | 354 | static if (hasUDA!((__traits(getMember, obj, memberName)), encode)) 355 | { 356 | static assert(getUDAs!((__traits(getMember, obj, memberName)), encode) 357 | .length == 1, "Member '" ~ memberName ~ "' can only have one encoder!"); 358 | mixin("value = obj." ~ getUDAs!((__traits(getMember, obj, memberName)), 359 | encode)[0].func ~ "(obj);"); 360 | } 361 | else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 362 | { 363 | static assert(isArray!(typeof((__traits(getMember, obj, 364 | memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 365 | "Binary member '" ~ memberName ~ "' can only be an array of 1 byte values"); 366 | static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType) 367 | .length == 1, "Binary member '" ~ memberName ~ "' can only have one type!"); 368 | BsonBinData.Type type = getUDAs!((__traits(getMember, obj, memberName)), binaryType)[0] 369 | .type; 370 | value = Bson(BsonBinData(type, 371 | cast(immutable(ubyte)[])(__traits(getMember, obj, memberName)))); 372 | } 373 | else 374 | { 375 | static if (__traits(compiles, { 376 | __traits(hasMember, typeof((__traits(getMember, obj, memberName))), "toBson"); 377 | }) && __traits(hasMember, typeof((__traits(getMember, obj, 378 | memberName))), "toBson") && !is(ReturnType!(typeof((__traits(getMember, 379 | obj, memberName)).toBson)) == Bson)) 380 | pragma(msg, "Warning: ", typeof((__traits(getMember, obj, memberName))) 381 | .stringof, ".toBson does not return a vibe.data.bson.Bson struct!"); 382 | 383 | value = memberToBson(__traits(getMember, obj, memberName)); 384 | } 385 | data[name] = value; 386 | } 387 | } 388 | 389 | return data; 390 | } 391 | 392 | /// Generates a struct/class object from a Bson node 393 | T fromSchemaBson(T)(Bson bson) 394 | { 395 | static if (__traits(compiles, cast(T) null) && __traits(compiles, { 396 | T foo = null; 397 | })) 398 | { 399 | if (bson.isNull) 400 | return null; 401 | } 402 | T obj = T.init; 403 | 404 | static if (hasMember!(T, "_schema_object_id_")) 405 | { 406 | if (!bson.tryIndex("_id").isNull) 407 | obj.bsonID = bson["_id"].get!BsonObjectID; 408 | } 409 | 410 | static foreach (memberName; getSerializableMembers!obj) 411 | { 412 | { 413 | string name = memberName; 414 | static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 415 | { 416 | static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 417 | .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 418 | name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 419 | } 420 | 421 | // compile time code will still be generated but not run at runtime 422 | if (!bson.tryIndex(name).isNull && bson[name].type != Bson.Type.undefined) 423 | { 424 | static if (hasUDA!((__traits(getMember, obj, memberName)), decode)) 425 | { 426 | static assert(getUDAs!((__traits(getMember, obj, memberName)), decode) 427 | .length == 1, "Member '" ~ memberName ~ "' can only have one decoder!"); 428 | mixin("obj." ~ memberName ~ " = obj." ~ getUDAs!((__traits(getMember, 429 | obj, memberName)), decode)[0].func ~ "(bson);"); 430 | } 431 | else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 432 | { 433 | static assert(isArray!(typeof((__traits(getMember, obj, 434 | memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 435 | "Binary member '" ~ memberName ~ "' can only be an array of 1 byte values"); 436 | static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType) 437 | .length == 1, "Binary member '" ~ memberName ~ "' can only have one type!"); 438 | assert(bson[name].type == Bson.Type.binData); 439 | auto data = bson[name].get!(BsonBinData).rawData; 440 | mixin("obj." ~ memberName ~ " = cast(typeof(obj." ~ memberName ~ ")) data;"); 441 | } 442 | else 443 | { 444 | mixin("obj." ~ memberName ~ " = bsonToMember(obj." ~ memberName ~ ", bson[name]);"); 445 | } 446 | } 447 | } 448 | } 449 | 450 | return obj; 451 | } 452 | -------------------------------------------------------------------------------- /source/mongoschema/query.d: -------------------------------------------------------------------------------- 1 | /// This module provides a typesafe querying framework. 2 | /// For now only very basic queries are supported 3 | module mongoschema.query; 4 | 5 | import mongoschema; 6 | 7 | import std.regex; 8 | import std.traits; 9 | 10 | /// Represents a field to compare 11 | struct FieldQuery(T, Obj) 12 | { 13 | enum isCompatible(V) = is(V : T) || is(V == Bson); 14 | 15 | Query!Obj* query; 16 | string name; 17 | 18 | @disable this(); 19 | @disable this(this); 20 | 21 | private this(string name, ref Query!Obj query) @trusted 22 | { 23 | this.name = name; 24 | this.query = &query; 25 | } 26 | 27 | ref Query!Obj equals(V)(V other) if (isCompatible!V) 28 | { 29 | query._query[name] = memberToBson(other); 30 | return *query; 31 | } 32 | 33 | alias equal = equals; 34 | alias eq = equals; 35 | 36 | ref Query!Obj ne(V)(V other) if (isCompatible!V) 37 | { 38 | query._query[name] = Bson(["$ne": memberToBson(other)]); 39 | return *query; 40 | } 41 | 42 | alias notEqual = ne; 43 | alias notEquals = ne; 44 | 45 | ref Query!Obj gt(V)(V other) if (isCompatible!V) 46 | { 47 | query._query[name] = Bson(["$gt": memberToBson(other)]); 48 | return *query; 49 | } 50 | 51 | alias greaterThan = gt; 52 | 53 | ref Query!Obj gte(V)(V other) if (isCompatible!V) 54 | { 55 | query._query[name] = Bson(["$gte": memberToBson(other)]); 56 | return *query; 57 | } 58 | 59 | alias greaterThanOrEqual = gt; 60 | 61 | ref Query!Obj lt(V)(V other) if (isCompatible!V) 62 | { 63 | query._query[name] = Bson(["$lt": memberToBson(other)]); 64 | return *query; 65 | } 66 | 67 | alias lessThan = lt; 68 | 69 | ref Query!Obj lte(V)(V other) if (isCompatible!V) 70 | { 71 | query._query[name] = Bson(["$lte": memberToBson(other)]); 72 | return *query; 73 | } 74 | 75 | alias lessThanOrEqual = lt; 76 | 77 | ref Query!Obj oneOf(Args...)(Args other) 78 | { 79 | Bson[] arr = new Bson(Args.length); 80 | static foreach (i, arg; other) 81 | arr[i] = memberToBson(arg); 82 | query._query[name] = Bson(["$in": Bson(arr)]); 83 | return *query; 84 | } 85 | 86 | ref Query!Obj inArray(V)(V[] array) if (isCompatible!V) 87 | { 88 | query._query[name] = Bson(["$in": memberToBson(array)]); 89 | return *query; 90 | } 91 | 92 | ref Query!Obj noneOf(Args...)(Args other) 93 | { 94 | Bson[] arr = new Bson(Args.length); 95 | static foreach (i, arg; other) 96 | arr[i] = memberToBson(arg); 97 | query._query[name] = Bson(["$nin": Bson(arr)]); 98 | return *query; 99 | } 100 | 101 | alias notOneOf = noneOf; 102 | 103 | ref Query!Obj notInArray(V)(V[] array) if (isCompatible!V) 104 | { 105 | query._query[name] = Bson(["$nin": memberToBson(array)]); 106 | return *query; 107 | } 108 | 109 | ref Query!Obj exists(bool exists = true) 110 | { 111 | query._query[name] = Bson(["$exists": Bson(exists)]); 112 | return *query; 113 | } 114 | 115 | ref Query!Obj typeOf(Bson.Type type) 116 | { 117 | query._query[name] = Bson(["$type": Bson(cast(int) type)]); 118 | return *query; 119 | } 120 | 121 | ref Query!Obj typeOfAny(Bson.Type[] types...) 122 | { 123 | Bson[] arr = new Bson[types.length]; 124 | foreach (i, type; types) 125 | arr[i] = Bson(cast(int) type); 126 | query._query[name] = Bson(["$type": Bson(arr)]); 127 | return *query; 128 | } 129 | 130 | ref Query!Obj typeOfAny(Bson.Type[] types) 131 | { 132 | query._query[name] = Bson(["$type": serializeToBson(types)]); 133 | return *query; 134 | } 135 | 136 | static if (is(T : U[], U)) 137 | { 138 | ref Query!Obj containsAll(U[] values) 139 | { 140 | query._query[name] = Bson(["$all": serializeToBson(values)]); 141 | return *query; 142 | } 143 | 144 | alias all = containsAll; 145 | 146 | ref Query!Obj ofLength(size_t length) 147 | { 148 | query._query[name] = Bson(["$size": Bson(length)]); 149 | return *query; 150 | } 151 | 152 | alias size = ofLength; 153 | } 154 | 155 | static if (isIntegral!T) 156 | { 157 | ref Query!Obj bitsAllClear(T other) 158 | { 159 | query._query[name] = Bson(["$bitsAllClear": Bson(other)]); 160 | return *query; 161 | } 162 | 163 | ref Query!Obj bitsAllSet(T other) 164 | { 165 | query._query[name] = Bson(["$bitsAllSet": Bson(other)]); 166 | return *query; 167 | } 168 | 169 | ref Query!Obj bitsAnyClear(T other) 170 | { 171 | query._query[name] = Bson(["$bitsAnyClear": Bson(other)]); 172 | return *query; 173 | } 174 | 175 | ref Query!Obj bitsAnySet(T other) 176 | { 177 | query._query[name] = Bson(["$bitsAnySet": Bson(other)]); 178 | return *query; 179 | } 180 | } 181 | 182 | static if (isNumeric!T) 183 | { 184 | ref Query!Obj remainder(T divisor, T remainder) 185 | { 186 | query._query[name] = Bson(["$mod": Bson([Bson(divisor), Bson(remainder)])]); 187 | return *query; 188 | } 189 | } 190 | 191 | static if (isSomeString!T) 192 | { 193 | ref Query!Obj regex(string regex, string options = null) 194 | { 195 | if (options.length) 196 | query._query[name] = Bson([ 197 | "$regex": Bson(regex), 198 | "$options": Bson(options) 199 | ]); 200 | else 201 | query._query[name] = Bson(["$regex": Bson(regex)]); 202 | return *query; 203 | } 204 | } 205 | } 206 | 207 | private string generateMember(string member, string name) 208 | { 209 | return `alias T_` ~ member ~ ` = typeof(__traits(getMember, T.init, "` ~ member ~ `")); 210 | 211 | FieldQuery!(T_` ~ member 212 | ~ `, T) ` ~ member ~ `() 213 | { 214 | return FieldQuery!(T_` ~ member ~ `, T)(` ~ '`' ~ name ~ '`' ~ `, this); 215 | } 216 | 217 | typeof(this) ` ~ member 218 | ~ `(T_` ~ member ~ ` equals) 219 | { 220 | return ` ~ member ~ `.equals(equals); 221 | }`; 222 | } 223 | 224 | private string generateMembers(T)(T obj) 225 | { 226 | string ret; 227 | static foreach (memberName; getSerializableMembers!obj) 228 | { 229 | { 230 | string name = memberName; 231 | static if (hasUDA!(__traits(getMember, obj, memberName), schemaName)) 232 | { 233 | static assert(getUDAs!(__traits(getMember, obj, memberName), schemaName) 234 | .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 235 | name = getUDAs!(__traits(getMember, obj, memberName), schemaName)[0].name; 236 | } 237 | 238 | static if (!hasUDA!(__traits(getMember, obj, memberName), encode) 239 | && !hasUDA!(__traits(getMember, obj, memberName), decode)) 240 | ret ~= generateMember(memberName, name); 241 | } 242 | } 243 | return ret; 244 | } 245 | 246 | struct Query(T) 247 | { 248 | Bson[string] _query; 249 | 250 | mixin(generateMembers!T(T.init)); 251 | 252 | static Bson toBson(Query!T query) 253 | { 254 | return Bson(query._query); 255 | } 256 | } 257 | 258 | Query!T query(T)() 259 | { 260 | return Query!T.init; 261 | } 262 | 263 | Query!T and(T)(Query!T[] exprs...) 264 | { 265 | return Query!T(["$and": memberToBson(exprs)]); 266 | } 267 | 268 | Query!T not(T)(Query!T[] exprs) 269 | { 270 | return Query!T(["$not": memberToBson(exprs)]); 271 | } 272 | 273 | Query!T nor(T)(Query!T[] exprs...) 274 | { 275 | return Query!T(["$nor": memberToBson(exprs)]); 276 | } 277 | 278 | Query!T or(T)(Query!T[] exprs...) 279 | { 280 | return Query!T(["$or": memberToBson(exprs)]); 281 | } 282 | 283 | unittest 284 | { 285 | struct CoolData 286 | { 287 | int number; 288 | bool boolean; 289 | string[] array; 290 | @schemaName("t") 291 | string text; 292 | } 293 | 294 | assert(memberToBson(and(query!CoolData.number.gte(10), 295 | query!CoolData.number.lte(20), query!CoolData.boolean(true) 296 | .array.ofLength(10).text.regex("^yes"))).toString == Bson( 297 | [ 298 | "$and": Bson([ 299 | Bson(["number": Bson(["$gte": Bson(10)])]), 300 | Bson(["number": Bson(["$lte": Bson(20)])]), 301 | Bson([ 302 | "array": Bson(["$size": Bson(10)]), 303 | "boolean": Bson(true), 304 | "t": Bson(["$regex": Bson("^yes")]) 305 | ]), 306 | ]) 307 | ]).toString); 308 | } 309 | -------------------------------------------------------------------------------- /source/mongoschema/variant.d: -------------------------------------------------------------------------------- 1 | /// This module provides a utility class to store different type values inside a single field. 2 | /// This can for example be used to model inheritance. 3 | module mongoschema.variant; 4 | 5 | import std.meta; 6 | import std.traits; 7 | import std.variant; 8 | 9 | import vibe.data.bson; 10 | 11 | import mongoschema; 12 | 13 | private enum bool distinctFieldNames(names...) = __traits(compiles, { 14 | static foreach (__name; names) 15 | static if (is(typeof(__name) : string)) 16 | mixin("enum int " ~ __name ~ " = 0;"); 17 | else 18 | mixin("enum int " ~ __name.stringof ~ " = 0;"); 19 | }); 20 | 21 | /// Represents a data type which can hold different kinds of values but always exactly one or none at a time. 22 | /// Types is a list of types the variant can hold. By default type IDs are assigned from the stringof value which is the type name without module name. 23 | /// You can pass custom type names by passing a string following the type. 24 | /// Those will affect the type value in the serialized bson and the convenience access function names. 25 | /// Serializes the Bson as `{"type": "T", "value": "my value here"}` 26 | final struct SchemaVariant(Specs...) if (distinctFieldNames!(Specs)) 27 | { 28 | // Parse (type,name) pairs (FieldSpecs) out of the specified 29 | // arguments. Some fields would have name, others not. 30 | private template parseSpecs(Specs...) 31 | { 32 | static if (Specs.length == 0) 33 | { 34 | alias parseSpecs = AliasSeq!(); 35 | } 36 | else static if (is(Specs[0])) 37 | { 38 | static if (is(typeof(Specs[1]) : string)) 39 | { 40 | alias parseSpecs = AliasSeq!(FieldSpec!(Specs[0 .. 2]), parseSpecs!(Specs[2 .. $])); 41 | } 42 | else 43 | { 44 | alias parseSpecs = AliasSeq!(FieldSpec!(Specs[0]), parseSpecs!(Specs[1 .. $])); 45 | } 46 | } 47 | else 48 | { 49 | static assert(0, 50 | "Attempted to instantiate Variant with an invalid argument: " ~ Specs[0].stringof); 51 | } 52 | } 53 | 54 | private template specTypes(Specs...) 55 | { 56 | static if (Specs.length == 0) 57 | { 58 | alias specTypes = AliasSeq!(); 59 | } 60 | else static if (is(Specs[0])) 61 | { 62 | static if (is(typeof(Specs[1]) : string)) 63 | { 64 | alias specTypes = AliasSeq!(Specs[0], specTypes!(Specs[2 .. $])); 65 | } 66 | else 67 | { 68 | alias specTypes = AliasSeq!(Specs[0], specTypes!(Specs[1 .. $])); 69 | } 70 | } 71 | else 72 | { 73 | static assert(0, 74 | "Attempted to instantiate Variant with an invalid argument: " ~ Specs[0].stringof); 75 | } 76 | } 77 | 78 | private template FieldSpec(T, string s = T.stringof) 79 | { 80 | alias Type = T; 81 | alias name = s; 82 | } 83 | 84 | alias Fields = parseSpecs!Specs; 85 | alias Types = specTypes!Specs; 86 | 87 | template typeIndex(T) 88 | { 89 | enum hasType = staticIndexOf!(T, Types); 90 | } 91 | 92 | template hasType(T) 93 | { 94 | enum hasType = staticIndexOf!(T, Types) != -1; 95 | } 96 | 97 | public: 98 | Algebraic!Types value; 99 | 100 | this(T)(T value) @trusted 101 | { 102 | this.value = value; 103 | } 104 | 105 | static foreach (Field; Fields) 106 | mixin("inout(Field.Type) " ~ Field.name 107 | ~ "() @trusted inout { checkType!(Field.Type); return value.get!(Field.Type); }"); 108 | 109 | void checkType(T)() const 110 | { 111 | if (!isType!T) 112 | throw new Exception("Attempted to access " ~ type ~ " field as " ~ T.stringof); 113 | } 114 | 115 | bool isType(T)() @trusted const 116 | { 117 | return value.type == typeid(T); 118 | } 119 | 120 | string type() const 121 | { 122 | if (!value.hasValue) 123 | return null; 124 | 125 | static foreach (Field; Fields) 126 | if (isType!(Field.Type)) 127 | return Field.name; 128 | 129 | assert(false, "Checked all possible types of variant but none of them matched?!"); 130 | } 131 | 132 | void opAssign(T)(T value) @trusted if (hasType!T) 133 | { 134 | this.value = value; 135 | } 136 | 137 | static Bson toBson(SchemaVariant!Specs value) 138 | { 139 | if (!value.value.hasValue) 140 | return Bson.init; 141 | 142 | static foreach (Field; Fields) 143 | if (value.isType!(Field.Type)) 144 | return Bson([ 145 | "type": Bson(Field.name), 146 | "value": toSchemaBson((() @trusted => value.value.get!(Field.Type))()) 147 | ]); 148 | 149 | assert(false, "Checked all possible types of variant but none of them matched?!"); 150 | } 151 | 152 | static SchemaVariant!Specs fromBson(Bson bson) 153 | { 154 | if (bson.type != Bson.Type.object) 155 | return SchemaVariant!Specs.init; 156 | auto type = "type" in bson.get!(Bson[string]); 157 | if (!type || type.type != Bson.Type.string) 158 | throw new Exception( 159 | "Malformed " ~ SchemaVariant!Specs.stringof ~ " bson, missing or invalid type argument"); 160 | 161 | switch (type.get!string) 162 | { 163 | static foreach (i, Field; Fields) 164 | { 165 | case Field.name: 166 | return SchemaVariant!Specs(fromSchemaBson!(Field.Type)(bson["value"])); 167 | } 168 | default: 169 | throw new Exception("Invalid " ~ SchemaVariant!Specs.stringof ~ " type " ~ type.get!string); 170 | } 171 | } 172 | } 173 | 174 | unittest 175 | { 176 | struct Foo 177 | { 178 | int x = 3; 179 | } 180 | 181 | struct Bar 182 | { 183 | string y = "bar"; 184 | } 185 | 186 | SchemaVariant!(Foo, Bar) var1; 187 | assert(typeof(var1).toBson(var1) == Bson.init); 188 | var1 = Foo(); 189 | assert(typeof(var1).toBson(var1) == Bson([ 190 | "type": Bson("Foo"), 191 | "value": Bson(["x": Bson(3)]) 192 | ])); 193 | assert(var1.type == "Foo"); 194 | var1 = Bar(); 195 | assert(typeof(var1).toBson(var1) == Bson([ 196 | "type": Bson("Bar"), 197 | "value": Bson(["y": Bson("bar")]) 198 | ])); 199 | assert(var1.type == "Bar"); 200 | 201 | var1 = typeof(var1).fromBson(Bson([ 202 | "type": Bson("Foo"), 203 | "value": Bson(["x": Bson(4)]) 204 | ])); 205 | assert(var1.type == "Foo"); 206 | assert(var1.Foo == Foo(4)); 207 | 208 | var1 = typeof(var1).fromBson(Bson([ 209 | "type": Bson("Bar"), 210 | "value": Bson(["y": Bson("barf")]) 211 | ])); 212 | assert(var1.type == "Bar"); 213 | assert(var1.Bar == Bar("barf")); 214 | 215 | SchemaVariant!(Foo, "foo", Bar, "bar") var2; 216 | assert(typeof(var2).toBson(var2) == Bson.init); 217 | var2 = Foo(); 218 | assert(var2.type == "foo"); 219 | assert(var2.foo == Foo()); 220 | assert(typeof(var2).toBson(var2) == Bson([ 221 | "type": Bson("foo"), 222 | "value": Bson(["x": Bson(3)]) 223 | ])); 224 | 225 | const x = var2; 226 | assert(x.type == "foo"); 227 | assert(x.isType!Foo); 228 | assert(typeof(x).toBson(x) == Bson([ 229 | "type": Bson("foo"), 230 | "value": Bson(["x": Bson(3)]) 231 | ])); 232 | } 233 | --------------------------------------------------------------------------------