Repositories for data management.

Repositories are used to apply persistence on instances (or documents). Using repositories one can store and retrieve instance in a clean and maintenable way.

This module provides the base interface Repository that defines the persistence services available in all kind of repos. JsonRepository factorizes all repositories dedicated to Json data or objects serializable to Json.

MongoRepository is provided as a concrete example of repository. It implements all the services from Repository using a Mongo database as backend.

Repositories can be used in Popcorn app to manage your data persistence. Here an example with a book management app:

import popcorn
import popcorn::pop_repos
import popcorn::pop_json

# Serializable book representation.
class Book
    serialize

    # Book uniq ID
    var id: String = (new MongoObjectId).id is serialize_as "_id"

    # Book title
    var title: String

    # ... Other fields

    redef fun to_s do return title
    redef fun ==(o) do return o isa SELF and id == o.id
    redef fun hash do return id.hash
end

# We then need to subclass the `MongoRepository` to provide Book specific services.

# Book repository for Mongo
class BookRepo
    super MongoRepository[Book]

    # Find books by title
    fun find_by_title(title: String): Array[Book] do
        var q = new JsonObject
        q["title"] = title
        return find_all(q)
    end
end

# The repository can be used in a Handler to manage book in a REST API.

class BookHandler
    super Handler

    var repo: BookRepo

    # Return a json array of all books
    #
    # If the get parameters `title` is provided, returns a json array of books
    # matching the `title`.
    redef fun get(req, res) do
        var title = req.string_arg("title")
        if title == null then
            res.json new JsonArray.from(repo.find_all)
        else
            res.json new JsonArray.from(repo.find_by_title(title))
        end
    end

    # Insert a new Book
    redef fun post(req, res) do
        var title = req.string_arg("title")
        if title == null then
            res.error 400
            return
        end
        var book = new Book(title)
        repo.save book
        res.json book
    end
end

# Let's wrap it all together in a Popcorn app:

# Init database
var mongo = new MongoClient("mongodb://mongo:27017/")
var db = mongo.database("tests_app_{100000.rand}")
var coll = db.collection("books")

# Init app
var app = new App
var repo = new BookRepo(coll)
app.use("/books", new BookHandler(repo))
app.listen("localhost", 3000)

Introduced classes

abstract class JsonRepository[E: Serializable]

popcorn :: JsonRepository

A Repository for JsonObjects.
class MongoRepository[E: Serializable]

popcorn :: MongoRepository

A Repository that uses MongoDB as backend.
abstract class RepoObject

popcorn :: RepoObject

Base serializable entity that can go into a JsonRepository
interface Repository[E: Serializable]

popcorn :: Repository

A Repository is an object that can store serialized instances.
interface RepositoryQuery

popcorn :: RepositoryQuery

An abstract Query representation.

Redefined classes

redef class AppConfig

popcorn :: pop_repos $ AppConfig

Configuration file for Popcorn apps
redef abstract class Deserializer

popcorn :: pop_repos $ Deserializer

Abstract deserialization service
redef class JsonObject

popcorn :: pop_repos $ JsonObject

JsonObject can be used as a RepositoryQuery.

All class definitions

redef class AppConfig

popcorn :: pop_repos $ AppConfig

Configuration file for Popcorn apps
redef abstract class Deserializer

popcorn :: pop_repos $ Deserializer

Abstract deserialization service
redef class JsonObject

popcorn :: pop_repos $ JsonObject

JsonObject can be used as a RepositoryQuery.
abstract class JsonRepository[E: Serializable]

popcorn $ JsonRepository

A Repository for JsonObjects.
class MongoRepository[E: Serializable]

popcorn $ MongoRepository

A Repository that uses MongoDB as backend.
abstract class RepoObject

popcorn $ RepoObject

Base serializable entity that can go into a JsonRepository
interface Repository[E: Serializable]

popcorn $ Repository

A Repository is an object that can store serialized instances.
interface RepositoryQuery

popcorn $ RepositoryQuery

An abstract Query representation.
package_diagram popcorn::pop_repos pop_repos popcorn::pop_config pop_config popcorn::pop_repos->popcorn::pop_config mongodb::queries queries popcorn::pop_repos->mongodb::queries config config popcorn::pop_config->config mongodb mongodb mongodb::queries->mongodb ...config ... ...config->config ...mongodb ... ...mongodb->mongodb github::loader loader github::loader->popcorn::pop_repos popcorn::pop_tracker pop_tracker popcorn::pop_tracker->popcorn::pop_repos a_star-m a_star-m a_star-m->github::loader a_star-m->popcorn::pop_tracker a_star-m... ... a_star-m...->a_star-m

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module array

