2022年11月11日、東京都中野区にあるJamstack推しで有名なchot.incでNimのハンズオンを行いました。
今回のこの時の教材に加筆・修正して、一般公開します!
はじめに
Pythonでは python3 -m http.server 8000
とすると、現在のディレクトリにあるファイルを配信するサーバーをローカルで起動することができます。
このコマンドをNimで実装することで、Nimの基本的な機能について学びましょう。
今回学べること
- NimでWebサーバーを建てられること
- NimでOSのファイルシステムを扱えること
- Nimの標準ライブラリのドキュメントの読み方
- Nimの3rdパーティのライブラリの使い方
- ソースコードからドキュメントを自動生成する方法
- ドキュメントの書き方
環境構築
Nimではchoosenim
というツールを使うと、PCに複数のバージョンをインストールして、パスが通るバージョンをコマンドから切り替えたり、最新バージョンをコマンドからインストールできるのでこれを使うのが便利です。
Windowsの人
-
ChoosenimのGithubから最新のバージョンのものを選び、Windows用の
choosenim-0.8.4_windows_amd64.exe
をダウンロードします。 -
choosenim-0.8.4_windows_amd64.exe
をchoosenim.exe
にリネームします。 - コマンドプロンプトやPowerShellから
choosenim.exe
をコマンドとして使えるので、これを使って次のコマンドを実行して最新版のインストールをします。
choosenim.exe stable
Linux / Intel Macの人
次のコマンドを実行します。
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
or
wget -qO - https://nim-lang.org/choosenim/init.sh | sh
M1 Macの人
M1Macではchoosenimを使えないので、Homebrewからインストールします。
brew install nim
プロジェクト作成
localserver
というディレクトリを作って、その中で作業することにします。
ディレクトリを作ったらその配下で次のコマンドを実行してください。
nimble init
対話型で聞かれるので、質問に答えていきます。
Package type?
では binary
を選択してください。
後はほとんどEnterでOK。
するとこのようなディレクトリ構造が自動生成されたと思います。
.
├── localserver.nimble
└── src
└── localserver.nim
Hello World
src/localserver.nim
の中身はこのようになっていると思います。
# This is just an example to get you started. A typical binary package
# uses this file as the main entry point of the application.
when isMainModule:
echo("Hello, World!")
このファイルを実行してみましょう
nim c -r src/localserver.nim
するとターミナルに Hello, World!
が表示されたと思います。このようなコマンドでNimはファイルを実行することができます。
httpserverを作る
標準ライブラリのasynchttpserver
に書いてる内容を元にサーバーを起動するプログラムを書きます。
https://nim-lang.org/docs/asynchttpserver.html
src/lib/server.nim
というファイルを作りましょう。
.
├── localserver.nimble
└── src
+ ├── lib
+ │ └── server.nim
├── localserver
└── localserver.nim
import std/asynchttpserver
import std/asyncdispatch
proc main() {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World", headers.newHttpHeaders())
server.listen(Port(8000)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
waitFor main()
そしてこのファイルを単体で実行すると、HTTPサーバーが起動します。
nim c -r src/lib/server.nim
ブラウザから http://localhost:8000 へアクセスすると、画面に「Hello World」が表示されます。
CLIアプリを作る
ではCLIコマンドの引数から起動するサーバーのポート番号を渡せるようにします。
NimではCLIアプリを作るのに非常に便利なcligenという3rdパーティライブラリがあるのでこれを使います。
nimbleコマンドでcligenをインストールします。
nimble install cligen -y
nimbleファイルに依存関係を追記します。
# Package
version = "0.1.0"
author = "Anonymous"
description = "A new awesome nimble package"
license = "MIT"
srcDir = "src"
bin = @["localserver"]
# Dependencies
requires "nim >= 1.6.10"
+ requires "cligen"
localserver.nim
の中身を以下のように書き換えます。
proc localserver() =
discard
when isMainModule:
import cligen
dispatch(localserver)
localserver.nim
に-h
を付けて起動してみましょう。
nim c -r src/localserver -h
すると以下のようなメッセージングが画面に表示されると思います。これはCLIコマンドとしての説明です。
Usage:
localserver [optional-params]
Options:
-h, --help print this cligen-erated help
--help-syntax advanced: prepend,plurals,..
ではlocalserver
関数の引数にポート番号をデフォルト値と共に書いて、更に与えられたポート番号をターミナルに表示させましょう。
proc localserver(port=8000) =
echo port
when isMainModule:
import cligen
dispatch(localserver)
この状態で-h
を付けて起動します。
nim c -r src/localserver -h
Usage:
localserver [optional-params]
Options:
-h, --help print this cligen-erated help
--help-syntax advanced: prepend,plurals,..
-p=, --port= int 8000 set port
表示されるメッセージが変わりました。port
についての説明が追加されています。
ヘルプの内容を編集する
以下のようにするとヘルプの内容を編集することができます。
import std/tables
proc localserver(port=8080) =
## ローカルでサーバーを起動するコマンドです
echo port
const HELP = {"port": "ここに指定したポート番号でサーバーが起動します"}.toTable()
when isMainModule:
import cligen
dispatch(localserver, help=HELP)
起動するとメッセージの内容が変わっていることがわかります。
nim c -r src/localserver -h
Usage:
localserver [optional-params]
ローカルでサーバーを起動するコマンドです
Options:
-h, --help print this cligen-erated help
--help-syntax advanced: prepend,plurals,..
-p=, --port= int 8000 ここに指定したポート番号でサーバーが起動します
コマンドライン引数からポート番号を渡す
ではコマンドライン引数からポート番号を渡してみましょう。
何も渡さず起動するとデフォルト値の8000が、数値を渡すとその数値がターミナルに表示され、数字以外を渡すとエラーが発生します。
nim c -r src/localserver
>> 8000
nim c -r src/localserver -p 7000
>> 7000
nim c -r src/localserver -p aaa
>> Bad value: "aaa" for option "p"; expecting int
Usage:
localserver [optional-params]
Options:
-h, --help print this cligen-erated help
--help-syntax advanced: prepend,plurals,..
-p=, --port= int 8000 set port
指定したポート番号でサーバーを起動する
コマンドライン引数からポート番号を渡せることはわかったので、HTTPサーバーに引数を渡せるようにします。
import std/tables
+ import std/asyncdispatch
+ import ./lib/server
proc localserver(port=8000) =
## ローカルでサーバーを起動するコマンドです
+ waitFor main(port)
const HELP = {"port": "ここに指定したポート番号でサーバーが起動します"}.toTable()
when isMainModule:
import cligen
dispatch(localserver, help=HELP)
import std/asynchttpserver
import std/asyncdispatch
+ proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World", headers.newHttpHeaders())
+ server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
- waitFor main()
実行するとそれぞれのポート番号でサーバーが起動することがわかります。
nim c -r src/localserver.nim -p 7000
nim c -r src/localserver.nim -p 8000
nim c -r src/localserver.nim -p 9000
ファイルの中身を読む
「ファイルの中身を読む」という処理はIOの処理です。
ここでは非同期でファイルの読み書きをするasyncfileライブラリを使います。
読み込まれるファイルのサンプルを作る
exampleディレクトリを作り、その中に以下のようなHTMLとCSSを作ります。
.
+ ├── example
+ │ ├── index.html
+ │ └── style.css
├── localserver.nimble
└── src
├── lib
│ ├── server
│ └── server.nim
├── localserver
└── localserver.nim
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="./style.css" rel="stylesheet">
<title>Document</title>
</head>
<body>
<main>
<div class="box1"></div>
<div class="box2"></div>
<div class="box3"></div>
</main>
</body>
</html>
.box1 {
height: 200px;
width: 200px;
margin: auto;
background-color: red;
}
.box1:hover {
background-color: blue;
}
.box2 {
height: 200px;
width: 200px;
margin: auto;
background-color: yellow;
}
.box2:hover {
background-color: red;
}
.box3 {
height: 200px;
width: 200px;
margin: auto;
background-color: blue;
}
.box3:hover {
background-color: green;
}
読み込んだファイルを画面に返す
ではserver.nim
の中にファイルを読み込み、画面に返して表示する処理を書いていきます。
import std/asynchttpserver
import std/asyncdispatch
+ import std/os
+ import std/asyncfile
proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
+ let filepath = getCurrentDir() / "example/index.html"
+ let file = openAsync(filepath, fmRead)
+ defer: file.close()
+ let data = file.readAll().await
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
+ await req.respond(Http200, data, headers.newHttpHeaders())
server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
起動して確認してみましょう。
nim c -r src/localserver
画面にHTMLファイルの中身が表示されました。
ファイルパスをURLパラメータから受け取る
ソースコードの中に文字列として固定値を入れていた example/index.html
をURLパラメータから受け取れるようにします。
また存在しないファイルパスが渡された時には404を返すようにします。
標準ライブラリasynchttpserver
のRequest
構造体やURI
構造体から値を取りだすことができます。
import std/asynchttpserver
import std/asyncdispatch
import std/os
import std/asyncfile
proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
+ let filepath = getCurrentDir() / req.url.path
+ if fileExists(filepath):
let file = openAsync(filepath, fmRead)
defer: file.close()
let data = file.readAll().await
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, data, headers.newHttpHeaders())
+ else:
+ let headers = {"Content-type": "text/plain; charset=utf-8"}
+ await req.respond(Http404, "", headers.newHttpHeaders())
server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
起動して、http://localhost:8000/example/index.html
にアクセスしてHTMLファイルの中身が表示されることを確認します。
また存在しないファイルパスにアクセスした時に404
になることを確認します。
MIMEタイプを判別してHTMLページとして表示する
ここまでは読み込んだファイルの中身がそのまま文字列として表示されていました。HTMLページとしてブラウザ上で描画できるようにします。
ブラウザはHTTPヘッダーのContent-Type
にあるMIMEタイプからファイルの種類を特定して描画します。
URLパラメータの拡張子からMIMEタイプを特定できるようにしましょう。
まずURLから拡張子を取り出します。
let path = req.url.path
echo path
# > /examples/index.html
#ドットで分割して配列にする
let pathArr = path.split(".")
echo pathArr
# > @["/example/index", "html"]
# 配列の一番最後を取りだす
let ext = pathArr[^1]
echo ext
# > "html"
標準ライブラリの mimetypes
ライブラリを使うと拡張子からMIMEタイプを得られます。
https://nim-lang.org/docs/mimetypes.html
import std/mimetypes
let ext = req.url.path.split(".")[^1]
let contentType = newMimetypes().getMimetype(ext)
echo contentType
最後にレスポンスヘッダーに Content-Type
をセットします。
let headers = newHttpHeaders()
headers["Content-Type"] = contentType
全体像としてこのようになります。
import std/asynchttpserver
import std/asyncdispatch
import std/os
import std/asyncfile
+ import std/mimetypes
+ import std/strutils
proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
let filepath = getCurrentDir() / req.url.path
if fileExists(filepath):
let file = openAsync(filepath, fmRead)
defer: file.close()
let data = file.readAll().await
echo (req.reqMethod, req.url, req.headers)
+ let ext = req.url.path.split(".")[^1]
+ let contentType = newMimetypes().getMimetype(ext)
- let headers = {"Content-type": "text/plain; charset=utf-8"}
+ let headers = newHttpHeaders()
+ headers["Content-Type"] = contentType
+ await req.respond(Http200, data, headers)
else:
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http404, "", headers.newHttpHeaders())
server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
起動するとHTMLとして描画されました。色の付いた正方形が3つ表示されています。これはCSSファイルについてもMIMEタイプの特定が正しく行われ、描画されていることを示しています。
ファイル一覧を表示する
ファイル単体での表示はできたので、ディレクトリへアクセスするとファイル一覧を表示できるようにしましょう。
ディレクトリかファイルかどうかはURLでの拡張子の有無で判別します。
現在のフォルダのファイル一覧を返す関数を作る
標準ライブラリのこの辺りの関数を使います。
os.walkDir…ファイル一覧をイテレーターで回す
https://nim-lang.org/docs/os.html#walkDir.i%2Cstring
os.PathComponent…ディレクトリにあるオブジェクトのタイプ
https://nim-lang.org/docs/os.html#PathComponent
strutils.contains…ある文字列にある文字列が含まれるかどうか
https://nim-lang.org/docs/strutils.html#contains%2Cstring%2Cstring
seq[T]…配列
https://nim-lang.org/docs/system.html#system-module-seqs
lib
ディレクトリの下にfile.nim
というファイルを新規作成しましょう。
.
├── example
│ ├── index.html
│ └── style.css
├── localserver.nimble
└── src
├── lib
+ │ ├── file.nim
│ ├── server
│ └── server.nim
├── localserver
└── localserver.nim
import std/os
import std/strutils
proc getFiles*(path:string):seq[string] =
let currentPath = getCurrentDir() / path
var files = newSeq[string]()
for row in walkDir(currentPath, relative=true):
# ディレクトリにあるものがディレクトリもしくは拡張子があるものの絶対パスを配列に追加していく
# →バイナリは含めない
if row.kind == pcDir or row.path.contains("."):
files.add(row.path)
return files
これをserver.nim
から呼び出します。
import std/asynchttpserver
import std/asyncdispatch
import std/os
import std/asyncfile
import std/mimetypes
import std/strutils
+ import ./file
proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
let filepath = getCurrentDir() / req.url.path
if fileExists(filepath):
let file = openAsync(filepath, fmRead)
defer: file.close()
let data = file.readAll().await
echo (req.reqMethod, req.url, req.headers)
let ext = req.url.path.split(".")[^1]
let contentType = newMimetypes().getMimetype(ext)
let headers = newHttpHeaders()
headers["Content-Type"] = contentType
await req.respond(Http200, data, headers)
else:
- let headers = {"Content-type": "text/plain; charset=utf-8"}
- await req.respond(Http404, "", headers.newHttpHeaders())
+ let files = getFiles(req.url.path)
+ let headers = newHttpHeaders()
+ await req.respond(Http200, $files, headers)
+ await req.respond(Http404, "")
server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
起動して http://localhost8080/example
にアクセスしてみましょう。
画面に @["index.html", "style.css"]
が表示されていると思います。
テンプレートエンジンを使って綺麗に表示する
NimにはSource Code Filters
という機能があり、これを使ってHTMLの中に変数を入れたりif文やfor文が使えます。
テンプレートエンジンとして使うことができます。
lib
ディレクトリの下にview.nim
というファイルを新規作成しましょう。
#? stdtmpl | standard
#proc displayView*(path:string, files:seq[string]): string =
<!DOCTYPE html>
<html lang="en">
<head>
<title>Current Directory Files</title>
</head>
<body>
# let urlPath = if path == "/": "" else: path
<h1>Directory listing for ${path}</h1>
<hr>
#if files.len > 0:
<ul>
#for file in files:
<li><a href="${urlPath}/${file}">${file}</a></li>
#end for
</ul>
#end if
<hr>
</body>
</html>
これをserver.nim
から呼び出します。
import std/asynchttpserver
import std/asyncdispatch
import std/os
import std/asyncfile
import std/mimetypes
import std/strutils
import ./file
+ import ./view
proc main*(port:int) {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
let filepath = getCurrentDir() / req.url.path
if fileExists(filepath):
let file = openAsync(filepath, fmRead)
defer: file.close()
let data = file.readAll().await
echo (req.reqMethod, req.url, req.headers)
let ext = req.url.path.split(".")[^1]
let contentType = newMimetypes().getMimetype(ext)
let headers = newHttpHeaders()
headers["Content-Type"] = contentType
await req.respond(Http200, data, headers)
else:
let files = getFiles(req.url.path)
+ let body = displayView(req.url.path, files)
let headers = newHttpHeaders()
await req.respond(Http200, body, headers)
server.listen(Port(port)) # or Port(8080) to hardcode the standard HTTP port.
let port = server.getPort
echo "test this with: curl localhost:" & $port.uint16 & "/"
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
await sleepAsync(500)
作ったコマンドをPCにインストールする
これで全ての処理が完成しました!PCにインストールして、ファイル単体で動かせるようにしましょう。
nimble install
localserver -h
localserver -p 8080
実行バイナリは ~/.nimble/bin/
にあります。
おまけ
Nimのソースコードからドキュメントを自動生成する
Nimではコマンド1発でソースコードからドキュメントを自動生成することができます。
今回作ったコマンドを使ってブラウザから見てみましょう。
M1 Macを使っている人は、インストール時にドキュメント生成する辺りのプログラムが正しくインストールできていない可能性があります。
エラー文で表示されている欠けているファイルをGithubから直接持ってくるか、Dockerを使ってください。
nim doc --project --index:on --outdir:docs src/localserver.nim
cd docs
localserver
http://localhost:8000/theindex.html にアクセス
今まで見てきたNimの標準ライブラリの公式ドキュメントもこの機能を使って作られています。
関数にコメントを書く
localserver/file.nim
のgetFile
関数にコメントを書いてみましょう。
- シャープ2つ「##」で始めた行がドキュメントコメントとして解釈されます。
- マークダウン記法で書けます。
- runnableExamplesの中のネストでサンプルのコードを書くことができます。
- runnableExamplesも引数の型の不一致、未定義変数の呼び出しなどでコンパイルエラーになります。
import std/os
import std/strutils
proc getFiles*(path:string):seq[string] =
## pathのディレクトリのファイル一覧を表示します
##
## バイナリは除外します
runnableExamples:
let files = getFiles("/path/to/dir")
echo files
# > @["subdir", "aaa.nim", "bbb.nim"]
let currentPath = getCurrentDir() / path
var files = newSeq[string]()
for row in walkDir(currentPath, relative=true):
# ディレクトリにあるものがディレクトリもしくは拡張子があるものの絶対パスを配列に追加していく
# →バイナリは含めない
if row.kind == pcDir or row.path.contains("."):
files.add(row.path)
return files
再度ドキュメント生成してブラウザから確認すると、コメントが反映されていることがわかります。
nim doc --project --index:on --outdir:docs src/localserver.nim
複雑なコマンドのショートカットを作る
nim doc --project --index:on --outdir:docs src/localserver.nim
これを何度も入力するのは大変です。nimbleファイルにはコマンドのショートカットをタスクとして登録することができます。NodeJSのpackage.json
のscripts
のところのようなものです。
# Package
version = "0.1.0"
author = "Anonymous"
description = "A new awesome nimble package"
license = "MIT"
srcDir = "src"
bin = @["localserver"]
# Dependencies
requires "nim >= 1.6.8"
requires "cligen"
+ task docs, "generate html documents":
+ let cmd = "nim doc --project --index:on --outdir:docs src/localserver.nim"
+ exec(cmd)
するとnimbleコマンドから呼び出すことができます。
nimble docs
登録したタスクはnimbleコマンドから確認することもできます。
nimble tasks
> docs generate html documents
ファイル自体にコメントを書く
ではドキュメントの整備に戻りまして、ファイル自体にコメントを書いていきます。
src/localserver.nim
の一番上に追記していきます。
## # local server
## 現在のディレクトリのファイルを返すサーバーです。
## ```sh
## localserver -p:8080
## > start server on http://localhost:8080
## ```
##
## このように`マークダウン`を書くことができます
## - aaa
## - bbb
## - ccc
import std/tables
import std/asyncdispatch
import ./lib/server
proc localserver(port=8000) =
...
おわり
このハンズオンではNimの基本的な文法、標準ライブラリの使い方、公式ドキュメントの読み方、3rdパーティライブラリの使い方、インストールの仕方、テンプレートエンジンからドキュメント生成まで触れました。
公式から提供されているエコシステムの充実さについて理解できたと思います。
このハンズオンをやった方はNimのエコシステムについて全部経験したので、「Nim完全に理解した!」と言っても大丈夫です。
これからもNimを使い続けてくれたら嬉しい限りです。
ありがとうございました。