Add tests for examples from Nim in Action.

This commit is contained in:
Dominik Picheta
2017-10-01 17:17:40 +01:00
parent a585748f27
commit 7889c03cbc
34 changed files with 1213 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# The ChatApp source code
This directory contains the ChatApp project, which is the project that is
created as part of Chapter 3 of the Nim in Action book.
To compile run:
```
nim c src/client
nim c src/server
```
You can then run the ``server`` in one terminal by executing ``./src/server``.
After doing so you can execute multiple clients in different terminals and have
them communicate via the server.
To execute a client, make sure to specify the server address and user name
on the command line:
```bash
./src/client localhost Peter
```
You should then be able to start typing in messages and sending them
by pressing the Enter key.

View File

@@ -0,0 +1,54 @@
import os, threadpool, asyncdispatch, asyncnet
import protocol
proc connect(socket: AsyncSocket, serverAddr: string) {.async.} =
## Connects the specified AsyncSocket to the specified address.
## Then receives messages from the server continuously.
echo("Connecting to ", serverAddr)
# Pause the execution of this procedure until the socket connects to
# the specified server.
await socket.connect(serverAddr, 7687.Port)
echo("Connected!")
while true:
# Pause the execution of this procedure until a new message is received
# from the server.
let line = await socket.recvLine()
# Parse the received message using ``parseMessage`` defined in the
# protocol module.
let parsed = parseMessage(line)
# Display the message to the user.
echo(parsed.username, " said ", parsed.message)
echo("Chat application started")
# Ensure that the correct amount of command line arguments was specified.
if paramCount() < 2:
# Terminate the client early with an error message if there was not
# enough command line arguments specified by the user.
quit("Please specify the server address, e.g. ./client localhost username")
# Retrieve the first command line argument.
let serverAddr = paramStr(1)
# Retrieve the second command line argument.
let username = paramStr(2)
# Initialise a new asynchronous socket.
var socket = newAsyncSocket()
# Execute the ``connect`` procedure in the background asynchronously.
asyncCheck connect(socket, serverAddr)
# Execute the ``readInput`` procedure in the background in a new thread.
var messageFlowVar = spawn stdin.readLine()
while true:
# Check if the ``readInput`` procedure returned a new line of input.
if messageFlowVar.isReady():
# If a new line of input was returned, we can safely retrieve it
# without blocking.
# The ``createMessage`` is then used to create a message based on the
# line of input. The message is then sent in the background asynchronously.
asyncCheck socket.send(createMessage(username, ^messageFlowVar))
# Execute the ``readInput`` procedure again, in the background in a
# new thread.
messageFlowVar = spawn stdin.readLine()
# Execute the asyncdispatch event loop, to continue the execution of
# asynchronous procedures.
asyncdispatch.poll()

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,55 @@
import json
type
Message* = object
username*: string
message*: string
MessageParsingError* = object of Exception
proc parseMessage*(data: string): Message {.raises: [MessageParsingError, KeyError].} =
var dataJson: JsonNode
try:
dataJson = parseJson(data)
except JsonParsingError:
raise newException(MessageParsingError, "Invalid JSON: " &
getCurrentExceptionMsg())
except:
raise newException(MessageParsingError, "Unknown error: " &
getCurrentExceptionMsg())
if not dataJson.hasKey("username"):
raise newException(MessageParsingError, "Username field missing")
result.username = dataJson["username"].getStr()
if result.username.len == 0:
raise newException(MessageParsingError, "Username field is empty")
if not dataJson.hasKey("message"):
raise newException(MessageParsingError, "Message field missing")
result.message = dataJson["message"].getStr()
if result.message.len == 0:
raise newException(MessageParsingError, "Message field is empty")
proc createMessage*(username, message: string): string =
result = $(%{
"username": %username,
"message": %message
}) & "\c\l"
when isMainModule:
block:
let data = """{"username": "dom", "message": "hello"}"""
let parsed = parseMessage(data)
doAssert parsed.message == "hello"
doAssert parsed.username == "dom"
# Test failure
block:
try:
let parsed = parseMessage("asdasd")
except MessageParsingError:
doAssert true
except:
doAssert false

View File

