mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
Adds backend code for the cross platform todo example.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,6 +22,10 @@ compiler/nimrod*
|
||||
build/[0-9]_[0-9]
|
||||
bin/nimrod
|
||||
examples/cross_calculator/nimrod_commandline/nimcalculator
|
||||
examples/cross_todo/nimrod_backend/*.html
|
||||
examples/cross_todo/nimrod_backend/backend
|
||||
examples/cross_todo/nimrod_backend/testbackend
|
||||
examples/cross_todo/nimrod_backend/todo.sqlite3
|
||||
|
||||
# iOS specific wildcards.
|
||||
*.mode1v3
|
||||
|
||||
@@ -9,3 +9,5 @@ platforms.
|
||||
To avoid duplication of code, the backend code lies in a separate directory and
|
||||
each platform compiles it with a different custom build process, usually
|
||||
generating C code in a temporary build directory.
|
||||
|
||||
For a more ellaborate and useful example see the cross_todo example.
|
||||
|
||||
222
examples/cross_todo/nimrod_backend/backend.nim
Normal file
222
examples/cross_todo/nimrod_backend/backend.nim
Normal file
@@ -0,0 +1,222 @@
|
||||
# Backend for a simple todo program with sqlite persistence.
|
||||
#
|
||||
# Most procs dealing with a TDbConn object may raise an EDb exception.
|
||||
|
||||
import db_sqlite
|
||||
import parseutils
|
||||
import strutils
|
||||
import times
|
||||
|
||||
|
||||
type
|
||||
TTodo* = object of TObject
|
||||
## A todo object holding the information serialized to the database.
|
||||
id: int64 ## Unique identifier of the object in the
|
||||
## database, use the getId() accessor to read it.
|
||||
text*: string ## Description of the task to do.
|
||||
priority*: int ## The priority can be any user defined integer.
|
||||
isDone*: bool ## Done todos are still kept marked.
|
||||
modificationDate: TTime ## The modification time can't be modified from
|
||||
## outside of this module, use the
|
||||
## getModificationDate accessor.
|
||||
|
||||
TPagedParams* = object of TObject
|
||||
## Contains parameters for a query, initialize default values with
|
||||
## initDefaults().
|
||||
pageSize*: int64 ## Lines per returned query page, -1 for
|
||||
## unlimited.
|
||||
priorityAscending*: bool ## Sort results by ascending priority.
|
||||
dateAscending*: bool ## Sort results by ascending modification date.
|
||||
showUnchecked*: bool ## Get unchecked objects.
|
||||
showChecked*: bool ## Get checked objects.
|
||||
|
||||
|
||||
# - General procs
|
||||
#
|
||||
proc initDefaults*(params: var TPagedParams) =
|
||||
## Sets sane defaults for a TPagedParams object.
|
||||
##
|
||||
## Note that you should always provide a non zero pageSize, either a specific
|
||||
## positive value or negative for unbounded query results.
|
||||
params.pageSize = high(int64)
|
||||
params.priorityAscending = false
|
||||
params.dateAscending = false
|
||||
params.showUnchecked = true
|
||||
params.showChecked = false
|
||||
|
||||
|
||||
proc openDatabase*(path: string): TDbConn =
|
||||
## Creates or opens the sqlite3 database.
|
||||
##
|
||||
## Pass the path to the sqlite database, if the database doesn't exist it
|
||||
## will be created. The proc may raise a EDB exception
|
||||
let
|
||||
conn = db_sqlite.open(path, "user", "pass", "db")
|
||||
query = sql"""CREATE TABLE IF NOT EXISTS Todos (
|
||||
id INTEGER PRIMARY KEY,
|
||||
priority INTEGER NOT NULL,
|
||||
is_done BOOLEAN NOT NULL,
|
||||
desc TEXT NOT NULL,
|
||||
modification_date INTEGER NOT NULL,
|
||||
CONSTRAINT Todos UNIQUE (id))"""
|
||||
|
||||
db_sqlite.exec(conn, query)
|
||||
result = conn
|
||||
|
||||
|
||||
# - Procs related to TTodo objects
|
||||
#
|
||||
proc initFromDB(id: int64; text: string; priority: int, isDone: bool;
|
||||
modificationDate: TTime): TTodo =
|
||||
## Returns an initialized TTodo object created from database parameters.
|
||||
##
|
||||
## The proc assumes all values are right. Note this proc is NOT exported.
|
||||
assert(id >= 0, "Identity identifiers should not be negative")
|
||||
result.id = id
|
||||
result.text = text
|
||||
result.priority = priority
|
||||
result.isDone = isDone
|
||||
result.modificationDate = modificationDate
|
||||
|
||||
|
||||
proc getId*(todo: TTodo): int64 =
|
||||
## Accessor returning the value of the private id property.
|
||||
return todo.id
|
||||
|
||||
|
||||
proc getModificationDate*(todo: TTodo): TTime =
|
||||
## Returns the last modification date of a TTodo entry.
|
||||
return todo.modificationDate
|
||||
|
||||
|
||||
proc update*(todo: var TTodo; conn: TDbConn): bool =
|
||||
## Checks the database for the object and refreshes its variables.
|
||||
##
|
||||
## Use this method if you (or another entity) have modified the database and
|
||||
## want to update the object you have with whatever the database has stored.
|
||||
## Returns true if the update suceeded, or false if the object was not found
|
||||
## in the database any more, in which case you should probably get rid of the
|
||||
## TTodo object.
|
||||
assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
|
||||
let query = sql"""SELECT desc, priority, is_done, modification_date
|
||||
FROM Todos WHERE id = ?"""
|
||||
|
||||
try:
|
||||
let rows = conn.GetAllRows(query, $todo.id)
|
||||
if len(rows) < 1:
|
||||
return
|
||||
assert(1 == len(rows), "Woah, didn't expect so many rows")
|
||||
todo.text = rows[0][0]
|
||||
todo.priority = rows[0][1].parseInt
|
||||
todo.isDone = rows[0][2].parseBool
|
||||
todo.modificationDate = TTime(rows[0][3].parseInt)
|
||||
result = true
|
||||
except:
|
||||
echo("Something went wrong selecting for id " & $todo.id)
|
||||
|
||||
|
||||
proc save*(todo: var TTodo; conn: TDbConn): bool =
|
||||
## Saves the current state of text, priority and isDone to the database.
|
||||
##
|
||||
## Returns true if the database object was updated (in which case the
|
||||
## modification date will have changed). The proc can return false if the
|
||||
## object wasn't found, for instance, in which case you should drop that
|
||||
## object anyway and create a new one with addTodo(). Also EDb can be raised.
|
||||
assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
|
||||
let
|
||||
currentDate = getTime()
|
||||
query = sql"""UPDATE Todos
|
||||
SET desc = ?, priority = ?, is_done = ?, modification_date = ?
|
||||
WHERE id = ?"""
|
||||
rowsUpdated = conn.execAffectedRows(query, $todo.text,
|
||||
$todo.priority, $todo.isDone, $int(currentDate), $todo.id)
|
||||
|
||||
if 1 == rowsUpdated:
|
||||
todo.modificationDate = currentDate
|
||||
result = true
|
||||
|
||||
|
||||
# - Procs dealing directly with the database
|
||||
#
|
||||
proc addTodo*(conn: TDbConn; priority: int; text: string): TTodo =
|
||||
## Inserts a new todo into the database.
|
||||
##
|
||||
## Returns the generated todo object. If there is an error EDb will be raised.
|
||||
let
|
||||
currentDate = getTime()
|
||||
query = sql"""INSERT INTO Todos
|
||||
(priority, is_done, desc, modification_date)
|
||||
VALUES (?, 'false', ?, ?)"""
|
||||
todoId = conn.insertId(query, priority, text, $int(currentDate))
|
||||
|
||||
result = initFromDB(todoId, text, priority, false, currentDate)
|
||||
|
||||
|
||||
proc deleteTodo*(conn: TDbConn; todoId: int64): int64 {.discardable.} =
|
||||
## Deletes the specified todo identifier.
|
||||
##
|
||||
## Returns the number of rows which were affected (1 or 0)
|
||||
let query = sql"""DELETE FROM Todos WHERE id = ?"""
|
||||
result = conn.execAffectedRows(query, $todoId)
|
||||
|
||||
|
||||
proc getNumEntries*(conn: TDbConn): int =
|
||||
## Returns the number of entries in the Todos table.
|
||||
##
|
||||
## If the function succeeds, returns the zero or positive value, if something
|
||||
## goes wrong a negative value is returned.
|
||||
let query = sql"""SELECT COUNT(id) FROM Todos"""
|
||||
try:
|
||||
let row = conn.getRow(query)
|
||||
result = row[0].parseInt
|
||||
except:
|
||||
echo("Something went wrong retrieving number of Todos entries")
|
||||
result = -1
|
||||
|
||||
|
||||
proc getPagedTodos*(conn: TDbConn; params: TPagedParams;
|
||||
page = 0'i64): seq[TTodo] =
|
||||
## Returns the todo entries for a specific page.
|
||||
##
|
||||
## Pages are calculated based on the params.pageSize parameter, which can be
|
||||
## set to a negative value to specify no limit at all. The query will be
|
||||
## affected by the TPagedParams, which should have sane values (call
|
||||
## initDefaults).
|
||||
assert(page >= 0, "You should request a page zero or bigger than zero")
|
||||
result = @[]
|
||||
|
||||
# Well, if you don't want to see anything, there's no point in asking the db.
|
||||
if not params.showUnchecked and not params.showChecked: return
|
||||
|
||||
let
|
||||
order_by = [
|
||||
if params.priorityAscending: "ASC" else: "DESC",
|
||||
if params.dateAscending: "ASC" else: "DESC"]
|
||||
|
||||
query = sql("""SELECT id, desc, priority, is_done, modification_date
|
||||
FROM Todos
|
||||
WHERE is_done = ? OR is_done = ?
|
||||
ORDER BY priority $1, modification_date $2, id DESC
|
||||
LIMIT ? * ?,?""" % order_by)
|
||||
|
||||
args = @[$params.showChecked, $(not params.showUnchecked),
|
||||
$params.pageSize, $page, $params.pageSize]
|
||||
|
||||
#echo("Query " & string(query))
|
||||
#echo("args: " & args.join(", "))
|
||||
|
||||
var newId: biggestInt
|
||||
for row in conn.fastRows(query, args):
|
||||
let numChars = row[0].parseBiggestInt(newId)
|
||||
assert(numChars > 0, "Huh, couldn't parse identifier from database?")
|
||||
result.add(initFromDB(int64(newId), row[1], row[2].parseInt,
|
||||
row[3].parseBool, TTime(row[4].parseInt)))
|
||||
|
||||
|
||||
proc getTodo*(conn: TDbConn; todoId: int64): ref TTodo =
|
||||
## Returns a reference to a TTodo or nil if the todo could not be found.
|
||||
var tempTodo: TTodo
|
||||
tempTodo.id = todoId
|
||||
if tempTodo.update(conn):
|
||||
new(result)
|
||||
result[] = tempTodo
|
||||
14
examples/cross_todo/nimrod_backend/readme.txt
Normal file
14
examples/cross_todo/nimrod_backend/readme.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
This directory contains the nimrod backend code for the todo cross platform
|
||||
example.
|
||||
|
||||
Unlike the cross platform calculator example, this backend features more code,
|
||||
using an sqlite database for storage. Also a basic test module is provided, not
|
||||
to be included with the final program but to test the exported functionality.
|
||||
The test is not embedded directly in the backend.nim file to avoid being able
|
||||
to access internal data types and procs not exported and replicate the
|
||||
environment of client code.
|
||||
|
||||
In a bigger project with several people you could run `nimrod doc backend.nim`
|
||||
(or use the doc2 command for a whole project) and provide the generated html
|
||||
documentation to another programer for her to implement an interface without
|
||||
having to look at the source code.
|
||||
86
examples/cross_todo/nimrod_backend/testbackend.nim
Normal file
86
examples/cross_todo/nimrod_backend/testbackend.nim
Normal file
@@ -0,0 +1,86 @@
|
||||
# Tests the backend code.
|
||||
|
||||
import backend
|
||||
import db_sqlite
|
||||
import strutils
|
||||
import times
|
||||
|
||||
|
||||
proc showPagedResults(conn: TDbConn; params: TPagedParams) =
|
||||
## Shows the contents of the database in pages of specified size.
|
||||
##
|
||||
## Hmm... I guess this is more of a debug proc which should be moved outside,
|
||||
## or to a commandline interface (hint).
|
||||
var
|
||||
page = 0'i64
|
||||
rows = conn.getPagedTodos(params)
|
||||
|
||||
while rows.len > 0:
|
||||
echo("page " & $page)
|
||||
for row in rows:
|
||||
echo("row id:$1, text:$2, priority:$3, done:$4, date:$5" % [$row.getId,
|
||||
$row.text, $row.priority, $row.isDone,
|
||||
$row.getModificationDate])
|
||||
# Query the database for the next page or quit.
|
||||
if params.pageSize > 0:
|
||||
page = page + 1
|
||||
rows = conn.getPagedTodos(params, page)
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
proc dumTest() =
|
||||
let conn = openDatabase("todo.sqlite3")
|
||||
try:
|
||||
let numTodos = conn.getNumEntries
|
||||
echo("Current database contains " & $numTodos & " todo items.")
|
||||
if numTodos < 10:
|
||||
# Fill some dummy rows if there are not many entries yet.
|
||||
discard conn.addTodo(3, "Filler1")
|
||||
discard conn.addTodo(4, "Filler2")
|
||||
|
||||
var todo = conn.addTodo(2, "Testing")
|
||||
echo("New todo added with id " & $todo.getId)
|
||||
|
||||
# Try changing it and updating the database.
|
||||
var clonedTodo = conn.getTodo(todo.getId)[]
|
||||
assert(clonedTodo.text == todo.text, "Should be equal")
|
||||
todo.text = "Updated!"
|
||||
todo.priority = 7
|
||||
todo.isDone = true
|
||||
if todo.save(conn):
|
||||
echo("Updated priority $1, done $2" % [$todo.priority, $todo.isDone])
|
||||
else:
|
||||
assert(false, "Uh oh, I wasn't expecting that!")
|
||||
|
||||
# Verify our cloned copy is different but can be updated.
|
||||
assert(clonedTodo.text != todo.text, "Should be different")
|
||||
discard clonedTodo.update(conn)
|
||||
assert(clonedTodo.text == todo.text, "Should be equal")
|
||||
|
||||
var params : TPagedParams
|
||||
params.initDefaults
|
||||
conn.showPagedResults(params)
|
||||
conn.deleteTodo(todo.getId)
|
||||
echo("Deleted rows for id 3? ")
|
||||
let res = conn.deleteTodo(todo.getId)
|
||||
echo("Deleted rows for id 3? " & $res)
|
||||
if todo.update(conn):
|
||||
echo("Later priority $1, done $2" % [$todo.priority, $todo.isDone])
|
||||
else:
|
||||
echo("Can't update object $1 from db!" % $todo.getId)
|
||||
|
||||
# Try to list content in a different way.
|
||||
params.pageSize = 5
|
||||
params.priorityAscending = true
|
||||
params.dateAscending = true
|
||||
params.showChecked = true
|
||||
conn.showPagedResults(params)
|
||||
finally:
|
||||
conn.close
|
||||
echo("Database closed")
|
||||
|
||||
|
||||
# Code that will be run only on the commandline.
|
||||
when isMainModule:
|
||||
dumTest()
|
||||
5
examples/cross_todo/readme.txt
Normal file
5
examples/cross_todo/readme.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
The cross platform todo illustrates how to use Nimrod to create a backend
|
||||
called by different native user interfaces.
|
||||
|
||||
This example builds on the knowledge learned from the cross_calculator example.
|
||||
Check it out first to learn how to set up nimrod on different platforms.
|
||||
Reference in New Issue
Block a user