core :: array

This module introduces the standard array structure.
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module c

c :: c

Structures and services for compatibility with the C language
module caching

serialization :: caching

Services for caching serialization engines
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module config

config :: config

Configuration options for nit tools and apps
module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.
module engine_tools

serialization :: engine_tools

Advanced services for serialization engines
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module error

json :: error

Intro JsonParseError which is exposed by all JSON reading APIs
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module ini

ini :: ini

Read and write INI configuration files
module inspect

serialization :: inspect

Refine Serializable::inspect to show more useful information
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module json

json :: json

Read and write JSON formatted text using the standard serialization services
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module meta

meta :: meta

Simple user-defined meta-level to manipulate types of instances as object.
module mongodb

mongodb :: mongodb

MongoDB Nit Driver.
module native

core :: native

Native structures for text and bytes
module native_mongodb

mongodb :: native_mongodb

Native wrapper for the MongoDB C Driver
module numeric

core :: numeric

Advanced services for Numeric types
module opts

opts :: opts

Management of options on the command line
module parser_base

parser_base :: parser_base

Simple base for hand-made parsers of all kinds
module poset

poset :: poset

Pre order sets and partial order set (ie hierarchies)
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module ropes

core :: ropes

Tree-based representation of a String.
module safe

serialization :: safe

Services for safer deserialization engines
module serialization

serialization :: serialization

General serialization services
module serialization_core

serialization :: serialization_core

Abstract services to serialize Nit objects to different formats
module serialization_read

json :: serialization_read

Services to read JSON: deserialize_json and JsonDeserializer
module serialization_write

json :: serialization_write

Services to write Nit objects to JSON strings: serialize_to_json and JsonSerializer
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module static

json :: static

Static interface to read Nit objects from JSON strings
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module pop_config

popcorn :: pop_config

Configuration file and options for Popcorn apps
module queries

mongodb :: queries

Mongo queries framework

Children

Descendants

module a_star-m

a_star-m

# Repositories for data management.
#
# Repositories are used to apply persistence on instances (or **documents**).
# Using repositories one can store and retrieve instance in a clean and maintenable
# way.
#
# This module provides the base interface `Repository` that defines the persistence
# services available in all kind of repos.
# `JsonRepository` factorizes all repositories dedicated to Json data or objects
# serializable to Json.
#
# `MongoRepository` is provided as a concrete example of repository.
# It implements all the services from `Repository` using a Mongo database as backend.
#
# Repositories can be used in Popcorn app to manage your data persistence.
# Here an example with a book management app:
#
# ~~~nitish
# # First we declare the `Book` class. It has to be serializable so it can be used
# # within a `Repository`.
#
# import popcorn
# import popcorn::pop_repos
# import popcorn::pop_json
#
# # Serializable book representation.
# class Book
#	serialize
#
#	# Book uniq ID
#	var id: String = (new MongoObjectId).id is serialize_as "_id"
#
#	# Book title
#	var title: String
#
#	# ... Other fields
#
#	redef fun to_s do return title
#	redef fun ==(o) do return o isa SELF and id == o.id
#	redef fun hash do return id.hash
# end
#
# # We then need to subclass the `MongoRepository` to provide Book specific services.
#
# # Book repository for Mongo
# class BookRepo
#	super MongoRepository[Book]
#
#	# Find books by title
#	fun find_by_title(title: String): Array[Book] do
#		var q = new JsonObject
#		q["title"] = title
#		return find_all(q)
#	end
# end
#
# # The repository can be used in a Handler to manage book in a REST API.
#
# class BookHandler
#	super Handler
#
#	var repo: BookRepo
#
#	# Return a json array of all books
#	#
#	# If the get parameters `title` is provided, returns a json array of books
#	# matching the `title`.
#	redef fun get(req, res) do
#		var title = req.string_arg("title")
#		if title == null then
#			res.json new JsonArray.from(repo.find_all)
#		else
#			res.json new JsonArray.from(repo.find_by_title(title))
#		end
#	end
#
#	# Insert a new Book
#	redef fun post(req, res) do
#		var title = req.string_arg("title")
#		if title == null then
#			res.error 400
#			return
#		end
#		var book = new Book(title)
#		repo.save book
#		res.json book
#	end
# end
#
# # Let's wrap it all together in a Popcorn app:
#
# # Init database
# var mongo = new MongoClient("mongodb://mongo:27017/")
# var db = mongo.database("tests_app_{100000.rand}")
# var coll = db.collection("books")
#
# # Init app
# var app = new App
# var repo = new BookRepo(coll)
# app.use("/books", new BookHandler(repo))
# app.listen("localhost", 3000)
# ~~~
module pop_repos

