mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
Add tests for examples from Nim in Action.
This commit is contained in:
26
tests/niminaction/Chapter3/ChatApp/readme.markdown
Normal file
26
tests/niminaction/Chapter3/ChatApp/readme.markdown
Normal 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.
|
||||
54
tests/niminaction/Chapter3/ChatApp/src/client.nim
Normal file
54
tests/niminaction/Chapter3/ChatApp/src/client.nim
Normal 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()
|
||||
1
tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg
Normal file
1
tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg
Normal file
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
55
tests/niminaction/Chapter3/ChatApp/src/protocol.nim
Normal file
55
tests/niminaction/Chapter3/ChatApp/src/protocol.nim
Normal 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
|
||||
|
||||
84
tests/niminaction/Chapter3/ChatApp/src/server.nim
Normal file
84
tests/niminaction/Chapter3/ChatApp/src/server.nim
Normal 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)
|
||||
79
tests/niminaction/Chapter6/WikipediaStats/concurrency.nim
Normal file
79
tests/niminaction/Chapter6/WikipediaStats/concurrency.nim
Normal 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)
|
||||
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
29
tests/niminaction/Chapter6/WikipediaStats/naive.nim
Normal file
29
tests/niminaction/Chapter6/WikipediaStats/naive.nim
Normal 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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
13
tests/niminaction/Chapter6/WikipediaStats/race_condition.nim
Normal file
13
tests/niminaction/Chapter6/WikipediaStats/race_condition.nim
Normal 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)
|
||||
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
--threads:on
|
||||
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")
|
||||
19
tests/niminaction/Chapter8/canvas/canvas.nim
Normal file
19
tests/niminaction/Chapter8/canvas/canvas.nim
Normal 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)
|
||||
19
tests/niminaction/Chapter8/canvas/canvas_test.nim
Normal file
19
tests/niminaction/Chapter8/canvas/canvas_test.nim
Normal 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()
|
||||
34
tests/niminaction/Chapter8/sdl/sdl.nim
Normal file
34
tests/niminaction/Chapter8/sdl/sdl.nim
Normal 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.}
|
||||
25
tests/niminaction/Chapter8/sdl/sdl_test.nim
Normal file
25
tests/niminaction/Chapter8/sdl/sdl_test.nim
Normal 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)
|
||||
26
tests/niminaction/Chapter8/sfml/sfml.nim
Normal file
26
tests/niminaction/Chapter8/sfml/sfml.nim
Normal 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()".}
|
||||
9
tests/niminaction/Chapter8/sfml/sfml_test.nim
Normal file
9
tests/niminaction/Chapter8/sfml/sfml_test.nim
Normal 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)
|
||||
84
tests/niminaction/Chapter9/configurator/configurator.nim
Normal file
84
tests/niminaction/Chapter9/configurator/configurator.nim
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user