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
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue