1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Repositories for data management.
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
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.
28 # `MongoRepository` is provided as a concrete example of repository.
29 # It implements all the services from `Repository` using a Mongo database as backend.
31 # Repositories can be used in Popcorn app to manage your data persistence.
32 # Here an example with a book management app:
35 # # First we declare the `Book` class. It has to be serializable so it can be used
36 # # within a `Repository`.
39 # import popcorn::pop_repos
41 # # Serializable book representation.
47 # var id: String = (new MongoObjectId).id is serialize_as "_id"
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
59 # # We then need to subclass the `MongoRepository` to provide Book specific services.
61 # # Book repository for Mongo
63 # super MongoRepository[Book]
65 # # Find books by title
66 # fun find_by_title(title: String): Array[Book] do
67 # var q = new JsonObject
73 # # The repository can be used in a Handler to manage book in a REST API.
80 # # Return a json array of all books
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)
89 # res.json new JsonArray.from(repo.find_by_title(title))
94 # redef fun post(req, res) do
95 # var title = req.string_arg("title")
96 # if title == null then
100 # var book = new Book(title)
106 # # Let's wrap it all together in a Popcorn app:
109 # var mongo = new MongoClient("mongodb://localhost:27017/")
110 # var db = mongo.database("tests_app_{100000.rand}")
111 # var coll = db.collection("books")
115 # var repo = new BookRepo(coll)
116 # app.use("/books", new BookHandler(repo))
117 # app.listen("localhost", 3000)
121 import popcorn
::pop_config
124 import mongodb
::queries
126 redef class AppConfig
128 # Default database host string for MongoDb
129 var default_db_host
= "mongodb://localhost:27017/"
131 # Default database hostname
132 var default_db_name
= "popcorn"
134 # MongoDB server used for data persistence
135 var db_host
: String is lazy
do return value_or_default
("db.host", default_db_host
)
137 # MongoDB DB used for data persistence
138 var db_name
: String is lazy
do return value_or_default
("db.name", default_db_name
)
141 var client
= new MongoClient(db_host
) is lazy
144 var db
: MongoDb = client
.database
(db_name
) is lazy
146 redef init from_options
(opts
) do
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
155 redef class AppOptions
158 var opt_db_host
= new OptionString("MongoDb host", "--db-host")
160 # MongoDb database name
161 var opt_db_name
= new OptionString("MongoDb database name", "--db-name")
165 add_option
(opt_db_host
, opt_db_name
)
169 # A Repository is an object that can store serialized instances.
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.
174 # Instances are stored in their serialized form. See the `serialization` package
175 # for more documentation.
176 interface Repository[E
: Serializable]
178 # Kind of queries accepted
180 # Can be redefined to accept more precise queries depending on the backend used.
181 type QUERY: RepositoryQuery
183 # Find an instance by it's `id`
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
189 # Find an instance based on `query`
190 fun find
(query
: QUERY): nullable E
is abstract
192 # Find all instances based on `query`
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
197 # Count instances that matches `query`
198 fun count
(query
: nullable QUERY): Int is abstract
201 fun save
(instance
: E
): Bool is abstract
203 # Remove the instance with `id`
204 fun remove_by_id
(id
: String): Bool is abstract
206 # Remove the instance based on `query`
207 fun remove
(query
: nullable QUERY): Bool is abstract
209 # Remove all the instances matching on `query`
210 fun remove_all
(query
: nullable QUERY): Bool is abstract
212 # Remove all instances
213 fun clear
: Bool is abstract
215 # Serialize an `instance` to a String.
216 fun serialize
(instance
: nullable E
): nullable String is abstract
218 # Deserialize a `string` to an instance.
219 fun deserialize
(string
: nullable String): nullable E
is abstract
222 # An abstract Query representation.
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
228 # Redefine `Repository::QUERY` to use your own kind of query.
229 interface RepositoryQuery end
231 # A Repository for JsonObjects.
233 # As for document oriented databases, Repository can be used to store and retrieve
235 # Serialization from/to Json is used to translate from/to nit instances.
237 # See `MongoRepository` for a concrete implementation example.
238 abstract class JsonRepository[E
: Serializable]
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
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
)
257 private class RepoSerializer
260 # Remove caching when saving refs to db
261 redef fun serialize_reference
(object
) do serialize object
264 # A Repository that uses MongoDB as backend.
268 # import popcorn::pop_repos
270 # # First, let's create a User abstraction:
272 # # Serializable user representation.
281 # var password: String is writable
283 # redef fun to_s do return login
286 # # We then need to subclass the `MongoRepository` to provide User specific services:
288 # # User repository for Mongo
290 # super MongoRepository[User]
292 # # Find a user by its login
293 # fun find_by_login(login: String): nullable User do
294 # var q = new JsonObject
300 # # The repository can then be used with User instances:
303 # var mongo = new MongoClient("mongodb://localhost:27017/")
304 # var db = mongo.database("tests")
305 # var coll = db.collection("test_pop_repo_{100000.rand}")
307 # # Create a user repo to store User instances
308 # var repo = new UserRepo(coll)
310 # # Create some users
311 # repo.save(new User("Morriar", "1234"))
312 # repo.save(new User("Alex", "password"))
314 # assert repo.find_all.length == 2
315 # assert repo.find_by_login("Morriar").password == "1234"
317 # assert repo.find_all.length == 0
319 class MongoRepository[E
: Serializable]
320 super JsonRepository[E
]
322 redef type QUERY: JsonObject
324 # MongoDB collection used to store objects
325 var collection
: MongoCollection
327 redef fun find_by_id
(id
) do
328 var query
= new JsonObject
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
)
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
)
347 redef fun count
(query
) do
348 return collection
.count
(query
or else new JsonObject)
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
)
357 redef fun remove_by_id
(id
) do
358 var query
= new JsonObject
363 redef fun remove
(query
) do
364 return collection
.remove
(query
or else new JsonObject)
367 redef fun remove_all
(query
) do
368 return collection
.remove_all
(query
or else new JsonObject)
371 redef fun clear
do return collection
.drop
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
385 # Base serializable entity that can go into a JsonRepository
387 # Provide boiler plate implementation of all object serializable to json.
389 # `id` is used as a primary key for `find_by_id`.
391 # Subclassing RepoObject makes it easy to create a serializable class:
393 # import popcorn::pop_repos
404 # Do not forget the `serialize` annotation else the fields will not be serialized.
406 # It is also possible to redefine the `id` primary key to use your own:
408 # import popcorn::pop_repos
414 # redef var id = "order-{get_time}"
420 abstract class RepoObject
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"
430 # Base object comparison on ID
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
436 redef fun hash
do return id
.hash
437 redef fun to_s
do return id
440 # JsonObject can be used as a `RepositoryQuery`.
443 redef class JsonObject
444 super RepositoryQuery