@@ -0,0 +1,84 @@
import asyncdispatch, asyncnet
type
Client = ref object
socket: AsyncSocket
netAddr: string
id: int
connected: bool
Server = ref object
socket: AsyncSocket
clients: seq[Client]
proc newServer(): Server =
## Constructor for creating a new ``Server``.
Server(socket: newAsyncSocket(), clients: @[])
proc `$`(client: Client): string =
## Converts a ``Client``'s information into a string.
$client.id & "(" & client.netAddr & ")"
proc processMessages(server: Server, client: Client) {.async.} =
## Loops while ``client`` is connected to this server, and checks
## whether as message has been received from ``client``.
while true:
# Pause execution of this procedure until a line of data is received from
# ``client``.
let line = await client.socket.recvLine()
# The ``recvLine`` procedure returns ``""`` (i.e. a string of length 0)
# when ``client`` has disconnected.
if line.len == 0:
echo(client, " disconnected!")
client.connected = false
# When a socket disconnects it must be closed.
client.socket.close()
return
# Display the message that was sent by the client.
echo(client, " sent: ", line)
# Send the message to other clients.
for c in server.clients:
# Don't send it to the client that sent this or to a client that is
# disconnected.
if c.id != client.id and c.connected:
await c.socket.send(line & "\c\l")
proc loop(server: Server, port = 7687) {.async.} =
## Loops forever and checks for new connections.
# Bind the port number specified by ``port``.
server.socket.bindAddr(port.Port)
# Ready the server socket for new connections.
server.socket.listen()
echo("Listening on localhost:", port)
while true:
# Pause execution of this procedure until a new connection is accepted.
let (netAddr, clientSocket) = await server.socket.acceptAddr()
echo("Accepted connection from ", netAddr)
# Create a new instance of Client.
let client = Client(
socket: clientSocket,
netAddr: netAddr,
id: server.clients.len,
connected: true
)
# Add this new instance to the server's list of clients.
server.clients.add(client)
# Run the ``processMessages`` procedure asynchronously in the background,
# this procedure will continuously check for new messages from the client.
asyncCheck processMessages(server, client)
# Check whether this module has been imported as a dependency to another
# module, or whether this module is the main module.
when isMainModule:
# Initialise a new server.
var server = newServer()
echo("Server initialised!")
# Execute the ``loop`` procedure. The ``waitFor`` procedure will run the
# asyncdispatch event loop until the ``loop`` procedure finishes executing.
waitFor loop(server)

View File

