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