Merge branch 'master' of github.com:Araq/Nimrod

This commit is contained in:
Araq
2012-11-03 15:57:29 +01:00
9 changed files with 741 additions and 16 deletions

7
.gitignore vendored
View File

@@ -16,12 +16,17 @@ compiler/pas2nim/nimcache
misc
doc/*.html
doc/*.idx
web/upload/*.html
/web/upload
koch
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
examples/cross_todo/nimrod_commandline/nimtodo
# iOS specific wildcards.
*.mode1v3

View File

@@ -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.

View 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

View 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.

View 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()

View File

@@ -0,0 +1,347 @@
# Implements a command line interface against the backend.
import backend
import db_sqlite
import os
import parseopt
import parseutils
import strutils
import times
const
USAGE = """nimtodo - Nimrod cross platform todo manager
Usage:
nimtodo [command] [list options]
Commands:
-a=int text Adds a todo entry with the specified priority and text.
-c=int Marks the specified todo entry as done.
-u=int Marks the specified todo entry as not done.
-d=int|all Deletes a single entry from the database, or all entries.
-g Generates some rows with values for testing.
-l Lists the contents of the database.
-h, --help shows this help
List options (optional):
-p=+|- Sorts list by ascending|desdencing priority. Default:desdencing.
-m=+|- Sorts list by ascending|desdencing date. Default:desdencing.
-t Show checked entries. By default they are not shown.
-z Hide unchecked entries. By default they are shown.
Examples:
nimtodo -a=4 Water the plants
nimtodo -c:87
nimtodo -d:2
nimtodo -d:all
nimtodo -l -p=+ -m=- -t
"""
type
TCommand = enum # The possible types of commands
commandAdd # The user wants to add a new todo entry.
commandCheck # User wants to check a todo entry.
commandUncheck # User wants to uncheck a todo entry.
commandDelete # User wants to delete a single todo entry.
commandNuke # User wants to purge all database entries.
commandGenerate # Add random rows to the database, for testing.
commandList # User wants to list contents.
TParamConfig = object of TObject
# Structure containing the parsed options from the commandline.
command: TCommand # Store the type of operation
addPriority: int # Only valid with commandAdd, stores priority.
addText: seq[string] # Only valid with commandAdd, stores todo text.
todoId: int64 # The todo id for operations like check or delete.
listParams: TPagedParams # Uses the backend structure directly for params.
proc initDefaults(params: var TParamConfig) =
## Initialises defaults value in the structure.
##
## Most importantly we want to have an empty list for addText.
params.listParams.initDefaults
params.addText = @[]
proc parseCmdLine(): TParamConfig =
## Parses the commandline.
##
## Returns a TParamConfig structure filled with the proper values or directly
## calls quit() with the appropriate error message.
var
specifiedCommand = false
usesListParams = false
p = initOptParser()
key, val: TaintedString
newId: biggestInt
result.initDefaults
try:
while true:
next(p)
key = p.key
val = p.val
case p.kind
of cmdArgument:
if specifiedCommand and commandAdd == result.command:
result.addText.add(key)
else:
stdout.write(USAGE)
quit("Argument ($1) detected without add command." % [key], 1)
of cmdLongOption, cmdShortOption:
case normalize(key)
of "help", "h":
stdout.write(USAGE)
quit(0)
of "a":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
result.command = commandAdd
result.addPriority = val.parseInt
specifiedCommand = true
of "c":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
result.command = commandCheck
let numChars = string(val).parseBiggestInt(newId)
if numChars < 1: raise newException(EInvalidValue, "Empty string?")
result.todoId = newId
specifiedCommand = true
of "u":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
result.command = commandUncheck
let numChars = val.parseBiggestInt(newId)
if numChars < 1: raise newException(EInvalidValue, "Empty string?")
result.todoId = newId
specifiedCommand = true
of "d":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
if "all" == val:
result.command = commandNuke
else:
result.command = commandDelete
let numChars = val.parseBiggestInt(newId)
if numChars < 1:
raise newException(EInvalidValue, "Empty string?")
result.todoId = newId
specifiedCommand = true
of "g":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
if val.len > 0:
stdout.write(USAGE)
quit("Unexpected value '$1' for switch l." % [val], 3)
result.command = commandGenerate
specifiedCommand = true
of "l":
if specifiedCommand:
stdout.write(USAGE)
quit("Only one command can be specified at a time! ($1)" % [val], 2)
else:
if val.len > 0:
stdout.write(USAGE)
quit("Unexpected value '$1' for switch l." % [val], 3)
result.command = commandList
specifiedCommand = true
of "p":
usesListParams = true
if "+" == val:
result.listParams.priorityAscending = true
elif "-" == val:
result.listParams.priorityAscending = false
else:
stdout.write(USAGE)
quit("Priority parameter ($1) should be + or |." % [val], 4)
of "m":
usesListParams = true
if "+" == val:
result.listParams.dateAscending = true
elif "-" == val:
result.listParams.dateAscending = false
else:
stdout.write(USAGE)
quit("Date parameter ($1) should be + or |." % [val], 4)
of "t":
usesListParams = true
if val.len > 0:
stdout.write(USAGE)
quit("Unexpected value '$1' for switch t." % [val], 5)
result.listParams.showChecked = true
of "z":
usesListParams = true
if val.len > 0:
stdout.write(USAGE)
quit("Unexpected value '$1' for switch z." % [val], 5)
result.listParams.showUnchecked = false
else:
stdout.write(USAGE)
quit("Unexpected option '$1'." % [key], 6)
of cmdEnd:
break
except EInvalidValue:
stdout.write(USAGE)
quit("Invalid int value '$1' for parameter '$2'." % [val, key], 7)
if not specifiedCommand:
stdout.write(USAGE)
quit("Didn't specify any command.", 8)
if commandAdd == result.command and result.addText.len < 1:
stdout.write(USAGE)
quit("Used the add command, but provided no text/description.", 9)
if usesListParams and commandList != result.command:
stdout.write(USAGE)
quit("Used list options, but didn't specify the list command.", 10)
proc generateDatabaseRows(conn: TDbConn) =
## Adds some rows to the database ignoring errors.
discard conn.addTodo(1, "Watch another random youtube video")
discard conn.addTodo(2, "Train some starcraft moves for the league")
discard conn.addTodo(3, "Spread the word about Nimrod")
discard conn.addTodo(4, "Give fruit superavit to neighbours")
var todo = conn.addTodo(4, "Send tax form through snail mail")
todo.isDone = true
discard todo.save(conn)
discard conn.addTodo(1, "Download new anime to watch")
todo = conn.addTodo(2, "Build train model from scraps")
todo.isDone = true
discard todo.save(conn)
discard conn.addTodo(5, "Buy latest Britney Spears album")
discard conn.addTodo(6, "Learn a functional programming language")
echo("Generated some entries, they were added to your database.")
proc listDatabaseContents(conn: TDbConn; listParams: TPagedParams) =
## Dumps the database contents formatted to the standard output.
##
## Pass the list/filter parameters parsed from the commandline.
var params = listParams
params.pageSize = -1
let todos = conn.getPagedTodos(params)
if todos.len < 1:
echo("Database empty")
return
echo("Todo id, is done, priority, last modification date, text:")
# First detect how long should be our columns for formatting.
var cols: array[0..2, int]
for todo in todos:
cols[0] = max(cols[0], ($todo.getId).len)
cols[1] = max(cols[1], ($todo.priority).len)
cols[2] = max(cols[2], ($todo.getModificationDate).len)
# Now dump all the rows using the calculated alignment sizes.
for todo in todos:
echo("$1 $2 $3, $4, $5" % [
($todo.getId).align(cols[0]),
if todo.isDone: "[X]" else: "[-]",
($todo.priority).align(cols[1]),
($todo.getModificationDate).align(cols[2]),
todo.text])
proc deleteOneTodo(conn: TDbConn; todoId: int64) =
## Deletes a single todo entry from the database.
let numDeleted = conn.deleteTodo(todoId)
if numDeleted > 0:
echo("Deleted todo id " & $todoId)
else:
quit("Couldn't delete todo id " & $todoId, 11)
proc deleteAllTodos(conn: TDbConn) =
## Deletes all the contents from the database.
##
## Note that it would be more optimal to issue a direct DELETE sql statement
## on the database, but for the sake of the example we will restrict
## ourselfves to the API exported by backend.
var
counter: int64
params: TPagedParams
params.initDefaults
params.pageSize = -1
params.showUnchecked = true
params.showChecked = true
let todos = conn.getPagedTodos(params)
for todo in todos:
if conn.deleteTodo(todo.getId) > 0:
counter += 1
else:
quit("Couldn't delete todo id " & $todo.getId, 12)
echo("Deleted $1 todo entries from database." % $counter)
proc setTodoCheck(conn: TDbConn; todoId: int64; value: bool) =
## Changes the check state of a todo entry to the specified value.
let
newState = if value: "checked" else: "unchecked"
todo = conn.getTodo(todoId)
if todo == nil:
quit("Can't modify todo id $1, its not in the database." % $todoId, 13)
if todo[].isDone == value:
echo("Todo id $1 was already set to $2." % [$todoId, newState])
return
todo[].isDone = value
if todo[].save(conn):
echo("Todo id $1 set to $2." % [$todoId, newState])
else:
quit("Error updating todo id $1 to $2." % [$todoId, newState])
proc addTodo(conn: TDbConn; priority: int; tokens: seq[string]) =
## Adds to the database a todo with the specified priority.
##
## The tokens are joined as a single string using the space character. The
## created id will be displayed to the user.
let todo = conn.addTodo(priority, tokens.join(" "))
echo("Created todo entry with id:$1 for priority $2 and text '$3'." % [
$todo.getId, $todo.priority, todo.text])
when isMainModule:
## Main entry point.
let
opt = parseCmdLine()
dbPath = getConfigDir() / "nimtodo.sqlite3"
if not dbPath.existsFile:
createDir(getConfigDir())
echo("No database found at $1, it will be created for you." % dbPath)
let conn = openDatabase(dbPath)
try:
case opt.command
of commandAdd: addTodo(conn, opt.addPriority, opt.addText)
of commandCheck: setTodoCheck(conn, opt.todoId, true)
of commandUncheck: setTodoCheck(conn, opt.todoId, false)
of commandDelete: deleteOneTodo(conn, opt.todoId)
of commandNuke: deleteAllTodos(conn)
of commandGenerate: generateDatabaseRows(conn)
of commandList: listDatabaseContents(conn, opt.listParams)
finally:
conn.close

View File

@@ -0,0 +1,18 @@
This directory contains the nimrod commandline version of the todo cross
platform example.
The commandline interface can be used only through switches, running the binary
once will spit out the basic help. The commands you can use are the typical on
such an application: add, check/uncheck and delete (further could be added,
like modification at expense of parsing/option complexity). The list command is
the only one which dumps the contents of the database. The output can be
filtered and sorted through additional parameters.
When you run the program for the first time the todo database will be generated
in your user's data directory. To cope with an empty database, a special
generation switch can be used to fill the database with some basic todo entries
you can play with.
Compilation of the interface is fairly easy, just include the path to the
backend in your compilation command. A basic build.sh is provided for unix like
platforms with the correct parameters.

View 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.

View File

@@ -1,26 +1,52 @@
# Test the SDL interface:
import
SDL
sdl, sdl_image, colors
var
screen, greeting: PSurface
r: TRect
event: TEvent
bgColor = colChocolate.int32
if Init(INIT_VIDEO) == 0:
screen = SetVideoMode(640, 480, 16, SWSURFACE or ANYFORMAT)
if screen == nil:
write(stdout, "screen is nil!\n")
else:
greeting = LoadBmp("backgrnd.bmp")
if greeting == nil:
write(stdout, "greeting is nil!")
r.x = 0'i16
r.y = 0'i16
discard blitSurface(greeting, nil, screen, addr(r))
discard flip(screen)
Delay(3000)
if init(INIT_VIDEO) != 0:
quit "SDL failed to initialize!"
screen = SetVideoMode(640, 480, 16, SWSURFACE or ANYFORMAT)
if screen.isNil:
quit($sdl.getError())
greeting = IMG_load("tux.png")
if greeting.isNil:
echo "Failed to load tux.png"
else:
write(stdout, "SDL_Init failed!\n")
## convert the image to alpha and free the old one
var s = greeting.displayFormatAlpha()
swap(greeting, s)
s.freeSurface()
r.x = 0
r.y = 0
block game_loop:
while true:
while pollEvent(addr event) > 0:
case event.kind
of QUITEV:
break game_loop
of KEYDOWN:
if EvKeyboard(addr event).keysym.sym == K_ESCAPE:
break game_loop
else:
discard
discard fillRect(screen, nil, bgColor)
discard blitSurface(greeting, nil, screen, addr r)
discard flip(screen)
greeting.freeSurface()
screen.freeSurface()
sdl.Quit()
## fowl wuz here 10/2012