@@ -0,0 +1,79 @@
# See this page for info about the format https://wikitech.wikimedia.org/wiki/Analytics/Data/Pagecounts-all-sites
import tables, parseutils, strutils, threadpool
const filename = "pagecounts-20160101-050000"
type
Stats = ref object
projectName, pageTitle: string
requests, contentSize: int
proc `$`(stats: Stats): string =
"(projectName: $#, pageTitle: $#, requests: $#, contentSize: $#)" % [
stats.projectName, stats.pageTitle, $stats.requests, $stats.contentSize
]
proc parse(chunk: string): Stats =
# Each line looks like: en Main_Page 242332 4737756101
result = Stats(projectName: "", pageTitle: "", requests: 0, contentSize: 0)
var projectName = ""
var pageTitle = ""
var requests = ""
var contentSize = ""
for line in chunk.splitLines:
var i = 0
projectName.setLen(0)
i.inc parseUntil(line, projectName, Whitespace, i)
i.inc skipWhitespace(line, i)
pageTitle.setLen(0)
i.inc parseUntil(line, pageTitle, Whitespace, i)
i.inc skipWhitespace(line, i)
requests.setLen(0)
i.inc parseUntil(line, requests, Whitespace, i)
i.inc skipWhitespace(line, i)
contentSize.setLen(0)
i.inc parseUntil(line, contentSize, Whitespace, i)
i.inc skipWhitespace(line, i)
if requests.len == 0 or contentSize.len == 0:
# Ignore lines with either of the params that are empty.
continue
let requestsInt = requests.parseInt
if requestsInt > result.requests and projectName == "en":
result = Stats(
projectName: projectName,
pageTitle: pageTitle,
requests: requestsInt,
contentSize: contentSize.parseInt
)
proc readChunks(filename: string, chunksize = 1000000): Stats =
result = Stats(projectName: "", pageTitle: "", requests: 0, contentSize: 0)
var file = open(filename)
var responses = newSeq[FlowVar[Stats]]()
var buffer = newString(chunksize)
var oldBufferLen = 0
while not endOfFile(file):
let readSize = file.readChars(buffer, oldBufferLen, chunksize - oldBufferLen) + oldBufferLen
var chunkLen = readSize
while chunkLen >= 0 and buffer[chunkLen - 1] notin NewLines:
# Find where the last line ends
chunkLen.dec
responses.add(spawn parse(buffer[0 .. <chunkLen]))
oldBufferLen = readSize - chunkLen
buffer[0 .. <oldBufferLen] = buffer[readSize - oldBufferLen .. ^1]
for resp in responses:
let statistic = ^resp
if statistic.requests > result.requests:
result = statistic
file.close()
when isMainModule:
echo readChunks(filename)

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,64 @@
# See this page for info about the format https://wikitech.wikimedia.org/wiki/Analytics/Data/Pagecounts-all-sites
import tables, parseutils, strutils, threadpool, re
const filename = "pagecounts-20160101-050000"
type
Stats = ref object
projectName, pageTitle: string
requests, contentSize: int
proc `$`(stats: Stats): string =
"(projectName: $#, pageTitle: $#, requests: $#, contentSize: $#)" % [
stats.projectName, stats.pageTitle, $stats.requests, $stats.contentSize
]
proc parse(chunk: string): Stats =
# Each line looks like: en Main_Page 242332 4737756101
result = Stats(projectName: "", pageTitle: "", requests: 0, contentSize: 0)
var matches: array[4, string]
var reg = re"([^\s]+)\s([^\s]+)\s(\d+)\s(\d+)"
for line in chunk.splitLines:
let start = find(line, reg, matches)
if start == -1: continue
let requestsInt = matches[2].parseInt
if requestsInt > result.requests and matches[0] == "en":
result = Stats(
projectName: matches[0],
pageTitle: matches[1],
requests: requestsInt,
contentSize: matches[3].parseInt
)
proc readChunks(filename: string, chunksize = 1000000): Stats =
result = Stats(projectName: "", pageTitle: "", requests: 0, contentSize: 0)
var file = open(filename)
var responses = newSeq[FlowVar[Stats]]()
var buffer = newString(chunksize)
var oldBufferLen = 0
while not endOfFile(file):
let readSize = file.readChars(buffer, oldBufferLen, chunksize - oldBufferLen) + oldBufferLen
var chunkLen = readSize
while chunkLen >= 0 and buffer[chunkLen - 1] notin NewLines:
# Find where the last line ends
chunkLen.dec
responses.add(spawn parse(buffer[0 .. <chunkLen]))
oldBufferLen = readSize - chunkLen
buffer[0 .. <oldBufferLen] = buffer[readSize - oldBufferLen .. ^1]
echo("Spawns: ", responses.len)
for resp in responses:
let statistic = ^resp
if statistic.requests > result.requests:
result = statistic
file.close()
when isMainModule:
echo readChunks(filename)

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,29 @@
# See this page for info about the format https://wikitech.wikimedia.org/wiki/Analytics/Data/Pagecounts-all-sites
import tables, parseutils, strutils
const filename = "pagecounts-20150101-050000"
proc parse(filename: string): tuple[projectName, pageTitle: string,
requests, contentSize: int] =
# Each line looks like: en Main_Page 242332 4737756101
var file = open(filename)
for line in file.lines:
var i = 0
var projectName = ""
i.inc parseUntil(line, projectName, Whitespace, i)
i.inc
var pageTitle = ""
i.inc parseUntil(line, pageTitle, Whitespace, i)
i.inc
var requests = 0
i.inc parseInt(line, requests, i)
i.inc
var contentSize = 0
i.inc parseInt(line, contentSize, i)
if requests > result[2] and projectName == "en":
result = (projectName, pageTitle, requests, contentSize)
file.close()
when isMainModule:
echo parse(filename)

View File