import popcorn::pop_config
import serialization
import json
import mongodb::queries

redef class AppConfig

	# Default database host string for MongoDb
	var default_db_host = "mongodb://mongo:27017/"

	# Default database hostname
	var default_db_name = "popcorn"

	# MongoDb host name
	var opt_db_host = new OptionString("MongoDb host", "--db-host")

	# MongoDb database name
	var opt_db_name = new OptionString("MongoDb database name", "--db-name")

	# MongoDB server used for data persistence
	fun db_host: String do return opt_db_host.value or else ini["db.host"] or else default_db_host

	# MongoDB DB used for data persistence
	fun db_name: String do return opt_db_name.value or else ini["db.name"] or else default_db_name

	init do
		super
		add_option(opt_db_host, opt_db_name)
	end

	# Mongo db client
	var client = new MongoClient(db_host) is lazy

	# Mongo db instance
	var db: MongoDb = client.database(db_name) is lazy
end

# A Repository is an object that can store serialized instances.
#
# Repository is the base class of all kind of persistance processes. It offers
# the base CRUD services to save (add/update), find and delete instances.
#
# Instances are stored in their serialized form. See the `serialization` package
# for more documentation.
interface Repository[E: Serializable]

	# Kind of queries accepted
	#
	# Can be redefined to accept more precise queries depending on the backend used.
	type QUERY: RepositoryQuery

	# Find an instance by it's `id`
	#
	# `id` is an abstract thing at this stage
	# TODO maybe introduce the `PrimaryKey` concept?
	fun find_by_id(id: String): nullable E is abstract

	# Find an instance based on `query`
	fun find(query: QUERY): nullable E is abstract

	# Find all instances based on `query`
	#
	# Using `query` == null will retrieve all the document in the repository.
	fun find_all(query: nullable QUERY, skip, limit: nullable Int): Array[E] is abstract

	# Count instances that matches `query`
	fun count(query: nullable QUERY): Int is abstract

	# Save an `instance`
	fun save(instance: E): Bool is abstract

	# Remove the instance with `id`
	fun remove_by_id(id: String): Bool is abstract

	# Remove the instance based on `query`
	fun remove(query: nullable QUERY): Bool is abstract

	# Remove all the instances matching on `query`
	fun remove_all(query: nullable QUERY): Bool is abstract

	# Remove all instances
	fun clear: Bool is abstract

	# Serialize an `instance` to a String.
	fun serialize(instance: nullable E): nullable String is abstract

	# Deserialize a `string` to an instance.
	fun deserialize(string: nullable String): nullable E is abstract
end

# An abstract Query representation.
#
# Since the kind of query available depends on the database backend choice or
# implementation, this interface is used to provide a common type to all the
# queries.
#
# Redefine `Repository::QUERY` to use your own kind of query.
interface RepositoryQuery end

# A Repository for JsonObjects.
#
# As for document oriented databases, Repository can be used to store and retrieve
# Json object.
# Serialization from/to Json is used to translate from/to nit instances.
#
# See `MongoRepository` for a concrete implementation example.
abstract class JsonRepository[E: Serializable]
	super Repository[E]

	redef fun serialize(item) do
		if item == null then return null
		var stream = new StringWriter
		var serializer = new RepoSerializer(stream)
		serializer.serialize item
		stream.close
		return stream.to_s
	end

	redef fun deserialize(string) do
		if string == null then return null
		var deserializer = new JsonDeserializer(string)
		return deserializer.deserialize.as(E)
	end
end

private class RepoSerializer
	super JsonSerializer

	# Remove caching when saving refs to db
	redef fun serialize_reference(object) do serialize object
end

