search_tests.sh: Unquote results
[nit.git] / lib / popcorn / pop_repos.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Repositories for data management.
18 #
19 # Repositories are used to apply persistence on instances (or **documents**).
20 # Using repositories one can store and retrieve instance in a clean and maintenable
21 # way.
22 #
23 # This module provides the base interface `Repository` that defines the persistence
24 # services available in all kind of repos.
25 # `JsonRepository` factorizes all repositories dedicated to Json data or objects
26 # serializable to Json.
27 #
28 # `MongoRepository` is provided as a concrete example of repository.
29 # It implements all the services from `Repository` using a Mongo database as backend.
30 #
31 # Repositories can be used in Popcorn app to manage your data persistence.
32 # Here an example with a book management app:
33 #
34 # ~~~
35 # # First we declare the `Book` class. It has to be serializable so it can be used
36 # # within a `Repository`.
37 #
38 # import popcorn
39 # import popcorn::pop_repos
40 #
41 # # Serializable book representation.
42 # class Book
43 # serialize
44 #
45 # # Book uniq ID
46 # var id: String = (new MongoObjectId).id is serialize_as "_id"
47 #
48 # # Book title
49 # var title: String
50 #
51 # # ... Other fields
52 #
53 # redef fun to_s do return title
54 # redef fun ==(o) do return o isa SELF and id == o.id
55 # redef fun hash do return id.hash
56 # end
57 #
58 # # We then need to subclass the `MongoRepository` to provide Book specific services.
59 #
60 # # Book repository for Mongo
61 # class BookRepo
62 # super MongoRepository[Book]
63 #
64 # # Find books by title
65 # fun find_by_title(title: String): Array[Book] do
66 # var q = new JsonObject
67 # q["title"] = title
68 # return find_all(q)
69 # end
70 # end
71 #
72 # # The repository can be used in a Handler to manage book in a REST API.
73 #
74 # class BookHandler
75 # super Handler
76 #
77 # var repo: BookRepo
78 #
79 # # Return a json array of all books
80 # #
81 # # If the get parameters `title` is provided, returns a json array of books
82 # # matching the `title`.
83 # redef fun get(req, res) do
84 # var title = req.string_arg("title")
85 # if title == null then
86 # res.json new JsonArray.from(repo.find_all)
87 # else
88 # res.json new JsonArray.from(repo.find_by_title(title))
89 # end
90 # end
91 #
92 # # Insert a new Book
93 # redef fun post(req, res) do
94 # var title = req.string_arg("title")
95 # if title == null then
96 # res.error 400
97 # return
98 # end
99 # var book = new Book(title)
100 # repo.save book
101 # res.json book
102 # end
103 # end
104 #
105 # # Let's wrap it all together in a Popcorn app:
106 #
107 # # Init database
108 # var mongo = new MongoClient("mongodb://localhost:27017/")
109 # var db = mongo.database("tests_app_{100000.rand}")
110 # var coll = db.collection("books")
111 #
112 # # Init app
113 # var app = new App
114 # var repo = new BookRepo(coll)
115 # app.use("/books", new BookHandler(repo))
116 # app.listen("localhost", 3000)
117 # ~~~
118 module pop_repos
119
120 import popcorn::pop_config
121 import serialization
122 import json
123 import mongodb::queries
124
125 redef class AppConfig
126
127 # Default database host string for MongoDb
128 var default_db_host = "mongodb://localhost:27017/"
129
130 # Default database hostname
131 var default_db_name = "popcorn"
132
133 # MongoDb host name
134 var opt_db_host = new OptionString("MongoDb host", "--db-host")
135
136 # MongoDb database name
137 var opt_db_name = new OptionString("MongoDb database name", "--db-name")
138
139 # MongoDB server used for data persistence
140 fun db_host: String do return opt_db_host.value or else ini["db.host"] or else default_db_host
141
142 # MongoDB DB used for data persistence
143 fun db_name: String do return opt_db_name.value or else ini["db.name"] or else default_db_name
144
145 init do
146 super
147 add_option(opt_db_host, opt_db_name)
148 end
149
150 # Mongo db client
151 var client = new MongoClient(db_host) is lazy
152
153 # Mongo db instance
154 var db: MongoDb = client.database(db_name) is lazy
155 end
156
157 # A Repository is an object that can store serialized instances.
158 #
159 # Repository is the base class of all kind of persistance processes. It offers
160 # the base CRUD services to save (add/update), find and delete instances.
161 #
162 # Instances are stored in their serialized form. See the `serialization` package
163 # for more documentation.
164 interface Repository[E: Serializable]
165
166 # Kind of queries accepted
167 #
168 # Can be redefined to accept more precise queries depending on the backend used.
169 type QUERY: RepositoryQuery
170
171 # Find an instance by it's `id`
172 #
173 # `id` is an abstract thing at this stage
174 # TODO maybe introduce the `PrimaryKey` concept?
175 fun find_by_id(id: String): nullable E is abstract
176
177 # Find an instance based on `query`
178 fun find(query: QUERY): nullable E is abstract
179
180 # Find all instances based on `query`
181 #
182 # Using `query` == null will retrieve all the document in the repository.
183 fun find_all(query: nullable QUERY, skip, limit: nullable Int): Array[E] is abstract
184
185 # Count instances that matches `query`
186 fun count(query: nullable QUERY): Int is abstract
187
188 # Save an `instance`
189 fun save(instance: E): Bool is abstract
190
191 # Remove the instance with `id`
192 fun remove_by_id(id: String): Bool is abstract
193
194 # Remove the instance based on `query`
195 fun remove(query: nullable QUERY): Bool is abstract
196
197 # Remove all the instances matching on `query`
198 fun remove_all(query: nullable QUERY): Bool is abstract
199
200 # Remove all instances
201 fun clear: Bool is abstract
202
203 # Serialize an `instance` to a String.
204 fun serialize(instance: nullable E): nullable String is abstract
205
206 # Deserialize a `string` to an instance.
207 fun deserialize(string: nullable String): nullable E is abstract
208 end
209
210 # An abstract Query representation.
211 #
212 # Since the kind of query available depends on the database backend choice or
213 # implementation, this interface is used to provide a common type to all the
214 # queries.
215 #
216 # Redefine `Repository::QUERY` to use your own kind of query.
217 interface RepositoryQuery end
218
219 # A Repository for JsonObjects.
220 #
221 # As for document oriented databases, Repository can be used to store and retrieve
222 # Json object.
223 # Serialization from/to Json is used to translate from/to nit instances.
224 #
225 # See `MongoRepository` for a concrete implementation example.
226 abstract class JsonRepository[E: Serializable]
227 super Repository[E]
228
229 redef fun serialize(item) do
230 if item == null then return null
231 var stream = new StringWriter
232 var serializer = new RepoSerializer(stream)
233 serializer.serialize item
234 stream.close
235 return stream.to_s
236 end
237
238 redef fun deserialize(string) do
239 if string == null then return null
240 var deserializer = new JsonDeserializer(string)
241 return deserializer.deserialize.as(E)
242 end
243 end
244
245 private class RepoSerializer
246 super JsonSerializer
247
248 # Remove caching when saving refs to db
249 redef fun serialize_reference(object) do serialize object
250 end
251
252 # A Repository that uses MongoDB as backend.
253 #
254 # ~~~
255 # import popcorn
256 # import popcorn::pop_repos
257 #
258 # # First, let's create a User abstraction:
259 #
260 # # Serializable user representation.
261 # class User
262 # super RepoObject
263 # serialize
264 #
265 # # User login
266 # var login: String
267 #
268 # # User password
269 # var password: String is writable
270 #
271 # redef fun to_s do return login
272 # end
273 #
274 # # We then need to subclass the `MongoRepository` to provide User specific services:
275 #
276 # # User repository for Mongo
277 # class UserRepo
278 # super MongoRepository[User]
279 #
280 # # Find a user by its login
281 # fun find_by_login(login: String): nullable User do
282 # var q = new JsonObject
283 # q["login"] = login
284 # return find(q)
285 # end
286 # end
287 #
288 # # The repository can then be used with User instances:
289 #
290 # # Init database
291 # var mongo = new MongoClient("mongodb://localhost:27017/")
292 # var db = mongo.database("tests")
293 # var coll = db.collection("test_pop_repo_{100000.rand}")
294 #
295 # # Create a user repo to store User instances
296 # var repo = new UserRepo(coll)
297 #
298 # # Create some users
299 # repo.save(new User("Morriar", "1234"))
300 # repo.save(new User("Alex", "password"))
301 #
302 # assert repo.find_all.length == 2
303 # assert repo.find_by_login("Morriar").password == "1234"
304 # repo.clear
305 # assert repo.find_all.length == 0
306 # ~~~
307 class MongoRepository[E: Serializable]
308 super JsonRepository[E]
309
310 redef type QUERY: JsonObject
311
312 # MongoDB collection used to store objects
313 var collection: MongoCollection
314
315 redef fun find_by_id(id) do
316 var query = new JsonObject
317 query["_id"] = id
318 return find(query)
319 end
320
321 redef fun find(query) do
322 var res = collection.find(query)
323 if res == null then return null
324 return deserialize(res.to_json)
325 end
326
327 redef fun find_all(query, skip, limit) do
328 var res = new Array[E]
329 for e in collection.find_all(query or else new JsonObject, skip, limit) do
330 res.add deserialize(e.to_json).as(E)
331 end
332 return res
333 end
334
335 redef fun count(query) do
336 return collection.count(query or else new JsonObject)
337 end
338
339 redef fun save(item) do
340 var json = serialize(item).as(String)
341 var obj = json.parse_json.as(JsonObject)
342 return collection.save(obj)
343 end
344
345 redef fun remove_by_id(id) do
346 var query = new JsonObject
347 query["_id"] = id
348 return remove(query)
349 end
350
351 redef fun remove(query) do
352 return collection.remove(query or else new JsonObject)
353 end
354
355 redef fun remove_all(query) do
356 return collection.remove_all(query or else new JsonObject)
357 end
358
359 redef fun clear do return collection.drop
360
361 # Perform an aggregation query over the repo.
362 fun aggregate(pipeline: JsonArray): Array[E] do
363 var res = new Array[E]
364 for obj in collection.aggregate(pipeline) do
365 var instance = deserialize(obj.to_json)
366 if instance == null then continue
367 res.add instance
368 end
369 return res
370 end
371 end
372
373 # Base serializable entity that can go into a JsonRepository
374 #
375 # Provide boiler plate implementation of all object serializable to json.
376 #
377 # `id` is used as a primary key for `find_by_id`.
378 #
379 # Subclassing RepoObject makes it easy to create a serializable class:
380 # ~~~
381 # import popcorn::pop_repos
382 #
383 # class Album
384 # super RepoObject
385 # serialize
386 #
387 # var title: String
388 # var price: Float
389 # end
390 # ~~~
391 #
392 # Do not forget the `serialize` annotation else the fields will not be serialized.
393 #
394 # It is also possible to redefine the `id` primary key to use your own:
395 # ~~~
396 # import popcorn::pop_repos
397 #
398 # class Order
399 # super RepoObject
400 # serialize
401 #
402 # redef var id = "order-{get_time}"
403 #
404 # # ...
405 #
406 # end
407 # ~~~
408 abstract class RepoObject
409 serialize
410
411 # `self` unique id.
412 #
413 # This attribute is serialized under the key `_id` to be used
414 # as primary key by MongoDb
415 var id: String = (new MongoObjectId).id is writable, serialize_as "_id"
416
417 # Base object comparison on ID
418 #
419 # Because multiple deserialization can exists of the same instance,
420 # we use the ID to determine if two object are the same.
421 redef fun ==(o) do return o isa SELF and id == o.id
422
423 redef fun hash do return id.hash
424 redef fun to_s do return id
425 end
426
427 # JsonObject can be used as a `RepositoryQuery`.
428 #
429 # See `mongodb` lib.
430 redef class JsonObject
431 super RepositoryQuery
432 end