@@ -0,0 +1,72 @@
import os, parseutils, threadpool, strutils
type
Stats = ref object
domainCode, pageTitle: string
countViews, totalSize: int
proc newStats(): Stats =
Stats(domainCode: "", pageTitle: "", countViews: 0, totalSize: 0)
proc `$`(stats: Stats): string =
"(domainCode: $#, pageTitle: $#, countViews: $#, totalSize: $#)" % [
stats.domainCode, stats.pageTitle, $stats.countViews, $stats.totalSize
]
proc parse(line: string, domainCode, pageTitle: var string,
countViews, totalSize: var int) =
if line.len == 0: return
var i = 0
domainCode.setLen(0)
i.inc parseUntil(line, domainCode, {' '}, i)
i.inc
pageTitle.setLen(0)
i.inc parseUntil(line, pageTitle, {' '}, i)
i.inc
countViews = 0
i.inc parseInt(line, countViews, i)
i.inc
totalSize = 0
i.inc parseInt(line, totalSize, i)
proc parseChunk(chunk: string): Stats =
result = newStats()
var domainCode = ""
var pageTitle = ""
var countViews = 0
var totalSize = 0
for line in splitLines(chunk):
parse(line, domainCode, pageTitle, countViews, totalSize)
if domainCode == "en" and countViews > result.countViews:
result = Stats(domainCode: domainCode, pageTitle: pageTitle,
countViews: countViews, totalSize: totalSize)
proc readPageCounts(filename: string, chunkSize = 1_000_000) =
var file = open(filename)
var responses = newSeq[FlowVar[Stats]]()
var buffer = newString(chunksize)
var oldBufferLen = 0
while not endOfFile(file):
let reqSize = chunksize - oldBufferLen
let readSize = file.readChars(buffer, oldBufferLen, reqSize) + oldBufferLen
var chunkLen = readSize
while chunkLen >= 0 and buffer[chunkLen - 1] notin NewLines:
chunkLen.dec
responses.add(spawn parseChunk(buffer[0 .. <chunkLen]))
oldBufferLen = readSize - chunkLen
buffer[0 .. <oldBufferLen] = buffer[readSize - oldBufferLen .. ^1]
var mostPopular = newStats()
for resp in responses:
let statistic = ^resp
if statistic.countViews > mostPopular.countViews:
mostPopular = statistic
echo("Most popular is: ", mostPopular)
when isMainModule:
const file = "pagecounts-20160101-050000"
let filename = getCurrentDir() / file
readPageCounts(filename)

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,13 @@
import threadpool
var counter = 0
proc increment(x: int) =
for i in 0 .. <x:
let value = counter + 1
counter = value
spawn increment(10_000)
spawn increment(10_000)
sync()
echo(counter)

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,34 @@
import os, parseutils
proc parse(line: string, domainCode, pageTitle: var string,
countViews, totalSize: var int) =
var i = 0
domainCode.setLen(0)
i.inc parseUntil(line, domainCode, {' '}, i)
i.inc
pageTitle.setLen(0)
i.inc parseUntil(line, pageTitle, {' '}, i)
i.inc
countViews = 0
i.inc parseInt(line, countViews, i)
i.inc
totalSize = 0
i.inc parseInt(line, totalSize, i)
proc readPageCounts(filename: string) =
var domainCode = ""
var pageTitle = ""
var countViews = 0
var totalSize = 0
var mostPopular = ("", "", 0, 0)
for line in filename.lines:
parse(line, domainCode, pageTitle, countViews, totalSize)
if domainCode == "en" and countViews > mostPopular[2]:
mostPopular = (domainCode, pageTitle, countViews, totalSize)
echo("Most popular is: ", mostPopular)
when isMainModule:
const file = "pagecounts-20160101-050000"
let filename = getCurrentDir() / file
readPageCounts(filename)

View File

@@ -0,0 +1,15 @@
import threadpool, locks
var counterLock: Lock
initLock(counterLock)
var counter {.guard: counterLock.} = 0
proc increment(x: int) =
for i in 0 .. <x:
let value = counter + 1
counter = value
spawn increment(10_000)
spawn increment(10_000)
sync()
echo(counter)

View File

@@ -0,0 +1 @@
--threads:on

View File

@@ -0,0 +1,14 @@
# Package
version = "0.1.0"
author = "Dominik Picheta"
description = "A simple Twitter clone developed in Nim in Action."
license = "MIT"
bin = @["tweeter"]
skipExt = @["nim"]
# Dependencies
requires "nim >= 0.13.1"
requires "jester >= 0.0.1"

View File

@@ -0,0 +1,117 @@
body {
background-color: #f1f9ea;
margin: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
div#main {
width: 80%;
margin-left: auto;
margin-right: auto;
}
div#user {
background-color: #66ac32;
width: 100%;
color: #c7f0aa;
padding: 5pt;
}
div#user > h1 {
color: #ffffff;
}
h1 {
margin: 0;
display: inline;
padding-left: 10pt;
padding-right: 10pt;
}
div#user > form {
float: right;
margin-right: 10pt;
}
div#user > form > input[type="submit"] {
border: 0px none;
padding: 5pt;
font-size: 108%;
color: #ffffff;
background-color: #515d47;
border-radius: 5px;
cursor: pointer;
}
div#user > form > input[type="submit"]:hover {
background-color: #538c29;
}
div#messages {
background-color: #a2dc78;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #1a1a1a;
}
div#messages > div {
border-left: 1px solid #869979;
border-right: 1px solid #869979;
border-bottom: 1px solid #869979;
padding: 5pt;
}
div#messages > div > a, div#messages > div > span {
color: #475340;
}
div#messages > div > a:hover {
text-decoration: none;
color: #c13746;
}
h3 {
margin-bottom: 0;
font-weight: normal;
}
div#login {
width: 200px;
margin-left: auto;
margin-right: auto;
margin-top: 20%;
font-size: 130%;
}
div#login span.small {
display: block;
font-size: 56%;
}
div#newMessage {
background-color: #538c29;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #ffffff;
padding: 5pt;
}
div#newMessage span {
padding-right: 5pt;
}
div#newMessage form {
display: inline;
}
div#newMessage > form > input[type="text"] {
width: 80%;
}
div#newMessage > form > input[type="submit"] {
font-size: 80%;
}