# A Repository that uses MongoDB as backend.
#
# ~~~nitish
# import popcorn
# import popcorn::pop_repos
# import popcorn::pop_json
#
# # First, let's create a User abstraction:
#
# # Serializable user representation.
# class User
#	super RepoObject
#	serialize
#
#	# User login
#	var login: String
#
#	# User password
#	var password: String is writable
#
#	redef fun to_s do return login
# end
#
# # We then need to subclass the `MongoRepository` to provide User specific services:
#
# # User repository for Mongo
# class UserRepo
#	super MongoRepository[User]
#
#	# Find a user by its login
#	fun find_by_login(login: String): nullable User do
#		var q = new JsonObject
#		q["login"] = login
#		return find(q)
#	end
# end
#
# # The repository can then be used with User instances:
#
# # Init database
# var mongo = new MongoClient("mongodb://mongo:27017/")
# var db = mongo.database("tests")
# var coll = db.collection("test_pop_repo_{100000.rand}")
#
# # Create a user repo to store User instances
# var repo = new UserRepo(coll)
#
# # Create some users
# repo.save(new User("Morriar", "1234"))
# repo.save(new User("Alex", "password"))
#
# assert repo.find_all.length == 2
# assert repo.find_by_login("Morriar").password == "1234"
# repo.clear
# assert repo.find_all.length == 0
# ~~~
class MongoRepository[E: Serializable]
	super JsonRepository[E]

	redef type QUERY: JsonObject

	# MongoDB collection used to store objects
	var collection: MongoCollection

	redef fun find_by_id(id) do
		var query = new JsonObject
		query["_id"] = id
		return find(query)
	end

	redef fun find(query) do
		var res = collection.find(query)
		if res == null then return null
		return deserialize(res.to_json)
	end

	redef fun find_all(query, skip, limit) do
		var res = new Array[E]
		for e in collection.find_all(query or else new JsonObject, skip, limit) do
			res.add deserialize(e.to_json).as(E)
		end
		return res
	end

	redef fun count(query) do
		return collection.count(query or else new JsonObject)
	end

	redef fun save(item) do
		var json = serialize(item).as(String)
		var obj = json.parse_json.as(JsonObject)
		return collection.save(obj)
	end

	redef fun remove_by_id(id) do
		var query = new JsonObject
		query["_id"] = id
		return remove(query)
	end

	redef fun remove(query) do
		return collection.remove(query or else new JsonObject)
	end

	redef fun remove_all(query) do
		return collection.remove_all(query or else new JsonObject)
	end

	redef fun clear do return collection.drop

	# Perform an aggregation query over the repo.
	fun aggregate(pipeline: JsonArray): Array[E] do
		var res = new Array[E]
		for obj in collection.aggregate(pipeline) do
			var instance = deserialize(obj.to_json)
			if instance == null then continue
			res.add instance
		end
		return res
	end
end

# Base serializable entity that can go into a JsonRepository
#
# Provide boiler plate implementation of all object serializable to json.
#
# `id` is used as a primary key for `find_by_id`.
#
# Subclassing RepoObject makes it easy to create a serializable class:
# ~~~
# import popcorn::pop_repos
#
# class Album
#	super RepoObject
#	serialize
#
#	var title: String
#	var price: Float
# end
# ~~~
#
# Do not forget the `serialize` annotation else the fields will not be serialized.
#
# It is also possible to redefine the `id` primary key to use your own:
# ~~~
# import popcorn::pop_repos
#
# class Order
#	super RepoObject
#	serialize
#
#	redef var id = "order-{get_time}"
#
#	# ...
#
# end
# ~~~
abstract class RepoObject
	serialize

	# `self` unique id.
	#
	# This attribute is serialized under the key `_id` to be used
	# as primary key by MongoDb
	var id: String = (new MongoObjectId).id is writable, serialize_as "_id"

	# Base object comparison on ID
	#
	# Because multiple deserialization can exists of the same instance,
	# we use the ID to determine if two object are the same.
	redef fun ==(o) do return o isa SELF and id == o.id

	redef fun hash do return id.hash
	redef fun to_s do return id
end

# JsonObject can be used as a `RepositoryQuery`.
#
# See `mongodb` lib.
redef class JsonObject
	super RepositoryQuery
end

lib/popcorn/pop_repos.nit:17,1--434,3