tests: add some runtime error in nitin.input
[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 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 # ~~~
256 # import popcorn
257 # import popcorn::pop_repos
258 #
259 # # First, let's create a User abstraction:
260 #
261 # # Serializable user representation.
262 # class User
263 # super RepoObject
264 # serialize
265 #
266 # # User login
267 # var login: String
268 #
269 # # User password
270 # var password: String is writable
271 #
272 # redef fun to_s do return login
273 # end
274 #
275 # # We then need to subclass the `MongoRepository` to provide User specific services:
276 #
277 # # User repository for Mongo
278 # class UserRepo
279 # super MongoRepository[User]
280 #
281 # # Find a user by its login
282 # fun find_by_login(login: String): nullable User do
283 # var q = new JsonObject
284 # q["login"] = login
285 # return find(q)
286 # end
287 # end
288 #
289 # # The repository can then be used with User instances:
290 #
291 # # Init database
292 # var mongo = new MongoClient("mongodb://localhost:27017/")
293 # var db = mongo.database("tests")
294 # var coll = db.collection("test_pop_repo_{100000.rand}")
295 #
296 # # Create a user repo to store User instances
297 # var repo = new UserRepo(coll)
298 #
299 # # Create some users
300 # repo.save(new User("Morriar", "1234"))
301 # repo.save(new User("Alex", "password"))
302 #
303 # assert repo.find_all.length == 2
304 # assert repo.find_by_login("Morriar").password == "1234"
305 # repo.clear
306 # assert repo.find_all.length == 0
307 # ~~~
308 class MongoRepository[E: Serializable]
309 super JsonRepository[E]
310
311 redef type QUERY: JsonObject
312
313 # MongoDB collection used to store objects
314 var collection: MongoCollection
315
316 redef fun find_by_id(id) do
317 var query = new JsonObject
318 query["_id"] = id
319 return find(query)
320 end
321
322 redef fun find(query) do
323 var res = collection.find(query)
324 if res == null then return null
325 return deserialize(res.to_json)
326 end
327
328 redef fun find_all(query, skip, limit) do
329 var res = new Array[E]
330 for e in collection.find_all(query or else new JsonObject, skip, limit) do
331 res.add deserialize(e.to_json).as(E)
332 end
333 return res
334 end
335
336 redef fun count(query) do
337 return collection.count(query or else new JsonObject)
338 end
339
340 redef fun save(item) do
341 var json = serialize(item).as(String)
342 var obj = json.parse_json.as(JsonObject)
343 return collection.save(obj)
344 end
345
346 redef fun remove_by_id(id) do
347 var query = new JsonObject
348 query["_id"] = id
349 return remove(query)
350 end
351
352 redef fun remove(query) do
353 return collection.remove(query or else new JsonObject)
354 end
355
356 redef fun remove_all(query) do
357 return collection.remove_all(query or else new JsonObject)
358 end
359
360 redef fun clear do return collection.drop
361
362 # Perform an aggregation query over the repo.
363 fun aggregate(pipeline: JsonArray): Array[E] do
364 var res = new Array[E]
365 for obj in collection.aggregate(pipeline) do
366 var instance = deserialize(obj.to_json)
367 if instance == null then continue
368 res.add instance
369 end
370 return res
371 end
372 end
373
374 # Base serializable entity that can go into a JsonRepository
375 #
376 # Provide boiler plate implementation of all object serializable to json.
377 #
378 # `id` is used as a primary key for `find_by_id`.
379 #
380 # Subclassing RepoObject makes it easy to create a serializable class:
381 # ~~~
382 # import popcorn::pop_repos
383 #
384 # class Album
385 # super RepoObject
386 # serialize
387 #
388 # var title: String
389 # var price: Float
390 # end
391 # ~~~
392 #
393 # Do not forget the `serialize` annotation else the fields will not be serialized.
394 #
395 # It is also possible to redefine the `id` primary key to use your own:
396 # ~~~
397 # import popcorn::pop_repos
398 #
399 # class Order
400 # super RepoObject
401 # serialize
402 #
403 # redef var id = "order-{get_time}"
404 #
405 # # ...
406 #
407 # end
408 # ~~~
409 abstract class RepoObject
410 super Jsonable
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