View File

@@ -0,0 +1,6 @@
import database
var db = newDatabase()
db.setup()
echo("Database created successfully!")
db.close()

View File

@@ -0,0 +1,93 @@
import times, db_sqlite, strutils #<1>
type #<2>
Database* = ref object
db*: DbConn
User* = object #<3>
username*: string #<4>
following*: seq[string] #<5>
Message* = object #<6>
username*: string #<7>
time*: Time #<8>
msg*: string #<9>
proc newDatabase*(filename = "tweeter.db"): Database =
new result
result.db = open(filename, "", "", "")
proc close*(database: Database) =
database.db.close()
proc setup*(database: Database) =
database.db.exec(sql"""
CREATE TABLE IF NOT EXISTS User(
username text PRIMARY KEY
);
""")
database.db.exec(sql"""
CREATE TABLE IF NOT EXISTS Following(
follower text,
followed_user text,
PRIMARY KEY (follower, followed_user),
FOREIGN KEY (follower) REFERENCES User(username),
FOREIGN KEY (followed_user) REFERENCES User(username)
);
""")
database.db.exec(sql"""
CREATE TABLE IF NOT EXISTS Message(
username text,
time integer,
msg text NOT NULL,
FOREIGN KEY (username) REFERENCES User(username)
);
""")
proc post*(database: Database, message: Message) =
if message.msg.len > 140: #<1>
raise newException(ValueError, "Message has to be less than 140 characters.")
database.db.exec(sql"INSERT INTO Message VALUES (?, ?, ?);", #<2>
message.username, $message.time.toSeconds().int, message.msg) #<3>
proc follow*(database: Database, follower: User, user: User) =
database.db.exec(sql"INSERT INTO Following VALUES (?, ?);",#<2>
follower.username, user.username)
proc create*(database: Database, user: User) =
database.db.exec(sql"INSERT INTO User VALUES (?);", user.username) #<2>
proc findUser*(database: Database, username: string, user: var User): bool =
let row = database.db.getRow(
sql"SELECT username FROM User WHERE username = ?;", username)
if row[0].len == 0: return false
else: user.username = row[0]
let following = database.db.getAllRows(
sql"SELECT followed_user FROM Following WHERE follower = ?;", username)
user.following = @[]
for row in following:
if row[0].len != 0:
user.following.add(row[0])
return true
proc findMessages*(database: Database, usernames: seq[string],
limit = 10): seq[Message] =
result = @[]
if usernames.len == 0: return
var whereClause = " WHERE "
for i in 0 .. <usernames.len:
whereClause.add("username = ? ")
if i != <usernames.len:
whereClause.add("or ")
let messages = database.db.getAllRows(
sql("SELECT username, time, msg FROM Message" &
whereClause &
"ORDER BY time DESC LIMIT " & $limit),
usernames)
for row in messages:
result.add(Message(username: row[0], time: fromSeconds(row[1].parseInt), msg: row[2]))

View File

@@ -0,0 +1,62 @@
import asyncdispatch, times
import jester
import database, views/user, views/general
proc userLogin(db: Database, request: Request, user: var User): bool =
if request.cookies.hasKey("username"):
if not db.findUser(request.cookies["username"], user):
user = User(username: request.cookies["username"], following: @[])
db.create(user)
return true
else:
return false
let db = newDatabase()
routes:
get "/":
var user: User
if db.userLogin(request, user):
let messages = db.findMessages(user.following & user.username)
resp renderMain(renderTimeline(user.username, messages))
else:
resp renderMain(renderLogin())
get "/@name":
cond '.' notin @"name"
var user: User
if not db.findUser(@"name", user):
halt "User not found"
let messages = db.findMessages(@[user.username])
var currentUser: User
if db.userLogin(request, currentUser):
resp renderMain(renderUser(user, currentUser) & renderMessages(messages))
else:
resp renderMain(renderUser(user) & renderMessages(messages))
post "/follow":
var follower: User
var target: User
if not db.findUser(@"follower", follower):
halt "Follower not found"
if not db.findUser(@"target", target):
halt "Follow target not found"
db.follow(follower, target)
redirect(uri("/" & @"target"))
post "/login":
setCookie("username", @"username", getTime().getGMTime() + 2.hours)
redirect("/")
post "/createMessage":
let message = Message(
username: @"username",
time: getTime(),
msg: @"message"
)
db.post(message)
redirect("/")
runForever()

