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