Add tests for examples from Nim in Action.
This commit is contained in:
parent
a585748f27
commit
7889c03cbc
34 changed files with 1213 additions and 0 deletions
14
tests/niminaction/Chapter7/Tweeter/Tweeter.nimble
Normal file
14
tests/niminaction/Chapter7/Tweeter/Tweeter.nimble
Normal 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"
|
||||
117
tests/niminaction/Chapter7/Tweeter/public/style.css
Normal file
117
tests/niminaction/Chapter7/Tweeter/public/style.css
Normal 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%;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import database
|
||||
|
||||
var db = newDatabase()
|
||||
db.setup()
|
||||
echo("Database created successfully!")
|
||||
db.close()
|
||||
93
tests/niminaction/Chapter7/Tweeter/src/database.nim
Normal file
93
tests/niminaction/Chapter7/Tweeter/src/database.nim
Normal 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]))
|
||||
62
tests/niminaction/Chapter7/Tweeter/src/tweeter.nim
Normal file
62
tests/niminaction/Chapter7/Tweeter/src/tweeter.nim
Normal 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()
|
||||
51
tests/niminaction/Chapter7/Tweeter/src/views/general.nim
Normal file
51
tests/niminaction/Chapter7/Tweeter/src/views/general.nim
Normal 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
|
||||
49
tests/niminaction/Chapter7/Tweeter/src/views/user.nim
Normal file
49
tests/niminaction/Chapter7/Tweeter/src/views/user.nim
Normal 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
|
||||
28
tests/niminaction/Chapter7/Tweeter/tests/database_test.nim
Normal file
28
tests/niminaction/Chapter7/Tweeter/tests/database_test.nim
Normal 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!")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
--path:"../src"
|
||||
#switch("path", "./src")
|
||||
Loading…
Add table
Add a link
Reference in a new issue