View File

@@ -0,0 +1,51 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
#import "../database"
#import user
#import xmltree
#
#proc `$!`(text: string): string = escape(text)
#end proc
#
#proc renderMain*(body: string): string =
# result = ""
<!DOCTYPE html>
<html>
<head>
<title>Tweeter written in Nim</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
${body}
</body>
</html>
#end proc
#
#proc renderLogin*(): string =
# result = ""
<div id="login">
<span>Login</span>
<span class="small">Please type in your username...</span>
<form action="login" method="post">
<input type="text" name="username">
<input type="submit" value="Login">
</form>
</div>
#end proc
#
#proc renderTimeline*(username: string, messages: seq[Message]): string =
# result = ""
<div id="user">
<h1>${$!username}'s timeline</h1>
</div>
<div id="newMessage">
<span>New message</span>
<form action="createMessage" method="post">
<input type="text" name="message">
<input type="hidden" name="username" value="${$!username}">
<input type="submit" value="Tweet">
</form>
</div>
${renderMessages(messages)}
#end proc

View File

@@ -0,0 +1,49 @@
#? stdtmpl(subsChar = '$', metaChar = '#', toString = "xmltree.escape")
#import "../database"
#import xmltree
#import times
#
#proc renderUser*(user: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
</div>
#end proc
#
#proc renderUser*(user: User, currentUser: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
#if user.username notin currentUser.following:
<form action="follow" method="post">
<input type="hidden" name="follower" value="${currentUser.username}">
<input type="hidden" name="target" value="${user.username}">
<input type="submit" value="Follow">
</form>
#end if
</div>
#
#end proc
#
#proc renderMessages*(messages: seq[Message]): string =
# result = ""
<div id="messages">
#for message in messages:
<div>
<a href="/${message.username}">${message.username}</a>
<span>${message.time.getGMTime().format("HH:mm MMMM d',' yyyy")}</span>
<h3>${message.msg}</h3>
</div>
#end for
</div>
#end proc
#
#when isMainModule:
# echo renderUser(User(username: "d0m96<>", following: @[]))
# echo renderMessages(@[
# Message(username: "d0m96", time: getTime(), msg: "Hello World!"),
# Message(username: "d0m96", time: getTime(), msg: "Testing")
# ])
#end when

View File

@@ -0,0 +1,28 @@
import database, os, times
when isMainModule:
removeFile("tweeter_test.db")
var db = newDatabase("tweeter_test.db")
db.setup()
db.create(User(username: "d0m96"))
db.create(User(username: "nim_lang"))
db.post(Message(username: "nim_lang", time: getTime() - 4.seconds,
msg: "Hello Nim in Action readers"))
db.post(Message(username: "nim_lang", time: getTime(),
msg: "99.9% off Nim in Action for everyone, for the next minute only!"))
var dom: User
doAssert db.findUser("d0m96", dom)
var nim: User
doAssert db.findUser("nim_lang", nim)
db.follow(dom, nim)
doAssert db.findUser("d0m96", dom)
let messages = db.findMessages(dom.following)
echo(messages)
doAssert(messages[0].msg == "99.9% off Nim in Action for everyone, for the next minute only!")
doAssert(messages[1].msg == "Hello Nim in Action readers")
echo("All tests finished successfully!")

View File

@@ -0,0 +1,2 @@
--path:"../src"
#switch("path", "./src")

View File

@@ -0,0 +1,19 @@
import dom
type
CanvasRenderingContext* = ref object
fillStyle* {.importc.}: cstring
strokeStyle* {.importc.}: cstring
{.push importcpp.}
proc getContext*(canvasElement: Element,
contextType: cstring): CanvasRenderingContext
proc fillRect*(context: CanvasRenderingContext, x, y, width, height: int)
proc moveTo*(context: CanvasRenderingContext, x, y: int)
proc lineTo*(context: CanvasRenderingContext, x, y: int)
proc stroke*(context: CanvasRenderingContext)

View File

