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