nitcc: add an example of a monkey-patching of a Lexer to add behavior.
[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
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 instances
164 fun clear: Bool is abstract
165
166 # Serialize an `instance` to a String.
167 fun serialize(instance: nullable E): nullable String is abstract
168
169 # Deserialize a `string` to an instance.
170 fun deserialize(string: nullable String): nullable E is abstract
171 end
172
173 # An abstract Query representation.
174 #
175 # Since the kind of query available depends on the database backend choice or
176 # implementation, this interface is used to provide a common type to all the
177 # queries.
178 #
179 # Redefine `Repository::QUERY` to use your own kind of query.
180 interface RepositoryQuery end
181
182 # A Repository for JsonObjects.
183 #
184 # As for document oriented databases, Repository can be used to store and retrieve
185 # Json object.
186 # Serialization from/to Json is used to translate from/to nit instances.
187 #
188 # See `MongoRepository` for a concrete implementation example.
189 interface JsonRepository[E: Serializable]
190 super Repository[E]
191
192 redef fun serialize(item) do
193 if item == null then return null
194 return item.serialize_to_json
195 end
196
197 redef fun deserialize(string) do
198 if string == null then return null
199 var deserializer = new JsonDeserializer(string)
200 return deserializer.deserialize.as(E)
201 end
202 end
203
204 # A Repository that uses MongoDB as backend.
205 #
206 # ~~~
207 # import popcorn
208 # import popcorn::pop_routes
209 #
210 # # First, let's create a User abstraction:
211 #
212 # # Serializable user representation.
213 # class User
214 # serialize
215 # super Jsonable
216 #
217 # # User uniq ID
218 # var id: String = (new MongoObjectId).id is serialize_as "_id"
219 #
220 # # User login
221 # var login: String
222 #
223 # # User password
224 # var password: String is writable
225 #
226 # redef fun to_s do return login
227 # redef fun ==(o) do return o isa SELF and id == o.id
228 # redef fun hash do return id.hash
229 # redef fun to_json do return serialize_to_json
230 # end
231 #
232 # # We then need to subclass the `MongoRepository` to provide User specific services:
233 #
234 # # User repository for Mongo
235 # class UserRepo
236 # super MongoRepository[User]
237 #
238 # # Find a user by its login
239 # fun find_by_login(login: String): nullable User do
240 # var q = new JsonObject
241 # q["login"] = login
242 # return find(q)
243 # end
244 # end
245 #
246 # # The repository can then be used with User instances:
247 #
248 # # Init database
249 # var mongo = new MongoClient("mongodb://localhost:27017/")
250 # var db = mongo.database("tests")
251 # var coll = db.collection("test_pop_repo_{100000.rand}")
252 #
253 # # Create a user repo to store User instances
254 # var repo = new UserRepo(coll)
255 #
256 # # Create some users
257 # repo.save(new User("Morriar", "1234"))
258 # repo.save(new User("Alex", "password"))
259 #
260 # assert repo.find_all.length == 2
261 # assert repo.find_by_login("Morriar").password == "1234"
262 # repo.clear
263 # assert repo.find_all.length == 0
264 # ~~~
265 class MongoRepository[E: Serializable]
266 super JsonRepository[E]
267
268 redef type QUERY: JsonObject
269
270 # MongoDB collection used to store objects
271 var collection: MongoCollection
272
273 redef fun find_by_id(id) do
274 var query = new JsonObject
275 query["_id"] = id
276 return find(query)
277 end
278
279 redef fun find(query) do
280 var res = collection.find(query)
281 if res == null then return null
282 return deserialize(res.to_json)
283 end
284
285 redef fun find_all(query) do
286 var res = new Array[E]
287 for e in collection.find_all(query or else new JsonObject) do
288 res.add deserialize(e.to_json).as(E)
289 end
290 return res
291 end
292
293 redef fun save(item) do
294 var json = serialize(item).as(String)
295 var obj = json.parse_json.as(JsonObject)
296 return collection.save(obj)
297 end
298
299 redef fun remove_by_id(id) do
300 var query = new JsonObject
301 query["_id"] = id
302 return remove(query)
303 end
304
305 redef fun remove(query) do
306 return collection.remove(query or else new JsonObject)
307 end
308
309 redef fun clear do return collection.drop
310 end
311
312 # JsonObject can be used as a `RepositoryQuery`.
313 #
314 # See `mongodb` lib.
315 redef class JsonObject
316 super RepositoryQuery
317 end