@@ -0,0 +1,19 @@
import canvas, dom
proc onLoad() {.exportc.} =
var canvas = document.getElementById("canvas").EmbedElement
canvas.width = window.innerWidth
canvas.height = window.innerHeight
var ctx = canvas.getContext("2d")
ctx.fillStyle = "#1d4099"
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight)
ctx.strokeStyle = "#ffffff"
let letterWidth = 100
let letterLeftPos = (window.innerWidth div 2) - (letterWidth div 2)
ctx.moveTo(letterLeftPos, 320)
ctx.lineTo(letterLeftPos, 110)
ctx.lineTo(letterLeftPos + letterWidth, 320)
ctx.lineTo(letterLeftPos + letterWidth, 110)
ctx.stroke()

View File

@@ -0,0 +1,34 @@
when defined(Windows):
const libName* = "SDL2.dll"
elif defined(Linux):
const libName* = "libSDL2.so"
elif defined(MacOsX):
const libName* = "libSDL2.dylib"
type
SdlWindow = object
SdlWindowPtr* = ptr SdlWindow
SdlRenderer = object
SdlRendererPtr* = ptr SdlRenderer
const INIT_VIDEO* = 0x00000020
{.push dynlib: libName.}
proc init*(flags: uint32): cint {.importc: "SDL_Init".}
proc createWindowAndRenderer*(width, height: cint, window_flags: cuint,
window: var SdlWindowPtr, renderer: var SdlRendererPtr): cint
{.importc: "SDL_CreateWindowAndRenderer".}
proc pollEvent*(event: pointer): cint {.importc: "SDL_PollEvent".}
proc setDrawColor*(renderer: SdlRendererPtr, r, g, b, a: uint8): cint
{.importc: "SDL_SetRenderDrawColor", discardable.}
proc present*(renderer: SdlRendererPtr) {.importc: "SDL_RenderPresent".}
proc clear*(renderer: SdlRendererPtr) {.importc: "SDL_RenderClear".}
proc drawLines*(renderer: SdlRendererPtr, points: ptr tuple[x, y: cint],
count: cint): cint {.importc: "SDL_RenderDrawLines", discardable.}
{.pop.}

View File

@@ -0,0 +1,25 @@
import os
import sdl
if sdl.init(INIT_VIDEO) == -1:
quit("Couldn't initialise SDL")
var window: SdlWindowPtr
var renderer: SdlRendererPtr
if createWindowAndRenderer(640, 480, 0, window, renderer) == -1:
quit("Couldn't create a window or renderer")
discard pollEvent(nil)
renderer.setDrawColor 29, 64, 153, 255
renderer.clear
renderer.setDrawColor 255, 255, 255, 255
var points = [
(260'i32, 320'i32),
(260'i32, 110'i32),
(360'i32, 320'i32),
(360'i32, 110'i32)
]
renderer.drawLines(addr points[0], points.len.cint)
renderer.present
sleep(5000)

View File

@@ -0,0 +1,26 @@
{.passL: "-lsfml-graphics -lsfml-system -lsfml-window".}
type
VideoMode* {.importcpp: "sf::VideoMode".} = object
RenderWindowObj {.importcpp: "sf::RenderWindow".} = object
RenderWindow* = ptr RenderWindowObj
Color* {.importcpp: "sf::Color".} = object
Event* {.importcpp: "sf::Event".} = object
{.push cdecl, header: "<SFML/Graphics.hpp>".}
proc videoMode*(modeWidth, modeHeight: cuint, modeBitsPerPixel: cuint = 32): VideoMode
{.importcpp: "sf::VideoMode(@)", constructor.}
proc newRenderWindow*(mode: VideoMode, title: cstring): RenderWindow
{.importcpp: "new sf::RenderWindow(@)", constructor.}
proc pollEvent*(window: RenderWindow, event: var Event): bool
{.importcpp: "#.pollEvent(@)".}
proc newColor*(red, green, blue, alpha: uint8): Color
{.importcpp: "sf::Color(@)", constructor.}
proc clear*(window: RenderWindow, color: Color) {.importcpp: "#.clear(@)".}
proc display*(window: RenderWindow) {.importcpp: "#.display()".}

View File

@@ -0,0 +1,9 @@
import sfml, os
var window = newRenderWindow(videoMode(800, 600), "SFML works!")
var event: Event
discard window.pollEvent(event)
window.clear(newColor(29, 64, 153, 255))
window.display()
sleep(1000)

View File

@@ -0,0 +1,84 @@
import macros
proc createRefType(ident: NimIdent, identDefs: seq[NimNode]): NimNode =
result = newTree(nnkTypeSection,
newTree(nnkTypeDef,
newIdentNode(ident),
newEmptyNode(),
newTree(nnkRefTy,
newTree(nnkObjectTy,
newEmptyNode(),
newEmptyNode(),
newTree(nnkRecList,
identDefs
)
)
)
)
)
proc toIdentDefs(stmtList: NimNode): seq[NimNode] =
expectKind(stmtList, nnkStmtList)
result = @[]
for child in stmtList:
expectKind(child, nnkCall)
result.add(newIdentDefs(child[0], child[1][0]))
template constructor(ident: untyped): untyped =
proc `new ident`(): `ident` =
new result
proc createLoadProc(typeName: NimIdent, identDefs: seq[NimNode]): NimNode =
var cfgIdent = newIdentNode("cfg")
var filenameIdent = newIdentNode("filename")
var objIdent = newIdentNode("obj")
var body = newStmtList()
body.add quote do:
var `objIdent` = parseFile(`filenameIdent`)
for identDef in identDefs:
let fieldNameIdent = identDef[0]
let fieldName = $fieldNameIdent.ident
case $identDef[1].ident
of "string":
body.add quote do:
`cfgIdent`.`fieldNameIdent` = `objIdent`[`fieldName`].getStr
of "int":
body.add quote do:
`cfgIdent`.`fieldNameIdent` = `objIdent`[`fieldName`].getNum().int
else:
doAssert(false, "Not Implemented")
return newProc(newIdentNode("load"),
[newEmptyNode(),
newIdentDefs(cfgIdent, newIdentNode(typeName)),
newIdentDefs(filenameIdent, newIdentNode("string"))],
body)
macro config*(typeName: untyped, fields: untyped): untyped =
result = newStmtList()
let identDefs = toIdentDefs(fields)
result.add createRefType(typeName.ident, identDefs)
result.add getAst(constructor(typeName.ident))
result.add createLoadProc(typeName.ident, identDefs)
echo treeRepr(typeName)
echo treeRepr(fields)
echo treeRepr(result)
echo toStrLit(result)
# TODO: Verify that we can export fields in config type so that it can be
# used in another module.
import json
config MyAppConfig:
address: string
port: int
var myConf = newMyAppConfig()
myConf.load("myappconfig.cfg")
echo("Address: ", myConf.address)
echo("Port: ", myConf.port)

View File

@@ -238,6 +238,48 @@ proc jsTests(r: var TResults, cat: Category, options: string) =
for testfile in ["strutils", "json", "random", "times", "logging"]:
test "lib/pure/" & testfile & ".nim"
# ------------------------- nim in action -----------
proc testNimInAction(r: var TResults, cat: Category, options: string) =
template test(filename: untyped, action: untyped) =
testSpec r, makeTest(filename, options, cat, action)
template testJS(filename: untyped) =
testSpec r, makeTest(filename, options, cat, actionCompile, targetJS)
template testCPP(filename: untyped) =
testSpec r, makeTest(filename, options, cat, actionCompile, targetCPP)
let tests = [
"niminaction/Chapter3/ChatApp/src/server",
"niminaction/Chapter3/ChatApp/src/client",
"niminaction/Chapter6/WikipediaStats/concurrency_regex",
"niminaction/Chapter6/WikipediaStats/concurrency",
"niminaction/Chapter6/WikipediaStats/naive",
"niminaction/Chapter6/WikipediaStats/parallel_counts",
"niminaction/Chapter6/WikipediaStats/race_condition",
"niminaction/Chapter6/WikipediaStats/sequential_counts",
"niminaction/Chapter7/Tweeter/src/tweeter",
"niminaction/Chapter7/Tweeter/src/createDatabase",
"niminaction/Chapter7/Tweeter/tests/database_test",
"niminaction/Chapter8/sdl/sdl_test",
]
for testfile in tests:
test "tests/" & testfile & ".nim", actionCompile
# TODO: This doesn't work for some reason ;\
# let reject = "tests/niminaction/Chapter6/WikipediaStats" &
# "/unguarded_access.nim"
# test reject, actionReject
let jsFile = "tests/niminaction/Chapter8/canvas/canvas_test.nim"
testJS jsFile
let cppFile = "tests/niminaction/Chapter8/sfml/sfml_test.nim"
testCPP cppFile
# ------------------------- manyloc -------------------------------------------
#proc runSpecialTests(r: var TResults, options: string) =
# for t in ["lib/packages/docutils/highlite"]:
@@ -420,6 +462,8 @@ proc processCategory(r: var TResults, cat: Category, options: string) =
testNimblePackages(r, cat, pfExtraOnly)
of "nimble-all":
testNimblePackages(r, cat, pfAll)
of "niminaction":
testNimInAction(r, cat, options)
of "untestable":
# We can't test it because it depends on a third party.
discard # TODO: Move untestable tests to someplace else, i.e. nimble repo.