├── .gitattributes
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── action.yml
│ └── docs.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── azure-pipelines.yml
├── changelog.md
├── docs
├── assets
│ ├── openapi
│ │ └── docs.jpg
│ └── quickstart
│ │ └── hello.jpg
├── cli.md
├── configure.md
├── context.md
├── deployment.md
├── errorhandler.md
├── event.md
├── extendctx.md
├── faq.md
├── headers.md
├── index.md
├── middleware.md
├── mocking.md
├── openapi.md
├── quickstart.md
├── request.md
├── response.md
├── routing.md
├── server.md
├── session.md
├── staticfiles.md
├── uploadfile.md
├── validation.md
├── views.md
└── websocket.md
├── examples
├── basic
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── myctx.nim
│ ├── readme.md
│ ├── urls.nim
│ └── views.nim
├── basicconf
│ ├── .config
│ │ ├── config.custom.json
│ │ ├── config.debug.json
│ │ ├── config.json
│ │ └── config.production.json
│ ├── app.nim
│ ├── config.nims
│ ├── readme.md
│ ├── urls.nim
│ └── views.nim
├── blog
│ ├── .env
│ ├── README.md
│ ├── app.nim
│ ├── blog.db
│ ├── config.nims
│ ├── consts.nim
│ ├── initdb.nim
│ ├── schema.sql
│ ├── screenshot
│ │ └── screenshot.jpg
│ ├── static
│ │ ├── blob.svg
│ │ ├── layout.css
│ │ └── main.css
│ ├── templates
│ │ ├── editpost.nim
│ │ ├── index.nim
│ │ ├── login.nim
│ │ ├── register.nim
│ │ └── share
│ │ │ ├── head.nim
│ │ │ └── nav.nim
│ ├── urls.nim
│ └── views.nim
├── csrf
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── csrf.nimf
│ ├── urls.nim
│ └── views.nim
├── helloworld
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── docs
│ │ └── openapi.json
│ ├── static
│ │ ├── hello.html
│ │ ├── index.html
│ │ └── upload.html
│ ├── templates
│ │ └── basic.nim
│ ├── urls.nim
│ └── views.nim
├── memorysession
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── urls.nim
│ └── views.nim
├── redissession
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── urls.nim
│ └── views.nim
├── signedcookiesession
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── urls.nim
│ └── views.nim
├── todoapp
│ ├── app.nim
│ ├── config.nims
│ ├── readme.md
│ ├── templates
│ │ ├── karax.license
│ │ ├── style.css
│ │ ├── todoapp.html
│ │ ├── todoapp.js
│ │ ├── todoapp.nim
│ │ └── todoapp.nims
│ └── todoapp.nims
├── todolist
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── readme.md
│ ├── templates
│ │ └── basic.nim
│ ├── urls.nim
│ └── views.nim
└── websocket
│ ├── .env
│ ├── app.nim
│ ├── config.nims
│ ├── readme.md
│ ├── urls.nim
│ └── views.nim
├── mkdocs.yml
├── prologue.nimble
├── requirements.txt
├── src
├── prologue.nim
└── prologue
│ ├── auth.nim
│ ├── auth
│ ├── auth.nim
│ └── users.nim
│ ├── cache
│ ├── cache.nim
│ ├── lfucache.nim
│ └── lrucache.nim
│ ├── core
│ ├── application.nim
│ ├── async
│ │ └── sync.nim
│ ├── basicregex.nim
│ ├── beast
│ │ ├── request.nim
│ │ └── server.nim
│ ├── configure.nim
│ ├── constants.nim
│ ├── contenttype.nim
│ ├── context.nim
│ ├── encode.nim
│ ├── form.nim
│ ├── group.nim
│ ├── httpcore
│ │ └── httplogue.nim
│ ├── httpexception.nim
│ ├── middlewaresbase.nim
│ ├── naive
│ │ ├── request.nim
│ │ └── server.nim
│ ├── nativesettings.nim
│ ├── pages.nim
│ ├── request.nim
│ ├── response.nim
│ ├── route.nim
│ ├── server.nim
│ ├── types.nim
│ ├── uid.nim
│ ├── urandom.nim
│ └── utils.nim
│ ├── i18n.nim
│ ├── i18n
│ └── i18n.nim
│ ├── middlewares.nim
│ ├── middlewares
│ ├── auth.nim
│ ├── clickjacking.nim
│ ├── cors.nim
│ ├── csrf.nim
│ ├── memorysession.nim
│ ├── middlewares.nim
│ ├── redissession.nim
│ ├── sessions
│ │ ├── memorysession.nim
│ │ ├── redissession.nim
│ │ ├── sessionsbase.nim
│ │ ├── signedcookiesession.nim
│ │ └── sqlsession.nim
│ ├── signedcookiesession.nim
│ ├── staticfile.nim
│ ├── staticfilevirtualpath.nim
│ └── utils.nim
│ ├── mocking.nim
│ ├── mocking
│ └── mocking.nim
│ ├── openapi.nim
│ ├── openapi
│ └── openapi.nim
│ ├── plog
│ └── plog.nim
│ ├── plugin.nim
│ ├── security.nim
│ ├── security
│ ├── hasher.nim
│ └── security.nim
│ ├── signing.nim
│ ├── signing
│ ├── signing.nim
│ └── signingbase.nim
│ ├── validater.nim
│ ├── validater
│ ├── basic.nim
│ └── validater.nim
│ ├── websocket.nim
│ └── websocket
│ └── websocket.nim
└── tests
├── assets
├── i18n
│ └── trans.ini
├── static
│ ├── test.txt
│ └── upload.html
└── tassets.nim
├── benchmark
├── b_httpx.nim
├── b_jester.nim
├── b_logue.nim
├── b_stdlib.nim
├── readme.md
└── tbenchmark.nim
├── compile
├── test_compile
│ └── test_compile.nim
├── test_examples
│ └── examples.nim
└── test_readme
│ ├── example1.nim
│ ├── example2.nim
│ └── readme.nim
├── config.nims
├── issue
└── tissue.nim
├── local
├── basic_auth
│ └── local_basic_auth_test.nim
├── extend_config
│ ├── .config
│ │ ├── config.custom.toml
│ │ ├── config.debug.toml
│ │ ├── config.production.toml
│ │ └── config.toml
│ ├── app.nim
│ └── utils.nim
├── extend_config_yaml
│ ├── .config
│ │ ├── config.custom.yaml
│ │ ├── config.debug.yaml
│ │ ├── config.production.yaml
│ │ └── config.yaml
│ └── app.nim
├── extend_context
│ ├── tmiddleware_general.nim
│ ├── tmiddleware_personal.nim
│ └── tsimple.nim
├── staticFile
│ ├── hello.html
│ ├── local_bigFile_test.nim
│ ├── local_download_test.nim
│ ├── local_staticFile_test.nim
│ ├── local_staticFile_virtualPath_test.nim
│ └── public
│ │ ├── idx.html
│ │ └── idx.txt
├── tlocal.nim
└── uploadFile
│ ├── local_uploadFile_test.nim
│ └── upload.html
├── mock
├── tmock_docs
│ ├── tmock_doc_errorhandler.nim
│ └── tmock_doc_headers.nim
└── tmock_mocking
│ ├── tmock_errorhandler.nim
│ ├── tmock_flash.nim
│ ├── tmock_group.nim
│ ├── tmock_route.nim
│ └── tmock_simple.nim
├── server
├── tserver_application.nim
└── utils.nim
├── start_server.nim
├── static
├── A
│ └── B
│ │ └── C
│ │ └── important_text.txt
├── favicon.ico
└── tstatic.nim
├── unit
├── tunit_cache
│ ├── tunit_lfucache.nim
│ └── tunit_lrucache.nim
├── tunit_core
│ ├── tunit_application.nim
│ ├── tunit_configure.nim
│ ├── tunit_constants.nim
│ ├── tunit_contenttype.nim
│ ├── tunit_context.nim
│ ├── tunit_encode.nim
│ ├── tunit_form.nim
│ ├── tunit_framework_config
│ │ ├── .config
│ │ │ ├── config.custom.json
│ │ │ ├── config.debug.json
│ │ │ ├── config.json
│ │ │ └── config.production.json
│ │ ├── tunit_framework_config.nim
│ │ └── tunit_loadsettings.nim
│ ├── tunit_group.nim
│ ├── tunit_httpexception.nim
│ ├── tunit_nativesettings.nim
│ ├── tunit_response.nim
│ ├── tunit_route.nim
│ ├── tunit_types.nim
│ ├── tunit_uid.nim
│ └── tunit_urandom.nim
├── tunit_security
│ ├── tunit_hasher.nim
│ └── tunit_signing.nim
├── tunit_staticfile
│ └── tunit_utils
│ │ ├── static
│ │ └── css
│ │ │ └── basic.css
│ │ ├── temp
│ │ └── basic.html
│ │ ├── templates
│ │ └── basic.html
│ │ └── tunit_utils.nim
└── tunit_validate
│ ├── test_basic.nim
│ └── test_validate.nim
└── webdriver
└── twebdriver.nim
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.md linguist-language=nim
2 | *.html linguist-language=nim
3 | *.js linguist-language=nim
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: planety
4 | # custom: ['https://www.buymeacoffee.com/flywind']
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/action.yml:
--------------------------------------------------------------------------------
1 | name: Test Prologue
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | os:
11 | - ubuntu-latest
12 | # - windows-latest
13 | # - macOS-latest
14 | nim-version:
15 | - stable
16 | - devel
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 |
21 | - name: Cache choosenim
22 | id: cache-choosenim
23 | uses: actions/cache@v4
24 | with:
25 | path: ~/.choosenim
26 | key: ${{ runner.os }}-choosenim-${{ matrix.nim-version}}
27 |
28 | - name: Cache nimble
29 | id: cache-nimble
30 | uses: actions/cache@v4
31 | with:
32 | path: ~/.nimble
33 | key: ${{ runner.os }}-nimble-${{ matrix.nim-version}}-${{ hashFiles('prologue.nimble') }}
34 | restore-keys: |
35 | ${{ runner.os }}-nimble-${{ matrix.nim-version}}-
36 |
37 | - name: Setup nim
38 | uses: jiro4989/setup-nim-action@v2
39 | with:
40 | nim-version: ${{ matrix.nim-version }}
41 | repo-token: ${{ secrets.GITHUB_TOKEN }}
42 |
43 | - name: Install Packages
44 | run: nimble install -y
45 |
46 | - name: Install extension
47 | run: logue extension all
48 |
49 | - name: Test
50 | run: nimble tests
51 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: github pages
2 |
3 | #on: [push] # debug
4 | on:
5 | push:
6 | branches:
7 | - devel
8 |
9 | jobs:
10 | framework-docs:
11 | runs-on: ubuntu-20.04
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: Setup Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: '3.8'
20 |
21 | - name: Upgrade pip
22 | run: |
23 | # install pip=>20.1 to use "pip cache dir"
24 | python -m pip install --upgrade pip
25 |
26 | - name: Get pip cache dir
27 | id: pip-cache
28 | run: echo "::set-output name=dir::$(pip cache dir)"
29 |
30 | - name: Cache dependencies
31 | uses: actions/cache@v4
32 | with:
33 | path: ${{ steps.pip-cache.outputs.dir }}
34 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
35 | restore-keys: |
36 | ${{ runner.os }}-pip-
37 |
38 | - name: Install dependencies
39 | run: python -m pip install -r ./requirements.txt
40 |
41 | - run: mkdocs build
42 |
43 | - name: Archive framework docs
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: framework-docs
47 | path: |
48 | site
49 |
50 | api-docs:
51 | runs-on: windows-latest
52 | steps:
53 | - name: Checkout
54 | uses: actions/checkout@v4
55 |
56 | - name: Cache choosenim
57 | id: cache-choosenim
58 | uses: actions/cache@v4
59 | with:
60 | path: ~/.choosenim
61 | key: ${{ runner.os }}-choosenim-stable
62 |
63 | - name: Cache nimble
64 | id: cache-nimble
65 | uses: actions/cache@v4
66 | with:
67 | path: ~/.nimble
68 | key: ${{ runner.os }}-nimble-${{ hashFiles('prologue.nimble') }}
69 | restore-keys: |
70 | ${{ runner.os }}-nimble-
71 |
72 | - name: Setup nim
73 | uses: jiro4989/setup-nim-action@v2
74 | with:
75 | nim-version: stable
76 |
77 | - name: Install Packages
78 | run: nimble install -y
79 |
80 | - name: Install extension
81 | run: logue extension all
82 |
83 | - name: Build API docs
84 | run: nimble apis
85 |
86 | - name: Archive API docs
87 | uses: actions/upload-artifact@v4
88 | with:
89 | name: api-docs
90 | path: |
91 | docs/coreapi
92 | docs/plugin
93 |
94 | deploy-docs:
95 | needs:
96 | - framework-docs
97 | - api-docs
98 | runs-on: ubuntu-latest
99 | steps:
100 | - name: Download all docs
101 | uses: actions/download-artifact@v4
102 |
103 | - name: Check files
104 | run: |
105 | find .
106 |
107 | - name: Setup docs
108 | run: |
109 | mv framework-docs/ docs/
110 | rm -rf docs/coreapi docs/plugin
111 | mv api-docs/coreapi api-docs/plugin docs/
112 |
113 | - name: Deploy
114 | if: success()
115 | uses: crazy-max/ghaction-github-pages@v4.2.0
116 | with:
117 | target_branch: gh-pages
118 | build_dir: ./docs
119 | env:
120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
121 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *exe
2 | nimcache
3 | testresults
4 | .gitignore
5 | outputGotten.txt
6 | tests/megatest.nim
7 | tests/server/tserver_application
8 | tests/start_server
9 | examples/todoapp/app
10 |
11 | # nimble texample
12 | tests/compile/test_examples/examples
13 |
14 | # nimble treadme
15 | tests/compile/test_readme/readme
16 |
17 | # nimble tcompile
18 | tests/compile/test_compile/test_compile
19 |
20 | *.exe
21 | .vscode
22 |
23 | docs/
24 | htmldocs/
25 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### prefer `len` to `default`
2 |
3 | ```nim
4 | # use len
5 | let
6 | a = ""
7 | b = @[]
8 |
9 | assert a.len != 0
10 | assert b.len != 0
11 |
12 | # don't use
13 | assert a != ""
14 | assert b != ""
15 | ```
16 |
17 | ### prefer `plain functions` to `macros`
18 |
19 | You can avoid `macros`. If it is necessary, you should only use a simple one.
20 |
21 | ```nim
22 | macro resp*(response: Response) =
23 | ## handy to make ctx's response
24 | var ctx = ident"ctx"
25 |
26 | result = quote do:
27 | `ctx`.response = `response`
28 | ```
29 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ## 0.6.4
2 |
3 | Better deps management.
4 |
5 | ## 0.6.2
6 |
7 | Added gcsafe pragmas to extend method.
8 |
9 | ## 0.6.0
10 |
11 | Added automatic URL decoding when fetching query parameters or path parameters from request object
12 |
13 | ## 0.5.8
14 |
15 | remove cursor annotation; Prologue should work with ORC (thanks to @Yardanico's advice)
16 |
17 | ## 0.5.6
18 |
19 | Added `getPostParamsOption`, `getQueryParamsOption`, `getFormParamsOption`, `getPathParamsOption`.
20 |
21 | ## 0.5.2
22 |
23 | Deprecated prologue/security/hashers (fix #140).
24 |
25 | ## 0.4.8
26 |
27 | A new form of user defined context.
28 |
29 | Make ip-address clickable (fix #85).
30 |
31 | ## 0.4.6
32 |
33 | Fixed `genUid` error.
34 |
35 | Fixed https://github.com/planety/prologue/issues/122
36 |
37 |
38 | ## 0.4.4
39 |
40 | Added `logueRouteLoose` to enable loosely route matching (fix #112).
41 |
42 | ## 0.4.2
43 |
44 | fix custom setting error (#100)
45 |
46 | plugin.nim is for document only (#99)
47 |
48 | fix static file serving is slow in windows(`usestd` also works)
49 |
50 | ## 0.3.8
51 |
52 | Move `basicAuthMiddleware` from `auth/auth.nim` to `middlewares/auth.nim`. Users need to change the import clause to `import prologue/middlewares/auth`.
53 |
54 | Setting doesn't set the default path of staticDirs anymore.
55 |
56 | Change `Response.headers` to `ResponseHeaders`, users can initialize it with `initResponseHeaders`.
57 |
58 |
59 | ## 0.3.6
60 |
61 | Fixes that sessionMiddleware doesn't work when user does not register session.
62 | Fixes HttpHeaders and adds nil check.
63 | Fixes cookies containing commas fail for asynchttpserver using base64 encode.
64 |
65 | ## 0.3.4
66 |
67 | Fixes "Always asked to install `cookiejar` when running" #36
68 |
69 | ## 0.3.2
70 |
71 | Fixes `resp "Hello"` will clear all attributes.
72 |
73 | Reduces unnecessary operations.
74 |
75 | Adds tests for cookie.
76 |
77 | Reduces unnecessary imports and compilation time.
78 |
79 | ## 0.3.0
80 |
81 | Windows support multi-thread HTTP server(httpx).
82 |
83 | The route of the request is stripped. (/hello/ -> /hello)
84 |
85 | ## 0.2.8
86 |
87 | Adds `Settings.address`, user can specify listening `address`.
88 |
89 | OpenAPI docs allows specifying source path.
90 |
91 | Fix `configure.getOrdefault`'s bug.
92 |
93 | Adds more documents.
94 |
95 | Adds more API docs.
96 |
97 | Changes import path, allows `import prologue/middlewares` instead of
98 | `import prologue/middlewares/middlewares`.
99 |
100 | Renames `validate` to `validater`. Supports `import prologue/auth`, `import prologue/auth`, `import prologue/middlewares`, `import prologue/openapi`, `import prologue/security`, `import prologue/signing` and `import prologue/validater`.
101 |
102 | Moves signing from the core directory.
103 |
--------------------------------------------------------------------------------
/docs/assets/openapi/docs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/docs/assets/openapi/docs.jpg
--------------------------------------------------------------------------------
/docs/assets/quickstart/hello.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/docs/assets/quickstart/hello.jpg
--------------------------------------------------------------------------------
/docs/cli.md:
--------------------------------------------------------------------------------
1 | # Command line tool
2 |
3 | `Prologue` ships with `logue` tool to help you start a new project quickly.
4 |
5 | ## Creates a new project
6 |
7 | Type `logue init projectName` in command line to create a new project. This will create `.env` file for configuration. If you want to use JSON format config file, please add `--useConfig` or `-u` to the command.
8 |
9 | ```
10 | logue init newapp
11 | ```
12 |
13 | Using json config:
14 |
15 | ```
16 | logue init newapp --useConfig
17 | # or
18 | logue init newapp -u
19 | ```
20 |
21 | ## Install the extensions
22 |
23 | Type `logue extension extensionName` to install the specific extension which is specified in `prologue.nimble`. If you want to install all the extensions, please input `logue extension all`.
24 |
25 | ```
26 | logue extension redis
27 | ```
28 |
29 | Install all the extensions:
30 |
31 | ```
32 | logue extension all
33 | ```
34 |
35 |
--------------------------------------------------------------------------------
/docs/configure.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | When starting a project, you need to configure your application.
4 |
5 | ## Simple settings
6 |
7 | For small program, you could use the default `settings` which is provided by `Prologue`.
8 |
9 | ```nim
10 | import prologue
11 |
12 | var app = newApp()
13 | app.run()
14 | ```
15 |
16 | You may want to specify settings by yourself. `Prologue` provides `newSettings` function to create a new settings. The program below creates a new settings with `debug = false`. This will disable the default logging.
17 |
18 | ```nim
19 | import prologue
20 |
21 | let settings = newSettings(debug = false)
22 | var app = newApp(settings = settings)
23 | app.run()
24 | ```
25 |
26 | You can also read settings from `.env` file. `Prologue` provides `loadPrologueEnv` to read data from `.env` file. You can use `get` or `getOrDefault` to retrieve the value.
27 |
28 | ```nim
29 | import prologue
30 |
31 | let
32 | env = loadPrologueEnv(".env")
33 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
34 | debug = false,
35 | port = Port(env.getOrDefault("port", 8080))
36 | )
37 |
38 | var app = newApp(settings = settings)
39 | app.run()
40 | ```
41 |
42 | ## Config file
43 |
44 | You need to specify a config file for a big project. `Prologue` provides `loadSettings` to read JSON file. You should give the path of the Json config file.
45 |
46 | ```nim
47 | let settings = loadSettings(".config/config.debug.json")
48 | var app = newApp(settings)
49 | ```
50 |
51 | `.config/config.debug.json`
52 |
53 | In config file, the `prologue` key must be present. The corresponding data will be used by framework. Among the corresponding data, the `secretKey` must be present and should not be empty. Otherwise, the program will raise exception. Other keys can be absent, they will be given a default value setting by `Prologue`.
54 |
55 | Below is the type of settings:
56 |
57 | ```
58 | address: string
59 | port: int
60 | debug: bool
61 | reusePort: bool
62 | appName: string
63 | secretKey: string
64 | bufSize: int
65 | ```
66 |
67 | ```json
68 | {
69 | "prologue": {
70 | "address": "",
71 | "port": 8080,
72 | "debug": true,
73 | "reusePort": true,
74 | "appName": "",
75 | "secretKey": "Set by yourself",
76 | "bufSize": 40960
77 | },
78 | "name": "debug"
79 | }
80 | ```
81 |
82 | ## Changing config file via environment variable
83 |
84 | `Prologue` also supports automatically loading configure file by environment variables. The `.config` directory must be present in the current path(in the same directory as the main program). If you don't set the environment variable(namely `PROLOGUE`) or the value is `default`, the application will read `.config/config.json` file. Otherwise, if you set the `PROLOGUE` environment variable to `custom`, the application will read `.config/config.custom.json`. The common names includes `debug` and `production`. If the file doesn't exist, it will raise exception.
85 |
86 | ```nim
87 | import prologue
88 |
89 | var app = newAppQueryEnv()
90 | app.run()
91 | ```
92 |
93 |
--------------------------------------------------------------------------------
/docs/errorhandler.md:
--------------------------------------------------------------------------------
1 | # Error Handler
2 |
3 | ## User-defined error pages
4 |
5 | When web application encounters some unexpected situations, it may send 404 response to the client.
6 | You may want to use user-defined 404 pages, then you can use `resp` to return 404 response.
7 |
8 |
9 | ```nim
10 | proc hello(ctx: Context) {.async.} =
11 | resp "Something is wrong, please retry.", Http404
12 | ```
13 |
14 | `Prologue` also provides an `error404` helper function to create a 404 response.
15 |
16 | ```nim
17 | proc hello(ctx: Context) {.async.} =
18 | resp error404(headers = ctx.response.headers)
19 | ```
20 |
21 | Or use `errorPage` to create a more descriptive error page.
22 |
23 | ```nim
24 | proc hello(ctx: Context) {.async.} =
25 | resp errorPage("Something is wrong"), Http404
26 | ```
27 |
28 | ## Default error handler
29 |
30 | Users can also set the default error handler. When `ctx.response.body` is empty, web application will use the default error handler.
31 |
32 | The basic example with `respDefault` which is equal to `resp errorPage("Something is wrong"), Http404`.
33 |
34 | ```nim
35 | proc hello(ctx: Context) {.async.} =
36 | respDefault Http404
37 | ```
38 |
39 | `Prologue` has registered two error handlers before application starts, namely `default404Handler` for `Http404` and `default500Handler` for `Http500`. You can change them using `registerErrorHandler`.
40 |
41 | ```nim
42 | proc go404*(ctx: Context) {.async.} =
43 | resp "Something wrong!", Http404
44 |
45 | proc go20x*(ctx: Context) {.async.} =
46 | resp "Ok!", Http200
47 |
48 | proc go30x*(ctx: Context) {.async.} =
49 | resp "EveryThing else?", Http301
50 |
51 | app.registerErrorHandler(Http404, go404)
52 | app.registerErrorHandler({Http200 .. Http204}, go20x)
53 | app.registerErrorHandler(@[Http301, Http304, Http307], go30x)
54 | ```
55 |
56 | If you don't want to use the default Error handler, you could clear the whole error handler table.
57 |
58 | ```nim
59 | var app = newApp(errorHandlerTable = newErrorHandlerTable())
60 | ```
61 |
62 | ## HTTP 500 handler
63 |
64 | `Http 500` indicates the internal error of the framework. In debug mode(`settings.debug = true`), the framework will send the exception msgs to the web browser if the length of error msgs is greater than zero.
65 | Otherwise, the framework will use the default error handled which has been registered before the application starts. Users could cover this handler by using their own error handler.
--------------------------------------------------------------------------------
/docs/event.md:
--------------------------------------------------------------------------------
1 | # Event
2 |
3 | `Prologue` supports both `startup` and `shutdown` events. `startup` events will be only executed once for each thread. In contrast, `shutdown` events will be executed once after the main loop.
4 |
5 | Let's first look at the structure of `Event`, you can see that `Event` supports both synchronous and asynchronous closure function pointers.
6 |
7 | ```nim
8 | type
9 | AsyncEvent* = proc(): Future[void] {.closure, gcsafe.}
10 | SyncEvent* = proc() {.closure, gcsafe.}
11 |
12 | Event* = object
13 | case async*: bool
14 | of true:
15 | asyncHandler*: AsyncEvent
16 | of false:
17 | syncHandler*: SyncEvent
18 | ```
19 |
20 | You can use `initEvent` and pass function pointers to create `Event`.
21 |
22 | ```nim
23 | proc initEvent*(handler: AsyncEvent): Event {.inline.} =
24 | Event(async: true, asyncHandler: handler)
25 |
26 | proc initEvent*(handler: SyncEvent): Event {.inline.} =
27 | Event(async: false, syncHandler: handler)
28 | ```
29 |
30 | `newApp` has `startup` and `shutdown` parameters. You can pass a sequence of events to `newApp`.
31 |
32 | ```nim
33 | proc newApp*(settings: Settings, middlewares: sink seq[HandlerAsync] = @[],
34 | startup: seq[Event] = @[], shutdown: seq[Event] = @[],
35 | errorHandlerTable = DefaultErrorHandler,
36 | appData = newStringTable(mode = modeCaseSensitive)): Prologue =
37 | ```
38 |
39 | Here is an [example](https://github.com/planety/prologue/tree/devel/examples/helloworld) for a `startup` event (A `shutdown` event has the same usage as a `startup` event).
40 |
41 | ```nim
42 | proc setLoggingLevel() =
43 | addHandler(newConsoleLogger())
44 | logging.setLogFilter(lvlInfo)
45 |
46 |
47 | let
48 | event = initEvent(setLoggingLevel)
49 |
50 | var
51 | app = newApp(settings = settings, startup = @[event])
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/extendctx.md:
--------------------------------------------------------------------------------
1 | # Extend Context
2 |
3 | `Prologue` provides flexible way to extend `Context` object. User-defined `Context` should inherit from the `Context` object.
4 |
5 | ## A simple example
6 |
7 | The following example shows how to make a `UserContext` object which contains
8 | a new data member of `int` type. You should use `extend` method to initialize
9 | the new attributes. Finally use `app.run(UserContext)` to register the type.
10 |
11 | ```nim
12 | import prologue
13 |
14 | type
15 | UserContext = ref object of Context
16 | data: int
17 |
18 | # initialize data
19 | method extend(ctx: UserContext) {.gcsafe.} =
20 | ctx.data = 999
21 |
22 | proc hello*(ctx: Context) {.async.} =
23 | let ctx = UserContext(ctx)
24 | doAssert ctx.data == 999
25 | resp "
Hello, Prologue! "
26 |
27 | var app = newApp()
28 | app.get("/", hello)
29 | app.run(UserContext)
30 | ```
31 |
32 | ## Make a middleware for personal use
33 |
34 | ```nim
35 | import prologue
36 |
37 | type
38 | UserContext = ref object of Context
39 | data: int
40 |
41 | proc init(ctx: UserContext) =
42 | ctx.data = 12
43 |
44 | proc experimentMiddleware(): HandlerAsync =
45 | result = proc(ctx: Context) {.async.} =
46 | let ctx = UserContext(ctx)
47 | doAssert ctx.data == 12
48 | inc ctx.data
49 | await switch(ctx)
50 |
51 | method extend(ctx: UserContext) {.gcsafe.} =
52 | init(ctx)
53 |
54 | proc hello*(ctx: Context) {.async.} =
55 | let ctx = UserContext(ctx)
56 | assert ctx.data == 13
57 | echo ctx.data
58 | resp "Hello, Prologue! "
59 |
60 | var app = newApp()
61 | app.use(experimentMiddleware())
62 | app.get("/", hello)
63 | app.run(UserContext)
64 | ```
65 |
66 | ## Make a general purpose middleware
67 |
68 | **Notes**: use prefix or suffix denoting data member to avoid conflicts with other middlewares.
69 |
70 | ```nim
71 | # middleware for general purpose
72 | type
73 | ExperimentContext = concept ctx
74 | ctx is Context
75 | ctx.data is int
76 |
77 | proc init[T: ExperimentContext](ctx: T) =
78 | ctx.data = 12
79 |
80 | proc experimentMiddleware[T: ExperimentContext](ctxType: typedesc[T]): HandlerAsync =
81 | result = proc(ctx: Context) {.async.} =
82 | let ctx = ctxType(ctx)
83 | doAssert ctx.data == 12
84 | inc ctx.data
85 | await switch(ctx)
86 |
87 |
88 | type
89 | UserContext = ref object of Context
90 | data: int
91 |
92 | method extend(ctx: UserContext) {.gcsafe.} =
93 | init(ctx)
94 |
95 | proc hello*(ctx: Context) {.async.} =
96 | let ctx = UserContext(ctx)
97 | assert ctx.data == 13
98 | echo ctx.data
99 | resp "Hello, Prologue! "
100 |
101 | var app = newApp()
102 | app.use(experimentMiddleware(UserContext))
103 | app.get("/", hello)
104 | app.run(UserContext)
105 | ```
106 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## Threads
4 |
5 | `Prologue` supports two HTTP server: `httpbeast` and `asynchttpserver`. If you are in Linux or MacOS, use `--threads:on` to enable the multi-threads HTTP server. If you are in windows, `threads` should not be used. You can use `-d:usestd` to switch to `asynchttpserver` in Linux or MacOS.
6 |
7 | ## Benchmarking and debug
8 |
9 | If you want to benchmark `prologue` or release your programs, make sure to set `settings.debug` = false.
10 |
11 | ```nim
12 | let
13 | # debug attributes must be false
14 | env = loadPrologueEnv(".env")
15 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
16 | debug = false,
17 | port = Port(env.getOrDefault("port", 8080)),
18 | secretKey = env.getOrDefault("secretKey", "")
19 | )
20 | ```
21 |
22 | or in `.env` file, set `debug = false`.
23 |
24 | ```nim
25 | # Don't commit this to source control.
26 | # Eg. Make sure ".env" in your ".gitignore" file.
27 | debug=false # change this
28 | port=8080
29 | appName=HelloWorld
30 | staticDir=/static
31 | secretKey=Pr435ol67ogue
32 | ```
33 |
34 | ## Disable logging
35 |
36 | There are two ways to disable logging messages:
37 |
38 | - set `settings.debug` = false
39 | - set a startup event
40 |
41 | ```nim
42 | proc setLoggingLevel() =
43 | addHandler(newConsoleLogger())
44 | logging.setLogFilter(lvlInfo)
45 |
46 |
47 | let
48 | event = initEvent(setLoggingLevel)
49 | var
50 | app = newApp(settings = settings, startup = @[event])
51 | ```
52 |
53 | ## Avoid using a function name which is same to the module name.
54 |
55 | `src/index.nim`
56 |
57 | ```nim
58 | proc index(ctx: Context) {.async.} =
59 | ...
60 | ```
61 |
62 | ## Use the full path of JS, CSS files.
63 |
64 | For instance in your HTML file use `templates/some.js` instead of `some.js`.
65 |
66 | ## Run in async mode
67 |
68 | The server can run in async mode, this is useful to perform other tasks beside
69 | accepting connections.
70 |
71 | ```nim
72 | import prologue
73 |
74 | proc handler(ctx: Context) {.async.} =
75 | resp "Hello world"
76 |
77 | let settings = newSettings(port = Port(8000))
78 | let app = newApp(settings)
79 | app.all("/*$", handler)
80 | waitFor app.runAsync()
81 |
82 | ```
83 |
84 |
--------------------------------------------------------------------------------
/docs/headers.md:
--------------------------------------------------------------------------------
1 | # Headers
2 |
3 | Prologue provides two types of `headers`. One is the headers of `request` which carries information from the client. The other is the headers of `response` which carries information sent to the client.
4 |
5 |
6 | ## The headers of the request
7 |
8 | The client will send headers to our HTTP server. You may want to check whether some keys are in the headers.
9 | If existing, you could get the values of them. The return type of `ctx.request.getHeader` is `seq[string]`. You often only need the first element of the sequence.
10 |
11 | The following code first checks whether the key exists in headers. If true, retrieve the sequence of values and display them in the browser.
12 |
13 | ```nim
14 | proc hello(ctx: Context) {.async.} =
15 | if ctx.request.hasHeader("cookie"):
16 | let values = ctx.request.getHeader("cookie")
17 | resp $values
18 | elif ctx.request.hasHeader("content-type"):
19 | let values = ctx.request.getHeaderOrDefault("content")
20 | resp $values
21 | ```
22 |
23 | ## The headers of the response
24 |
25 | `Prologue` also sends HTTP headers to the client. It uses `ResponseHeaders` to store them. It has similar API like the headers of the request. First, `Prologue` initializes `ctx.response` with `initResponseHeaders`. Then
26 | users could use `hasHeader`, `addHeader` or `setHeader` to do what they want.
27 |
28 | Notes that, `addHeader` will append values to existing keys in headers. `setHeader` will reset the values of key no matter whether key is in the headers.
29 |
30 | ```nim
31 | proc hello(ctx: Context) {.async.} =
32 | ctx.response.addHeader("Content-Type", "text/plain")
33 |
34 | doAssert ctx.response.getHeader("CONTENT-TYPE") == @[
35 | "text/html; charset=UTF-8", "text/plain"]
36 |
37 | ctx.response.setHeader("Content-Type", "text/plain")
38 |
39 | doAssert ctx.response.getHeader("CONTENT-TYPE") == @[
40 | "text/html; charset=UTF-8", "text/plain"]
41 | ```
--------------------------------------------------------------------------------
/docs/mocking.md:
--------------------------------------------------------------------------------
1 | # Mocking
2 |
3 | Mocking module can be used for quick test without HTTP server.
4 |
5 | First use `mockApp` to add `mockingMiddleware` to the application. Next create a new mocking request using `initMockingRequest`. Then run the mocking application with `runOnce`. Finally check whether `ctx` meets your requirements.
6 |
7 | ```nim
8 | import prologue
9 | import prologue/mocking
10 |
11 | import std/uri
12 |
13 |
14 | proc hello*(ctx: Context) {.async.} =
15 | resp "Hello, Prologue! "
16 |
17 |
18 | let settings = newSettings(debug = true)
19 | var app = newApp(settings = settings)
20 | app.addRoute("/", hello)
21 |
22 | mockApp(app)
23 |
24 |
25 | let url = parseUri("/")
26 |
27 | let req = initMockingRequest(
28 | httpMethod = HttpGet,
29 | headers = newHttpHeaders(),
30 | url = url,
31 | cookies = initCookieJar(),
32 | postParams = newStringTable(),
33 | queryParams = newStringTable(),
34 | formParams = initFormPart(),
35 | pathParams = newStringTable()
36 | )
37 |
38 | let ctx = app.runOnce(req)
39 |
40 | doAssert ctx.response.code == Http200
41 | doAssert ctx.response.getHeader("content-type") == @["text/html; charset=UTF-8"]
42 | doAssert ctx.response.body == "Hello, Prologue! "
43 | ```
--------------------------------------------------------------------------------
/docs/openapi.md:
--------------------------------------------------------------------------------
1 | # openapi
2 |
3 | `Prologue` supplies minimal supports for `openapi` docs. You need to write `openapi.json` by yourself. Then `Prologue` will register corresponding routes.
4 |
5 | ```nim
6 | import prologue
7 | import prologue/openapi
8 |
9 |
10 | app.serveDocs("docs/openapi.json")
11 | app.run()
12 | ```
13 |
14 | [example](https://github.com/planety/prologue/blob/devel/examples/helloworld/docs/openapi.json) for `docs/openapi.json`.
15 |
16 | visit `localhost:8080/docs` or `localhost:8080/redocs`
17 |
18 | 
19 |
--------------------------------------------------------------------------------
/docs/request.md:
--------------------------------------------------------------------------------
1 | # Request
2 |
3 | `Request` contains the information from the HTTP server. You can visit this attribute by using `ctx.request`.
4 |
5 | For example If you want to get state from users, query the `cookies` attribute.
6 |
7 | ```nim
8 | proc hello(ctx: Context) {.async.} =
9 | if ctx.request.cookies.hasKey("happy"):
10 | echo "Yea, I'm happy"
11 | ```
12 |
13 |
14 | ## Request utils
15 |
16 | ### request.url
17 | Gets the url of the request.
18 |
19 | ```nim
20 | proc hello(ctx: Context) {.async.} =
21 | echo ctx.request.url
22 | ```
23 |
24 | ### request.port
25 | Gets the port of the request.
26 |
27 | ```nim
28 | proc hello(ctx: Context) {.async.} =
29 | echo ctx.request.port.int
30 | ```
31 |
32 | ### request.path
33 | Gets the path of the request.
34 |
35 | ```nim
36 | proc hello(ctx: Context) {.async.} =
37 | echo ctx.request.path
38 | ```
39 |
40 |
41 | ### request.reqMethod
42 | Gets the `HttpMethod` of the request.
43 |
44 | ```nim
45 | proc hello(ctx: Context) {.async.} =
46 | echo ctx.request.reqMethod
47 | ```
48 |
49 | ### request.contentType
50 | Gets the contentType of the request.
51 |
52 | ```nim
53 | proc hello(ctx: Context) {.async.} =
54 | echo ctx.request.contentType
55 | ```
56 |
57 | ### request.hostName
58 | Gets the hostname of the request.
59 |
60 | ```nim
61 | proc hello(ctx: Context) {.async.} =
62 | echo ctx.request.hostName
63 | ```
64 |
--------------------------------------------------------------------------------
/docs/response.md:
--------------------------------------------------------------------------------
1 | # Response
2 |
3 | ## Respond by types
4 |
5 | You can specify different responses by types.
6 |
7 | - htmlResponse -> HTML format
8 | ```nim
9 | import prologue
10 |
11 | # this proc will return an html response to the client
12 | proc response*(ctx: Context) {.async.} =
13 | resp htmlResponse("Hello, Prologue! ")
14 |
15 | let app = newApp()
16 | app.addRoute("/", response)
17 | app.run()
18 | ```
19 | - plainTextResponse -> Plain Text format
20 | ```nim
21 | import prologue
22 |
23 | # this proc will return plain text to the client
24 | proc response*(ctx: Context) {.async.} =
25 | resp plainTextResponse("Hello, Prologue!")
26 |
27 | let app = newApp()
28 | app.addRoute("/", response)
29 | app.run()
30 | ```
31 | - jsonResponse -> Json format
32 | ```nim
33 | import prologue, std/json
34 |
35 | # this proc will return json to the client
36 | proc response*(ctx: Context) {.async.} =
37 | # the %* operator creates json from nim types. more info: https://nim-lang.org/docs/json.html
38 | var info = %*
39 | [
40 | { "name": "John", "age": 30 },
41 | { "name": "Susan", "age": 45 }
42 | ]
43 |
44 | resp jsonResponse(info)
45 |
46 | let app = newApp()
47 | app.addRoute("/", response)
48 | app.run()
49 | ```
50 |
51 | ## Respond by error code
52 |
53 | - error404 -> return 404
54 | - redirect -> return 301 and redirect to a new page
55 | - abort -> return 401
56 |
57 | ## Other utils
58 |
59 | You can set the cookie and header of the response.
60 |
61 | `SetCookie`: sets the cookie of the response.
62 | `DeleteCookie`: deletes the cookie of the response.
63 | `setHeader`: sets the header values of the response.
64 | `addHeader`: adds header values to the existing `HttpHeaders`.
65 |
66 | ## Send user-defined response
67 |
68 | `Prologue` framework will automatically send the final response to the client. You just need to set the attributes of response.
69 |
70 | It also supports sending response by yourself. For example you can use `ctx.respond` to send data to the client.
71 |
72 | ```nim
73 | proc sendResponse(ctx: Context) {.async.} =
74 | await ctx.respond(Http200, "data")
75 | ```
76 |
77 | But this will leads that "data" message is sent twice, it's ok for some situations. For example, you may want to send another message, you can change the body of the response.
78 |
79 | ```nim
80 | proc sendResponse(ctx: Context) {.async.} =
81 | await ctx.respond(Http200, "data")
82 | ctx.response.body = "message"
83 | ```
84 |
85 | First this handler will send "data" to the client, then the handler will send "message" to the client. However, this may be not the intended behaviour. You want to make sure when you send response by yourself, the framework shouldn't handle the response anymore.
86 |
87 | You can set the `handled` attribute of context to true. Now the framework won't handle `ctx.response` any more and the error handler won't handle the response too. Only the "data" message is sent to the client.
88 |
89 | ```nim
90 | proc sendResponse(ctx: Context) {.async.} =
91 | await ctx.respond(Http200, "data")
92 | ctx.handled = true
93 | ctx.response.code = Http500
94 | ctx.response.body = "message"
95 | ```
96 |
--------------------------------------------------------------------------------
/docs/server.md:
--------------------------------------------------------------------------------
1 | # Server settings
2 | Current implementation of `Prologue` supports two HTTP servers. Some settings may work in one of these backends, and won't work in the other backends. This is called additional settings.
3 |
4 | ## Settings
5 |
6 | If you want to use `maxBody` attribute which only work in `asynchttpserver` backend, you can set them with `newSettings`. `newSettings` supports data of JSON format.
7 |
8 | In `asynchttpserver` backend(namely `-d:usestd`), you can set `maxBody` attribute to respond 413 when the contentLength in HTTP headers is over limitation.
9 |
10 | In `httpx` backend, you could set `numThreads` settings which only work in Unix OS. In windows this setting won't work, the number of threads will always be one. This setting allows user to configure how many threads to run the event loop.
11 |
--------------------------------------------------------------------------------
/docs/session.md:
--------------------------------------------------------------------------------
1 | # Session
2 |
3 | The session helps with storing users' state. If you want to use `session` or `flash` messages, you must use `sessionMiddleware` first.
4 |
5 | ## Session based on signed cookie
6 | This session is based on signed cookie. **It is not safe**. You must not use it to store sensitive or important info except for testing.
7 |
8 | Prologue provides you with `sessionMiddleware`.
9 |
10 | ### Usage
11 |
12 | First you should register `sessionMiddleware` in global middlewares or handler's middlewares.
13 |
14 | ```nim
15 | import prologue
16 | import prologue/middlewares/sessions/signedcookiesession
17 |
18 | let settings = newSettings()
19 | var app = newApp(settings = settings)
20 | app.use(sessionMiddleware(settings))
21 | ```
22 |
23 | Then you can use session in all handlers. You can set/get/clear session.
24 |
25 | ```nim
26 | proc login*(ctx: Context) {.async.} =
27 | ctx.session["flywind"] = "123"
28 | ctx.session["ordontfly"] = "345"
29 | resp "Hello, Prologue! "
30 |
31 | proc logout*(ctx: Context) {.async.} =
32 | resp $ctx.session
33 | ```
34 |
35 | More session examples are in [Signed Cookie Session](https://github.com/planety/prologue/tree/devel/examples/signedcookiesession) and [Blog](https://github.com/planety/prologue/tree/devel/examples/blog)
36 |
37 |
38 | ## Session based on memory
39 |
40 | The usage of memory session is similar to signed cookie session. Just change the import statement to `import prologue/middlewares/sessions/memorysession`. This is meant for testing too. Because the data will be lost if the program stops.
41 |
42 | ```nim
43 | import prologue
44 | import prologue/middlewares/sessions/memorysession
45 |
46 |
47 | let settings = newSettings()
48 | var app = newApp(settings)
49 | app.use(sessionMiddleware(settings))
50 | ```
51 |
52 | ## Session based on redis
53 |
54 | You should install `redis` first(`logue extension redis`).
55 |
56 | ```nim
57 | import prologue
58 | import prologue/middlewares/sessions/redissession
59 |
60 |
61 | let settings = newSettings()
62 | var app = newApp(settings)
63 | app.use(sessionMiddleware(settings))
64 | ```
65 |
66 | ## Flash messages
67 |
68 | Sometimes you need to store some messages to session, then you can visit these messages in the next request. They will be used once. Once you have visit these messages, they will be popped from the session. You must use one of session middleware above.
69 |
70 | ```nim
71 | import src/prologue
72 | import src/prologue/middlewares/signedcookiesession
73 | import std/with
74 |
75 |
76 | proc hello(ctx: Context) {.async.} =
77 | ctx.flash("Please retry again!")
78 | resp "Hello, world"
79 |
80 | proc tea(ctx: Context) {.async.} =
81 | let msg = ctx.getFlashedMsg(FlashLevel.Info)
82 | if msg.isSome:
83 | resp msg.get
84 | else:
85 | resp "My tea"
86 |
87 | let settings = newSettings()
88 | var app = newApp(settings)
89 |
90 | with app:
91 | use(sessionMiddleware(settings))
92 | get("/", hello)
93 | get("/hello", hello)
94 | get("/tea", tea)
95 | run()
96 | ```
97 |
--------------------------------------------------------------------------------
/docs/staticfiles.md:
--------------------------------------------------------------------------------
1 | # Static Files
2 |
3 | Prologue supports serving static files.
4 |
5 | ## Send static file Response
6 |
7 | You can use `staticFileResponse` to make a static file response.
8 |
9 | ```nim
10 | proc home(ctx: Context) {.async.} =
11 | await ctx.staticFileResponse("hello.html", "")
12 | ```
13 |
14 | ## Download files
15 |
16 | User maybe want to download some files from the server. You can use `staticFileResponse` to send the file to be downloaded.
17 |
18 | ```nim
19 | proc downloadFile(ctx: Context) {.async.} =
20 | await ctx.staticFileResponse("index.html", "static", downloadName = "download.html")
21 | ```
22 |
23 | ## Serve static files
24 |
25 | `staticfile` is implemented as middleware. It should be imported first. You can specify the path of static directories. `staticDirs` is of `varargs[string]` type. It contains all
26 | the directories of static files which will be checked in every request.
27 |
28 | ```nim
29 | import prologue
30 | import prologue/middlewares/staticfile
31 |
32 |
33 | var app = newApp(settings = settings)
34 | app.use(staticFileMiddleware(env.get("staticDir")))
35 | # add your routes
36 | app.run()
37 | ```
38 |
39 | Multiple directories:
40 |
41 | ```nim
42 | import prologue
43 | import prologue/middlewares/staticfile
44 |
45 |
46 | var app = newApp(settings = settings)
47 | app.use(staticFileMiddleware("public", "templates"))
48 | # Or seq[string]
49 | # app.use(staticFileMiddleware(@["public", "templates"]))
50 | # Or array[N, string]
51 | # app.use(staticFileMiddleware(["public", "templates"]))
52 | app.addRoute(urls.urlPatterns, "")
53 | app.run()
54 | ```
55 |
56 | ## Serving Favicon
57 |
58 | You may want to add an icon for your website, you can use a favicon. The browser maybe request `/favicon.ico` to find an icon. `redirctTo` is handy for this work. `dest` is the real path of a favicon. For example, you can put it under `static` directory.
59 |
60 | ```nim
61 | import prologue
62 | from prologue/middlewares/staticfile import redirectTo
63 |
64 |
65 | var app = newApp()
66 | app.get("/favicon.ico", redirectTo("/static/favicon.ico"))
67 | app.run()
68 | ```
69 |
--------------------------------------------------------------------------------
/docs/uploadfile.md:
--------------------------------------------------------------------------------
1 | # Upload Files
2 |
3 | `getUploadFile` accepts the name of file in order to get the infos. The function returns the name and contents of the file. For this example, the name is "file".
4 |
5 | ```html
6 |
10 | ```
11 |
12 | `getUploadFile` only works when using form parameters and HttpPost method. `Context` provides a helper function to `save` the uploadFile to disks. If you don't specify the name of the file, it will use the original name from the client.
13 |
14 | ```nim
15 | proc upload(ctx: Context) {.async.} =
16 | if ctx.request.reqMethod == HttpGet:
17 | await ctx.staticFileResponse("tests/local/uploadFile/upload.html", "")
18 | elif ctx.request.reqMethod == HttpPost:
19 | let file = ctx.getUploadFile("file")
20 | file.save("tests/assets/temp")
21 | file.save("tests/assets/temp", "set.txt")
22 | resp fmt"{file.filename} {file.body}
"
23 | ```
24 |
25 | The full [example](https://github.com/planety/prologue/blob/devel/tests/local/uploadFile/local_uploadFile_test.nim)
26 |
--------------------------------------------------------------------------------
/docs/validation.md:
--------------------------------------------------------------------------------
1 | # Validation
2 |
3 | `Prologue` provides lots of helper functions for validating data from users.
4 |
5 | ## Single Record
6 |
7 | Each helper function could be used directly, for examples you want to check whether the content of a string is an int.
8 |
9 | ```nim
10 | import prologue/validate/validate
11 |
12 | let
13 | msg = "Int required"
14 | checkInt = isInt(msg)
15 |
16 | doAssert checkInt("12") == (true, "")
17 | doAssert checkInt("912.6") == (false, msg)
18 | ```
19 |
20 | ## Multiple Records
21 |
22 | You could also check whether multiple records meets the requirements.
23 |
24 | ```nim
25 | import prologue/validate/validate
26 | import strtabs
27 |
28 | var form = newFormValidation({
29 | "accepted": @[required(), accepted()],
30 | "required": @[required()],
31 | "requiredInt": @[required(), isInt()],
32 | "minValue": @[required(), isInt(), minValue(12), maxValue(19)]
33 | })
34 | let
35 | chk1 = form.validate({"required": "on", "accepted": "true",
36 | "requiredInt": "12", "minValue": "15"}.newStringTable)
37 | chk2 = form.validate({"required": "on", "time": "555",
38 | "minValue": "10"}.newStringTable)
39 | chk3 = form.validate({"required": "on", "time": "555",
40 | "minValue": "10"}.newStringTable, allMsgs = false)
41 | chk4 = form.validate({"required": "on", "accepted": "true",
42 | "requiredInt": "12.5", "minValue": "13"}.newStringTable, allMsgs = false)
43 |
44 | doAssert chk1 == (true, "")
45 | doAssert not chk2.hasValue
46 | doAssert chk2.msg == "Can\'t find key: accepted\nCan\'t find key: " &
47 | "required\nCan\'t find key: requiredInt\n10 is not greater than or equal to 12.0!\n"
48 | doAssert not chk3.hasValue
49 | doAssert chk3.msg == "Can\'t find key: accepted\n"
50 | doAssert not chk4.hasValue
51 | doAssert chk4.msg == "12.5 is not an integer!\n"
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/views.md:
--------------------------------------------------------------------------------
1 | # Views
2 |
3 | `Prologue` doesn't provide any templates engines. But we recommend [karax](https://github.com/pragmagic/karax) to you. `karax` is a powerful template engine based on DSL. It is suitable for server side rendering.
4 |
5 | You should use `nimble install karax` or `logue extension karax` to install it.
6 |
7 | ```nim
8 | import karax / [karaxdsl, vdom]
9 |
10 | const frameworks = ["Prologue", "Httpx", "Starlight"]
11 |
12 |
13 | proc render*(L: openarray[string]): string =
14 | let vnode = buildHtml(tdiv(class = "mt-3")):
15 | h1: text "Which is my favourite web framework?"
16 | p: text "echo Prologue"
17 |
18 | ul:
19 | for item in L:
20 | li: text item
21 | dl:
22 | dt: text "Is Prologue an elegant web framework?"
23 | dd: text "Yes"
24 | result = $vnode
25 |
26 | echo render(frameworks)
27 | ```
28 |
29 | You can combine them easily and create reusable components for later use. They are just like plain functions. It is very flexible for you to use them.
30 |
31 | ```nim
32 | import karax / [karaxdsl, vdom]
33 |
34 | const frameworks = ["Prologue", "Httpx", "Starlight"]
35 |
36 | proc prepare(L: openarray[string]): VNode =
37 | result = buildHtml(tdiv):
38 | ul:
39 | for item in L:
40 | li: text item
41 | dl:
42 | dt: text "Is Prologue an elegant web framework?"
43 | dd: text "Yes"
44 |
45 | proc render*(L: openarray[string]): VNode =
46 | result = buildHtml(tdiv(class = "mt-3")):
47 | h1: text "Which is my favourite web framework?"
48 | p: text "echo Prologue"
49 | prepare(L)
50 |
51 |
52 | echo $render(frameworks)
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/websocket.md:
--------------------------------------------------------------------------------
1 | # Websocket
2 |
3 | `Prologue` provides `websocket` supports, you need to install `websocketx` first(`nimble install websocketx` or `logue extension websocketx`).
4 |
5 | ## Echo server example
6 |
7 | First create a new websocket object, then you can send msgs to the client. Finally, you receive msgs from the client and send them back to the client.
8 |
9 | ```nim
10 | import prologue
11 | import prologue/websocket
12 |
13 |
14 | proc hello*(ctx: Context) {.async.} =
15 | var ws = await newWebSocket(ctx)
16 | await ws.send("Welcome to simple echo server")
17 | while ws.readyState == Open:
18 | let packet = await ws.receiveStrPacket()
19 | await ws.send(packet)
20 |
21 | resp "Hello, Prologue! "
22 | ```
23 |
24 | ## More details
25 |
26 | You can ref to [ws](https://github.com/treeform/ws) to find more usages.
27 |
--------------------------------------------------------------------------------
/examples/basic/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure set `debug=false`.
4 | debug=true
5 | address=127.0.0.2
6 | port=8080
7 | appName=HelloWorld
8 | staticDir=/static
9 | secretKey=Pr435ol67ogue
--------------------------------------------------------------------------------
/examples/basic/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/staticfile
3 |
4 | import ./urls
5 |
6 | import myctx
7 |
8 | let
9 | env = loadPrologueEnv(".env")
10 | settings = newSettings(
11 | appName = env.getOrDefault("appName", "Prologue"),
12 | debug = env.getOrDefault("debug", true),
13 | port = Port(env.getOrDefault("port", 8080)),
14 | secretKey = env.getOrDefault("secretKey", "")
15 | )
16 |
17 | var app = newApp(settings = settings)
18 | app.use(staticFileMiddleware(env.get("staticDir")))
19 | app.addRoute(urls.urlPatterns, "")
20 |
21 | app.run(DataContext)
22 |
--------------------------------------------------------------------------------
/examples/basic/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/basic/myctx.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | type
4 | DataContext* = ref object of Context
5 | id*: int
6 |
7 | method extend*(ctx: DataContext) {.gcsafe.} =
8 | ctx.id = 999
--------------------------------------------------------------------------------
/examples/basic/readme.md:
--------------------------------------------------------------------------------
1 | # Prologue Basic example
2 | A small example of a prologue application that demonstrates:
3 | 1. How to set up a simple route
4 | 2. One way to load settings
5 | 3. How to use middleware for static file serving
6 | 4. How to extend the context of a request with your own custom data (useful e.g. for adding login data to your context via middleware)
7 |
8 | The binary that this example compiles to serves a response on `/` and also serves file of a directory `./static` (filepath relative to binary placement) as defined by `.env`.
9 |
10 | ### .env file
11 | A tiny config file
12 |
13 | ### app.nim file
14 | The main file of the project.
15 | It loads config values from a small `.env` file via [`loadPrologueEnv`](https://planety.github.io/prologue/configure/) to generate the settings of this application.
16 |
17 | With the settings it then creates the prologue application `app`, which gets [middleware for static file serving](https://planety.github.io/prologue/middleware/) and [a route](https://planety.github.io/prologue/routing/) attached to it.
18 | After all that setup is done, the server is started.
19 |
20 | ### myctx.nim file
21 | Extends the `Context` of a request (which contains the request the user sent, settings of the server and more) by a single field called `id`.
22 |
23 | ### urls.nim file
24 | Simply associates urls (`"/"`) with procs to call when a HTTP request for that url arrives (`hello`).
25 |
26 | This is done in `urls.nim` instead of `views.nim` as an example of how you can structure a prologue application with a clean separation of concerns. This way, the `views` module is only concerned with creating procs that can handle HTTP requests, while the `urls` module is only concerned with mapping which proc should be used when a specific URL gets called.
27 |
28 | The `hello` proc stems from the `views.nim` module
29 |
30 | ### views.nim file
31 | Simply defines a controller/handler proc called `hello` to deal with an incoming HTTP request.
32 |
33 | It makes use of the context that was extended to `DataContext` to echo out the newly defined id field.
34 |
35 | ## Compile and run project
36 | Simply call `nim compile --run app.nim` while in this directory and access 127.0.0.1:8080 URL in your browser.
37 |
--------------------------------------------------------------------------------
/examples/basic/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello)
8 | ]
9 |
--------------------------------------------------------------------------------
/examples/basic/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import myctx
3 |
4 | proc hello*(ctx: Context) {.async.} =
5 | let ctx = DataContext(ctx)
6 | echo ctx.id
7 | resp "Hello, Prologue! This is number " & $ctx.id
8 |
--------------------------------------------------------------------------------
/examples/basicconf/.config/config.custom.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/basicconf/.config/config.debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/basicconf/.config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/basicconf/.config/config.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": false,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/basicconf/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./urls
4 |
5 |
6 | var app = newAppQueryEnv()
7 | app.addRoute(urls.urlPatterns, "")
8 | app.run()
9 |
--------------------------------------------------------------------------------
/examples/basicconf/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/basicconf/readme.md:
--------------------------------------------------------------------------------
1 | # Prologue Configuration example
2 | A small example of a prologue application that demonstrates:
3 | 1. How to set up a simple route
4 | 2. How to change the config file loaded based on the `PROLOGUE` environmental variable
5 |
6 | ### app.nim file
7 | The main file of the project.
8 | It loads the settings from a file in the `.config` directory. [Which specific config file from there is loaded depends on the `PROLOGUE` environment variable](https://planety.github.io/prologue/configure/#Changing-config-file-via-environment-variable).
9 |
10 | With the settings it then creates the prologue application `app`, which gets [a route](https://planety.github.io/prologue/routing/) attached to it.
11 | After all that setup is done, the server is started.
12 |
13 | ### urls.nim file
14 | Simply associates urls (`"/"`) with procs to call when a HTTP request for that url arrives (`hello`).
15 |
16 | This is done in `urls.nim` instead of `views.nim` as an example of how you can structure a prologue application with a clean separation of concerns. This way, the `views` module is only concerned with creating procs that can handle HTTP requests, while the `urls` module is only concerned with mapping which proc should be used when a specific URL gets called.
17 |
18 | The `hello` proc stems from the `views.nim` module
19 |
20 | ### views.nim file
21 | Simply defines a controller/handler proc called `hello` to deal with an incoming HTTP request.
22 |
23 | ## Compile and run project
24 | Simply call `nim compile --run app.nim` while in this directory and access 127.0.0.1:8080 URL in your browser.
--------------------------------------------------------------------------------
/examples/basicconf/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello)
8 | ]
9 |
--------------------------------------------------------------------------------
/examples/basicconf/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | proc hello*(ctx: Context) {.async.} =
4 | resp "Hello, Prologue! "
5 |
--------------------------------------------------------------------------------
/examples/blog/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | debug=true
4 | port=8080
5 | appName=Blog
6 | staticDir=/static
7 | secretKey=test
8 | dbPath=blog.db
--------------------------------------------------------------------------------
/examples/blog/README.md:
--------------------------------------------------------------------------------
1 | # Prologue Blog Example
2 | ## Purpose of this example
3 | The purpose of this 'Blog' example is to cover following basic actions:
4 | - User registration.
5 | - User authentication.
6 | - List multiple records.
7 | - Edit or delete particular record
8 | - Create new record.
9 |
10 | ## Project structure
11 | The structure is straightforward:
12 | ```
13 | static
14 | templates
15 | .env
16 | app.nim
17 | blog.db
18 | consts.nim
19 | initdb.nim
20 | schema.sql
21 | urls.nim
22 | views.nim
23 | ```
24 |
25 | ## Screenshot
26 |
27 | 
28 |
29 | ### Static folder
30 | Every public assets are stored in this folder.
31 |
32 | ### Templates folder
33 | This folder is for storing templates written with Karax DSL (domain specific language) which is kind of similar to the popular Pug template engine approach.
34 | Each template file consists of two procs inside:
35 | - proc ended with `Page` name - it acts as a final template (e.g. `indexPage` or `loginPage` and etc). It's a common top level structure of our template and generally it's the same across all pages.
36 | - proc ended with `Section` name - it is where our actual template layout and logic is written.
37 |
38 | There are also commonly used blocks (or `chunks`/`partials` - whatever you call them) that are stored in `share` subfolder and we call them inside our templates to reduce duplication in our code.
39 |
40 | ### .env file
41 | Env file holds values necessary to run an application.
42 |
43 | ### app.nim file
44 | Our starting point is where we create the Prologue app.
45 |
46 | ### blog.db
47 | A SQLite database file that will be created automatically when the application runs.
48 |
49 | ### consts.nim
50 | A small file to store constants like database and schema files (yes we could use an .env file for that so existence of this file is questionable).
51 |
52 | ### initdb.nim and schema.sql
53 | A small proc for automatically creating a sqlite db file using schema.sql file if `blog.db` file is absent.
54 |
55 | ### urls.nim
56 | This file is for grouping URL endpoints and linking them to their corresponding procs from `views.nim`.
57 |
58 | ### views.nim
59 | This file contains business logic that is called from 'attached' urls. It's like a 'controller' or a 'route'
60 |
61 | ## Compile and run project
62 | Simply call `nim compile --run app.nim` and access 127.0.0.1:8080 URL in your browser.
63 |
64 | ## Miscellaneous
65 | This example uses Marx classless framework with custom modifications made by @keshon
--------------------------------------------------------------------------------
/examples/blog/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/signedcookiesession
3 | import prologue/middlewares/staticfile
4 |
5 | import ./urls
6 | import ./initdb
7 |
8 |
9 | initDb()
10 |
11 | let
12 | env = loadPrologueEnv(".env")
13 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
14 | debug = env.getOrDefault("debug", true),
15 | port = Port(env.getOrDefault("port", 8080)),
16 | secretKey = env.getOrDefault("secretKey", "")
17 | )
18 |
19 | var app = newApp(settings = settings)
20 |
21 | app.use(staticFileMiddleware(env.get("staticDir")), sessionMiddleware(settings, path = "/"))
22 | app.addRoute(urls.indexPatterns, "/")
23 | app.addRoute(urls.authPatterns, "/auth")
24 | app.addRoute(urls.blogPatterns, "/blog")
25 | app.run()
26 |
--------------------------------------------------------------------------------
/examples/blog/blog.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/examples/blog/blog.db
--------------------------------------------------------------------------------
/examples/blog/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/blog/consts.nim:
--------------------------------------------------------------------------------
1 | const
2 | dbPath* = "blog.db"
3 | schemaPath* = "schema.sql"
4 |
--------------------------------------------------------------------------------
/examples/blog/initdb.nim:
--------------------------------------------------------------------------------
1 | import std/[os, strutils, logging]
2 | import db_connector/db_sqlite
3 | import ./consts
4 |
5 |
6 | proc initDb*() =
7 | if not fileExists(consts.dbPath):
8 | let
9 | db = open(consts.dbPath, "", "", "")
10 | schema = readFile(schemaPath)
11 | for line in schema.split(";"):
12 | if line == "\c\n" or line == "\n":
13 | continue
14 | db.exec(sql(line.strip))
15 | db.close()
16 | logging.info("Initialized the database.")
17 |
--------------------------------------------------------------------------------
/examples/blog/schema.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS users;
2 | DROP TABLE IF EXISTS posts;
3 |
4 | CREATE TABLE users (
5 | id INTEGER PRIMARY KEY AUTOINCREMENT,
6 | fullname TEXT NOT NULL,
7 | username TEXT UNIQUE NOT NULL,
8 | password TEXT NOT NULL
9 | );
10 |
11 | CREATE TABLE posts (
12 | id INTEGER PRIMARY KEY AUTOINCREMENT,
13 | author_id INTEGER NOT NULL,
14 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | title TEXT NOT NULL,
16 | body TEXT NOT NULL,
17 | FOREIGN KEY (author_id) REFERENCES users (id)
18 | );
19 |
--------------------------------------------------------------------------------
/examples/blog/screenshot/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/examples/blog/screenshot/screenshot.jpg
--------------------------------------------------------------------------------
/examples/blog/static/blob.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/examples/blog/static/layout.css:
--------------------------------------------------------------------------------
1 | body { max-width: 768px; margin: 0 auto;}
2 |
3 | header{
4 | margin: 0;
5 | }
6 |
7 | .brand {
8 | flex: auto;
9 | font-weight: 700;
10 | padding: 1rem 0;
11 | }
12 | .brand a {
13 | color: #000;
14 | text-decoration: none;
15 | font-size: 1.5rem;
16 | vertical-align: text-top;
17 | font-weight: 500;
18 | background-color: white;
19 | filter: invert(1);
20 | padding: 0.3rem 0.85rem 0.3rem 0.3rem;
21 | border-radius: 0.35rem;
22 | letter-spacing: 1px;
23 | text-transform: uppercase;
24 | }
25 | .brand a:before {
26 | content: "";
27 | background: url(blob.svg) no-repeat;
28 | height: 35px;
29 | width: 35px;
30 | display: inline-flex;
31 | }
32 |
33 | nav {display: flex; align-items: center;}
34 | nav ul {
35 | display: flex; list-style: none; margin: 0; padding: 0;
36 | }
37 | nav ul li a,
38 | nav ul li span,
39 | header .action {
40 | display: block;
41 | padding: 0.5rem;
42 | list-style: none;
43 | }
44 |
45 | main {
46 | display: block;
47 | margin: 0 auto;
48 | max-width: 768px;
49 | /* padding: 0 16px 16px; */
50 | border: 1px solid #ccc;
51 | border-radius: 0.35rem;
52 | padding: 1rem 1rem 0 1rem;
53 | margin-bottom: 2rem;
54 | }
55 |
56 | a.action:first-child:after {
57 | content: "";
58 | border-bottom: 1px solid #ccc;
59 | display: block;
60 | margin: 0 -1rem;
61 | padding-bottom: 1rem;
62 | }
63 | a.action:first-child {
64 | margin-bottom: 1rem;
65 | display: block;
66 | }
67 |
68 | .alert {
69 | background-color: #000;
70 | color: #fff;
71 | margin: 1rem -1rem;
72 | padding: 0.5rem 1rem;
73 | }
74 | .is-empty {
75 | min-height: 10rem;
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | font-weight: 700;
80 | color: #ccc;
81 | font-size: 2rem;
82 | margin-bottom: 1rem;
83 | }
84 |
85 | .post {
86 | /*margin-bottom: 3em;*/
87 | }
88 | .post:after {
89 | content: "";
90 | border-bottom: 1px solid #ccc;
91 | display: block;
92 | margin: 0 -1rem;
93 | padding-top: 1rem;
94 | }
95 | .post:last-of-type {
96 | margin-bottom: 0;
97 | }
98 | .post:last-of-type:after {
99 | border: medium none;
100 | }
101 | .post h3 {
102 | font-size: 1.4rem;
103 | }
104 | .post:first-of-type h3 {
105 | margin-top: 0;
106 | }
107 | .post .about {
108 | color: #bfbfbf;
109 | margin-bottom: 1rem;
110 | }
111 |
112 |
113 | form,
114 | form input,
115 | form textarea {
116 | margin-bottom: 1em;
117 | }
118 | form textarea {
119 | min-height: 12em;
120 | resize: vertical;
121 | }
122 | form input[type="submit"] {
123 | margin-bottom: 0;
124 | min-width: 10em;
125 | }
126 | form a {
127 | text-align: center;
128 | font-weight: 400;
129 | line-height: 1.5;
130 | padding: 8px 16px;
131 | vertical-align: middle;
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/examples/blog/templates/editpost.nim:
--------------------------------------------------------------------------------
1 | import karax / [karaxdsl, vdom]
2 | import prologue
3 |
4 | import
5 | share/head,
6 | share/nav
7 |
8 |
9 | proc editSection*(ctx: Context, post: seq[string] = @[]): VNode =
10 | var
11 | id = ""
12 | title = ""
13 | content = ""
14 |
15 | if post.len > 0:
16 | id = post[0]
17 | title = post[3]
18 | content = post[4]
19 |
20 | result = buildHtml(main(class = "content")):
21 | h4: text "Edit post"
22 | form(`method` = "post"):
23 | if id.len > 0:
24 | input(`type` = "hidden", name = "id", value = id)
25 | label(`for` = "title"): text "Blog title"
26 | input(name = "title", id = "title", required = "required", value = title)
27 | label(`for` = "content"): text "Blog content"
28 | textarea(name = "content", id = "content", required = "required"):
29 | text content
30 | tdiv:
31 | input(`type` = "submit", value = "Save")
32 | a(href = "/"): text "Cancel"
33 | if id.len > 0:
34 | a(href = "/blog/delete/" & id): text "Delete"
35 |
36 |
37 | proc editPage*(ctx: Context, title: string, post: seq[string] = @[]): string =
38 | let head = sharedHead(ctx, title)
39 | let nav = sharedNav(ctx)
40 | let edit = editSection(ctx, post)
41 | let vNode = buildHtml(html):
42 | head
43 | nav
44 | edit
45 |
46 | result = "\n" & $vNode
47 |
--------------------------------------------------------------------------------
/examples/blog/templates/index.nim:
--------------------------------------------------------------------------------
1 | import strtabs, strformat
2 |
3 | import karax / [karaxdsl, vdom]
4 | import prologue
5 |
6 | import
7 | share/head,
8 | share/nav
9 |
10 |
11 | # This is 'primary' section for our template it separated for convenience
12 | proc indexSection*(ctx: Context, posts: seq[seq[string]]): VNode =
13 | result = buildHtml(main(class = "content")):
14 | #h3: text "Posts"
15 | let poi = ctx.session.getOrDefault("userId")
16 | if poi.len != 0:
17 | a(class = "action", href = "/blog/create"): text "Create new post"
18 |
19 | if posts.len > 0:
20 | for post in posts:
21 | tdiv(class = "post"):
22 | tdiv:
23 | h3: text post[3]
24 | tdiv(class = "about"): text fmt"""by {post[1]} on {post[2]}"""
25 | p(class = "body"): text post[4]
26 | if poi == post[1]:
27 | a(class = "action", href = fmt"""/blog/update/{post[0]}"""): text "Edit"
28 | else:
29 | tdiv(class = "is-empty"):
30 | text "This blog is empty"
31 |
32 |
33 | # This is composed HTML view that should be exposed to relevant route/controller/view
34 | # There is no 'extend' feature so we 'include' our html sections/partials/html chunks into final template
35 | proc indexPage*(ctx: Context, title: string, posts: seq[seq[string]]): string =
36 | let head = sharedHead(ctx, title) # 'shared' head part
37 | let nav = sharedNav(ctx) # 'shared' navbar
38 | let posts = indexSection(ctx, posts) # 'primary' section from above
39 | let vNode = buildHtml(html):
40 | # Call our sections
41 | head
42 | nav
43 | posts
44 |
45 | # Don't forget Doctype declaration to avoid any failing validation tests
46 | # like this one https://validator.w3.org/
47 | result = "\n" & $vNode
48 |
--------------------------------------------------------------------------------
/examples/blog/templates/login.nim:
--------------------------------------------------------------------------------
1 | when (compiles do: import karax / karaxdsl):
2 | import karax / [karaxdsl, vdom]
3 | else:
4 | {.error: "Please use `logue extension karax` to install!".}
5 |
6 | import prologue
7 |
8 | import
9 | share/head,
10 | share/nav
11 |
12 |
13 | proc loginSection*(ctx: Context, title: string, error: string = ""): VNode =
14 | result = buildHtml(main(class = "content")):
15 | h3: text title
16 | if error.len > 0:
17 | tdiv(class = "alert"):
18 | text error
19 | form(`method` = "post"):
20 | label(`for` = "username"): text "Username"
21 | input(name = "username", id = "username", required = "required")
22 | label(`for` = "password"): text "Password"
23 | input(`type` = "password", name = "password", id = "password",
24 | required = "required")
25 | input(`type` = "submit", value = "Login")
26 |
27 |
28 | proc loginPage*(ctx: Context, title: string, error: string = ""): string =
29 | let head = sharedHead(ctx, title)
30 | let nav = sharedNav(ctx)
31 | let login = loginSection(ctx, title, error)
32 | let vNode = buildHtml(html):
33 | head
34 | nav
35 | login
36 |
37 | result = "\n" & $vNode
38 |
--------------------------------------------------------------------------------
/examples/blog/templates/register.nim:
--------------------------------------------------------------------------------
1 | import karax / [karaxdsl, vdom]
2 | import prologue
3 |
4 | import
5 | share/head,
6 | share/nav
7 |
8 |
9 | proc registerSection*(ctx: Context, error: string = ""): VNode =
10 | result = buildHtml(main(class = "content")):
11 | h3: text "Register"
12 | if error.len > 0:
13 | tdiv(class = "alert"):
14 | text error
15 | form(`method` = "post"):
16 | label(`for` = "fullname"): text "Full name"
17 | input(name = "fullname", id = "fullname", required = "required")
18 | label(`for` = "username"): text "Username"
19 | input(name = "username", id = "username", required = "required")
20 | label(`for` = "password"): text "Password"
21 | input(`type` = "password", name = "password", id = "password",
22 | required = "required")
23 | input(`type` = "submit", value = "Register")
24 |
25 |
26 | proc registerPage*(ctx: Context, title: string, error: string = ""): string =
27 | let head = sharedHead(ctx, title)
28 | let nav = sharedNav(ctx)
29 | let register = registerSection(ctx, error)
30 | let vNode = buildHtml(html):
31 | head
32 | nav
33 | register
34 |
35 | result = "\n" & $vNode
36 |
--------------------------------------------------------------------------------
/examples/blog/templates/share/head.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import karax/[karaxdsl, vdom]
3 |
4 |
5 | # This is html section. It's shared across all templates
6 | proc sharedHead*(ctx: Context, title: string): VNode =
7 | let
8 | env = loadPrologueEnv(".env")
9 | appName = env.getOrDefault("appName", "Prologue")
10 |
11 | let vNode = buildHtml(head):
12 | title: text title & " - " & appName
13 | link(rel = "stylesheet", href = "/static/main.css")
14 | link(rel = "stylesheet", href = "/static/layout.css")
15 | return vNode
16 |
--------------------------------------------------------------------------------
/examples/blog/templates/share/nav.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import karax/[karaxdsl, vdom]
3 |
4 |
5 | proc sharedNav*(ctx: Context): VNode =
6 | let fullname = ctx.session.getOrDefault("userFullname", "")
7 | let vNode = buildHtml(header):
8 | nav:
9 | tdiv(class = "brand"):
10 | a(href = "/"):
11 | text "Blog Example"
12 | ul:
13 | if fullname.len == 0:
14 | li: a(href = "/auth/register"): text "Register"
15 | li: a(href = "/auth/login"): text "Log In"
16 | else:
17 | li: span: text fullname
18 | li: a(href = "/auth/logout"): text "Log Out"
19 |
20 | return vNode
21 |
--------------------------------------------------------------------------------
/examples/blog/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | let
7 | indexPatterns* = @[
8 | pattern("/", views.read, @[HttpGet], name = "index")
9 | ]
10 | authPatterns* = @[
11 | pattern("/login", views.login, @[HttpGet, HttpPost], name = "login"),
12 | pattern("/register", views.register, @[HttpGet, HttpPost]),
13 | pattern("/logout", views.logout, @[HttpGet, HttpPost]),
14 | ]
15 | blogPatterns* = @[
16 | pattern("/create", views.create, @[HttpGet, HttpPost], name = "create"),
17 | pattern("/update/{id}", views.update, @[HttpGet, HttpPost],
18 | name = "update"),
19 | pattern("/delete/{id}", views.delete, @[HttpGet], name = "delete")
20 | ]
21 |
--------------------------------------------------------------------------------
/examples/csrf/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=csrf
7 | staticDir=/static
8 | secretKey=Pr435ol67ogue
9 |
--------------------------------------------------------------------------------
/examples/csrf/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/staticfile
3 |
4 | import ./urls
5 |
6 | let
7 | env = loadPrologueEnv(".env")
8 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
9 | debug = env.getOrDefault("debug", true),
10 | port = Port(env.getOrDefault("port", 8080)),
11 | secretKey = env.getOrDefault("secretKey", "")
12 | )
13 |
14 |
15 | var app = newApp(settings = settings)
16 |
17 | app.use(staticFileMiddleware(env.get("staticDir")))
18 | app.addRoute(urls.urlPatterns, "")
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/csrf/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/csrf/csrf.nimf:
--------------------------------------------------------------------------------
1 | #? stdtmpl | standard
2 | #import prologue/middlewares/csrf
3 | #
4 | #proc alignForm(tok: string): string =
5 | # result = ""
6 |
10 | #end proc
11 |
--------------------------------------------------------------------------------
/examples/csrf/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello)
8 | ]
9 |
--------------------------------------------------------------------------------
/examples/csrf/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | include "csrf.nimf"
4 |
5 | proc hello*(ctx: Context) {.async.} =
6 | resp alignForm(csrfToken(ctx))
7 |
--------------------------------------------------------------------------------
/examples/helloworld/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=HelloWorld
7 | staticDir=/static
8 | secretKey=Pr435ol67ogue
--------------------------------------------------------------------------------
/examples/helloworld/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/staticfile
3 | import prologue/middlewares/utils
4 | from prologue/openapi import serveDocs
5 |
6 | # import logging
7 |
8 | import views, urls
9 |
10 | let
11 | env = loadPrologueEnv(".env")
12 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
13 | debug = env.getOrDefault("debug", true),
14 | address = env.getOrDefault("address", ""),
15 | port = Port(env.getOrDefault("port", 8080)),
16 | secretKey = env.getOrDefault("secretKey", "")
17 | )
18 |
19 |
20 | proc setLoggingLevel() =
21 | discard
22 | # addHandler(newConsoleLogger())
23 | # logging.setLogFilter(lvlInfo)
24 |
25 | let
26 | event = initEvent(setLoggingLevel)
27 | var
28 | app = newApp(settings = settings,
29 | startup = @[event])
30 |
31 | app.use(staticFileMiddleware(env.get("staticDir")))
32 | app.use(debugRequestMiddleware())
33 | app.addRoute(urls.urlPatterns, "/todolist")
34 | # only supports (?Pexp)
35 | app.addRoute(re"/post(?P[\d]+)", articles, HttpGet)
36 | app.addRoute(re"/post(?P[\d]+)", articles, HttpGet)
37 |
38 | app.addRoute("/", home, HttpGet)
39 | app.addRoute("/", home, HttpPost)
40 | app.addRoute("/index.html", index, HttpGet, name = "index")
41 | app.addRoute("/prefix/home", home, HttpGet)
42 | app.addRoute("/home", home, HttpGet)
43 | app.addRoute("/hello", hello, HttpGet)
44 | app.addRoute("/redirect", testRedirect, HttpGet)
45 | app.addRoute("/login", login, HttpGet)
46 | app.addRoute("/login", do_login, HttpPost)
47 | # will match /hello/Nim and /hello/
48 | app.addRoute("/hello/{name}", helloName, HttpGet, name = "helloname")
49 | app.addRoute("/multipart", multiPart, HttpGet)
50 | app.addRoute("/multipart", do_multiPart, HttpPost)
51 | app.addRoute("/upload", upload, HttpGet)
52 | app.addRoute("/upload", do_upload, HttpPost)
53 | # server openapi
54 | app.serveDocs("docs/openapi.json")
55 | app.run()
56 |
--------------------------------------------------------------------------------
/examples/helloworld/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/helloworld/docs/openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.2",
3 | "info": {
4 | "title": "HelloWorld",
5 | "description": "My Conquest is the Sea of Stars.",
6 | "termsOfService": "",
7 | "contact": {
8 | "name": "",
9 | "url": "",
10 | "email": ""
11 | },
12 | "license": {
13 | "name": "MIT",
14 | "url": "https://www.mit-license.org"
15 | },
16 | "version": "0.1.4"
17 | },
18 | "paths": {
19 | "/": {
20 | "get": {
21 | "summary": "Root",
22 | "operationId": "root__get",
23 | "responses": {
24 | "200": {
25 | "description": "Successful Response",
26 | "content": {
27 | "application/json": {
28 | "schema": {}
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/examples/helloworld/static/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello
4 |
5 |
6 | Do something for fun.
7 | This is the hello page.
8 |
9 |
--------------------------------------------------------------------------------
/examples/helloworld/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Nim
4 |
5 |
6 | Do something for fun.
7 | This is the index page.
8 |
9 |
--------------------------------------------------------------------------------
/examples/helloworld/static/upload.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/helloworld/templates/basic.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/examples/helloworld/templates/basic.nim
--------------------------------------------------------------------------------
/examples/helloworld/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 |
4 | import ./views
5 |
6 |
7 | const urlPatterns* = @[
8 | pattern("/", home),
9 | pattern("/", home, HttpPost),
10 | pattern("/home", home),
11 | pattern("/login", login),
12 | pattern("/login", do_login, HttpPost),
13 | pattern("/redirect", testRedirect),
14 | pattern("/multipart", multipart)
15 | ]
16 |
--------------------------------------------------------------------------------
/examples/helloworld/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import std/[tables, logging]
3 |
4 |
5 | proc articles*(ctx: Context) {.async.} =
6 | resp $ctx.getPathParams("num", 1)
7 |
8 | proc hello*(ctx: Context) {.async.} =
9 | # await sleepAsync(3000)
10 | resp "Hello, Prologue! "
11 | if true:
12 | raise newException(ValueError, "can't be reached")
13 |
14 | proc index*(ctx: Context) {.async.} =
15 | await ctx.staticFileResponse("index.html", "static")
16 |
17 | proc helloName*(ctx: Context) {.async.} =
18 | logging.debug ctx.getPathParams("name")
19 | resp "Hello, " & ctx.getPathParams("name", "World") & " "
20 |
21 | proc home*(ctx: Context) {.async.} =
22 | logging.debug urlFor(ctx, "index")
23 | logging.debug urlFor(ctx, "helloname", {"name": "flywind"}, {"age": "20"})
24 | logging.debug ctx.request.queryParams.getOrDefault("name", "")
25 | resp redirect(urlFor(ctx, "helloname", {"name": "flywind"}, {"age": "20", "hobby": "Nim"}), Http302)
26 |
27 | proc testRedirect*(ctx: Context) {.async.} =
28 | resp redirect("/hello", Http302)
29 |
30 | proc login*(ctx: Context) {.async.} =
31 | resp loginPage()
32 |
33 | proc do_login*(ctx: Context) {.async.} =
34 | logging.debug "-----------------------------------------------------"
35 | logging.debug ctx.request.postParams
36 | resp redirect("/hello/Nim")
37 |
38 | proc multiPart*(ctx: Context) {.async.} =
39 | resp multiPartPage()
40 |
41 | proc do_multiPart*(ctx: Context) {.async.} =
42 | logging.debug ctx.request.formParams["username"].body
43 | logging.debug ctx.request.formParams["password"].body
44 | resp redirect("/login")
45 |
46 | proc upload*(ctx: Context) {.async.} =
47 | await ctx.staticFileResponse("upload.html", "static")
48 |
49 | proc do_upload*(ctx: Context) {.async.} =
50 | logging.debug ctx.request.formParams
51 | resp ctx.request.formParams["file"].body
52 |
--------------------------------------------------------------------------------
/examples/memorysession/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=session
7 | staticDir=/static
8 | secretKey=Pr435ol67ogue
9 |
--------------------------------------------------------------------------------
/examples/memorysession/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/sessions/memorysession
3 | import prologue/middlewares/utils
4 | import ./urls
5 |
6 |
7 | let
8 | env = loadPrologueEnv(".env")
9 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
10 | debug = env.getOrDefault("debug", true),
11 | port = Port(env.getOrDefault("port", 8080)),
12 | secretKey = env.getOrDefault("secretKey", "")
13 | )
14 |
15 | var app = newApp(settings = settings)
16 |
17 | app.use(@[debugResponseMiddleware(), sessionMiddleware(settings)])
18 | app.addRoute(urls.urlPatterns, "")
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/memorysession/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/memorysession/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello),
8 | pattern("/login", login),
9 | pattern("/logout", logout),
10 | pattern("/print", print)
11 | ]
12 |
--------------------------------------------------------------------------------
/examples/memorysession/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | proc hello*(ctx: Context) {.async.} =
4 | resp "Hello, Prologue! "
5 |
6 | proc login*(ctx: Context) {.async.} =
7 | ctx.session["flywind"] = "123"
8 | ctx.session["ordontfly"] = "345"
9 | ## Be careful when using session or csrf middlewares,
10 | ## Response object will cover the headers of before.
11 | resp htmlResponse("Login ", headers = ctx.response.headers)
12 |
13 | proc print*(ctx: Context) {.async.} =
14 | resp $ctx.session
15 |
16 | proc logout*(ctx: Context) {.async.} =
17 | ctx.session.clear()
18 | resp htmlResponse("Logout ", headers = ctx.response.headers)
19 |
--------------------------------------------------------------------------------
/examples/redissession/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=session
7 | staticDir=/static
8 | secretKey=Pr435ol67ogue
9 |
--------------------------------------------------------------------------------
/examples/redissession/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/sessions/redissession
3 | import prologue/middlewares/utils
4 | import ./urls
5 |
6 |
7 | let
8 | env = loadPrologueEnv(".env")
9 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
10 | debug = env.getOrDefault("debug", true),
11 | port = Port(env.getOrDefault("port", 8080)),
12 | secretKey = env.getOrDefault("secretKey", "")
13 | )
14 |
15 | var app = newApp(settings = settings)
16 |
17 | app.use([debugResponseMiddleware(), sessionMiddleware(settings)])
18 | app.addRoute(urls.urlPatterns, "")
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/redissession/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/redissession/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello),
8 | pattern("/login", login),
9 | pattern("/logout", logout),
10 | pattern("/print", print)
11 | ]
12 |
--------------------------------------------------------------------------------
/examples/redissession/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | proc hello*(ctx: Context) {.async.} =
4 | resp "Hello, Prologue! "
5 |
6 | proc login*(ctx: Context) {.async.} =
7 | ctx.session["flywind"] = "123"
8 | ctx.session["ordontfly"] = "345"
9 | ## Be careful when using session or csrf middlewares,
10 | ## Response object will cover the headers of before.
11 | resp htmlResponse("Login ", headers = ctx.response.headers)
12 |
13 | proc print*(ctx: Context) {.async.} =
14 | resp $ctx.session
15 |
16 | proc logout*(ctx: Context) {.async.} =
17 | ctx.session.clear()
18 | resp htmlResponse("Logout ", headers = ctx.response.headers)
19 |
--------------------------------------------------------------------------------
/examples/signedcookiesession/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=session
7 | staticDir=/static
8 | secretKey=Pr435ol67ogue
9 |
--------------------------------------------------------------------------------
/examples/signedcookiesession/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/utils
3 | import prologue/middlewares/signedcookiesession
4 | import ./urls
5 |
6 |
7 | let
8 | env = loadPrologueEnv(".env")
9 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
10 | debug = env.getOrDefault("debug", true),
11 | port = Port(env.getOrDefault("port", 8080)),
12 | secretKey = env.getOrDefault("secretKey", "")
13 | )
14 |
15 | var app = newApp(settings = settings)
16 |
17 | app.use(@[debugRequestMiddleware(), sessionMiddleware(settings)])
18 | app.addRoute(urls.urlPatterns, "")
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/signedcookiesession/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/signedcookiesession/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 |
6 | const urlPatterns* = @[
7 | pattern("/", hello),
8 | pattern("/login", login),
9 | pattern("/logout", logout)
10 | ]
11 |
--------------------------------------------------------------------------------
/examples/signedcookiesession/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | proc hello*(ctx: Context) {.async.} =
4 | resp "Hello, Prologue! "
5 |
6 | proc login*(ctx: Context) {.async.} =
7 | ctx.session["flywind"] = "123"
8 | ctx.session["ordontfly"] = "345"
9 | resp "Hello, Prologue! "
10 |
11 | proc logout*(ctx: Context) {.async.} =
12 | resp $ctx.session
13 |
--------------------------------------------------------------------------------
/examples/todoapp/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/staticfile
3 |
4 |
5 | proc home(ctx: Context) {.async.} =
6 | resp readFile("templates/todoapp.html")
7 |
8 |
9 | var
10 | app = newApp(newSettings(port = Port(8080)))
11 |
12 | app.use(staticFileMiddleware("templates"))
13 | app.addRoute("/home", home)
14 | app.run()
15 |
--------------------------------------------------------------------------------
/examples/todoapp/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/todoapp/readme.md:
--------------------------------------------------------------------------------
1 | Examples from [Karax](https://github.com/pragmagic/karax)
2 | Karax is a framework for developing single page applications in Nim.
3 | You should install karax first.
4 | ```bash
5 | nimble install karax
6 | ```
7 |
--------------------------------------------------------------------------------
/examples/todoapp/templates/karax.license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Xored Software, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/examples/todoapp/templates/todoapp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo app
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/todoapp/templates/todoapp.nims:
--------------------------------------------------------------------------------
1 | setCommand "js"
--------------------------------------------------------------------------------
/examples/todoapp/todoapp.nims:
--------------------------------------------------------------------------------
1 | setCommand "js"
--------------------------------------------------------------------------------
/examples/todolist/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | debug=true
4 | port=8080
5 | appName=TodoList
6 | staticDir=/static
7 | secretKey=Pr435ol67ogue
--------------------------------------------------------------------------------
/examples/todolist/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/middlewares/utils
3 | import prologue/middlewares/staticfile
4 | import ./urls
5 |
6 | let
7 | env = loadPrologueEnv(".env")
8 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
9 | debug = env.getOrDefault("debug", true),
10 | port = Port(env.getOrDefault("port", 8080)),
11 | secretKey = env.getOrDefault("secretKey", "")
12 | )
13 |
14 | var app = newApp(settings = settings)
15 |
16 | app.use(staticFileMiddleware(env.get("staticDir")))
17 | app.use(debugRequestMiddleware())
18 | app.addRoute(urls.urlPatterns, "")
19 | app.run()
20 |
--------------------------------------------------------------------------------
/examples/todolist/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/todolist/readme.md:
--------------------------------------------------------------------------------
1 | # todolist
2 | Execute `nim c -r app.nim` and visit `localhost:8080`.
3 |
--------------------------------------------------------------------------------
/examples/todolist/templates/basic.nim:
--------------------------------------------------------------------------------
1 | when (compiles do: import karax / karaxdsl):
2 | import karax / [karaxdsl, vdom]
3 | else:
4 | {.error: "Please use `logue extension karax` to install!".}
5 |
6 |
7 | import strformat
8 |
9 |
10 | proc makeList*(rows: seq[seq[string]]): string =
11 | let vnode = buildHtml(html):
12 | p: text "List items are as follows:"
13 | table(border = "1"):
14 | for row in rows:
15 | tr:
16 | for i, col in row:
17 | if i == 0:
18 | td: a(href = fmt"/item/{col}"): text col
19 | else:
20 | td: text col
21 | td: a(href = fmt"/edit/{row[0]}"): text "Edit"
22 | p: a(href = "/new"): text "New Item"
23 | result = $vnode
24 |
25 | proc editList*(id: int, value: seq[string]): string =
26 | let vnode = buildHtml(html):
27 | p: text fmt"Edit the task with ID = {id}"
28 | form(action = fmt"/edit/{id}", `method` = "get"):
29 | input(`type` = "text", name = "task", value = value[0], size = "100",
30 | maxlength = "80")
31 | select(name = "status"):
32 | option: text "open"
33 | option: text "closed"
34 | br()
35 | input(`type` = "submit", name = "save", value = "save")
36 | result = $vnode
37 |
38 | proc newList*(): string =
39 | let vnode = buildHtml(html):
40 | p: text "Add a new task to the ToDo list:"
41 | form(action = "/new", `method` = "get"):
42 | input(`type` = "text", size = "100", maxlength = "80", name = "task")
43 | input(`type` = "submit", name = "save", value = "save")
44 | result = $vnode
45 |
46 |
47 | when isMainModule:
48 | let t = makeList(@[@["1", "2", "3"], @["4", "6", "9"]])
49 | let e = editList(12, @["ok"])
50 | let n = newList()
51 | writeFile("todo.html", t)
52 | writeFile("edit.html", e)
53 | writeFile("new.html", n)
54 |
--------------------------------------------------------------------------------
/examples/todolist/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 |
3 | import ./views
4 |
5 | const urlPatterns* = @[
6 | pattern("/", todoList),
7 | pattern("/new", newItem),
8 | pattern("/edit/{id}", editItem),
9 | pattern("/item/{item}", showItem)
10 | ]
11 |
--------------------------------------------------------------------------------
/examples/todolist/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import std/[db_sqlite, strformat, strutils]
3 | from std/sqlite3 import last_insert_rowid
4 |
5 | import ./templates/basic
6 |
7 | let
8 | db = open("todo.db", "", "", "") # Warning: This file is created in the current directory
9 |
10 | if not db.tryExec(sql"select count(*) from todo"):
11 | db.exec(sql"CREATE TABLE todo (id INTEGER PRIMARY KEY, task char(80) NOT NULL, status bool NOT NULL)")
12 | db.exec(sql"""INSERT INTO todo (task,status) VALUES ("Nim lang",0)""")
13 | db.exec(sql"""INSERT INTO todo (task,status) VALUES ("Prologue web framework",1)""")
14 | db.exec(sql"""INSERT INTO todo (task,status) VALUES ("Let's start to study Prologue web framework",1)""")
15 | db.exec(sql"""INSERT INTO todo (task,status) VALUES ("My favourite web framework",1)""")
16 | db.close()
17 |
18 | proc todoList*(ctx: Context) {.async.} =
19 | let db = open("todo.db", "", "", "")
20 | let rows = db.getAllRows(sql("""SELECT id, task FROM todo WHERE status LIKE "1""""))
21 | db.close()
22 | resp htmlResponse(makeList(rows=rows))
23 |
24 | proc newItem*(ctx: Context) {.async.} =
25 | if ctx.getQueryParams("save").len != 0:
26 | let
27 | row = ctx.getQueryParams("task").strip
28 | db = open("todo.db", "", "", "")
29 | db.exec(sql"INSERT INTO todo (task,status) VALUES (?,?)", row, 1)
30 | let
31 | id = last_insert_rowid(db)
32 | db.close()
33 | resp htmlResponse(fmt"The new task was inserted into the database, the ID is {id}
Back to list ")
34 | else:
35 | resp htmlResponse(newList())
36 |
37 | proc editItem*(ctx: Context) {.async.} =
38 | if ctx.getQueryParams("save").len != 0:
39 | let
40 | edit = ctx.getQueryParams("task").strip
41 | status = ctx.getQueryParams("status").strip
42 | id = ctx.getPathParams("id", "")
43 | var statusId = 0
44 | if status == "open":
45 | statusId = 1
46 | let db= open("todo.db", "", "", "")
47 | db.exec(sql"UPDATE todo SET task = ?, status = ? WHERE id LIKE ?", edit, statusId, id)
48 | db.close()
49 | resp htmlResponse(fmt"The item number {id} was successfully updated
Back to list ")
50 | else:
51 | let db= open("todo.db", "", "", "")
52 | let id = ctx.getPathParams("id", "")
53 | let data = db.getAllRows(sql"SELECT task FROM todo WHERE id LIKE ?", id)
54 | resp htmlResponse(editList(id.parseInt, data[0]))
55 |
56 | proc showItem*(ctx: Context) {.async.} =
57 | let
58 | db = open("todo.db", "", "", "")
59 | item = ctx.getPathParams("item", "")
60 | rows = db.getAllRows(sql"SELECT task, status FROM todo WHERE id LIKE ?", item)
61 | db.close()
62 | let home_link = """Back to list """
63 | if rows.len == 0:
64 | resp "This item number does not exist!" & home_link
65 | else:
66 | let
67 | task = rows[0][0]
68 | status = block:
69 | if rows[0][1] == "1":
70 | "Done"
71 | else:
72 | "Doing"
73 | resp fmt"Task: {task} Status: {status}" & home_link
74 |
--------------------------------------------------------------------------------
/examples/websocket/.env:
--------------------------------------------------------------------------------
1 | # Don't commit this to source control.
2 | # Eg. Make sure ".env" in your ".gitignore" file.
3 | # If you want to release you programs, make sure debug=false.
4 | debug=true
5 | port=8080
6 | appName=websocket
7 | secretKey=Pr435ol67ogue
8 |
--------------------------------------------------------------------------------
/examples/websocket/app.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import ./urls
3 |
4 | let
5 | env = loadPrologueEnv(".env")
6 | settings = newSettings(appName = env.getOrDefault("appName", "Prologue"),
7 | debug = env.getOrDefault("debug", true),
8 | port = Port(env.getOrDefault("port", 8080)),
9 | secretKey = env.getOrDefault("secretKey", "")
10 | )
11 |
12 | var app = newApp(settings = settings)
13 |
14 | app.addRoute(urls.urlPatterns, "")
15 | app.run()
16 |
--------------------------------------------------------------------------------
/examples/websocket/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../../src")
2 | when not defined(windows):
3 | switch("threads", "on")
4 |
--------------------------------------------------------------------------------
/examples/websocket/readme.md:
--------------------------------------------------------------------------------
1 | In the console of the browser, input the following commands.
2 | ```javascript
3 | ws = new WebSocket("ws://localhost:8080/ws")
4 | ws.send("hello, Prologue!")
5 | ```
--------------------------------------------------------------------------------
/examples/websocket/urls.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import ./views
3 |
4 |
5 | const urlPatterns* = @[
6 | pattern("/ws", hello)
7 | ]
8 |
--------------------------------------------------------------------------------
/examples/websocket/views.nim:
--------------------------------------------------------------------------------
1 | import prologue
2 | import prologue/websocket
3 |
4 |
5 | proc hello*(ctx: Context) {.async.} =
6 | var ws = await newWebSocket(ctx)
7 | await ws.send("Welcome to simple echo server")
8 | while ws.readyState == Open:
9 | let packet = await ws.receiveStrPacket()
10 | await ws.send(packet)
11 |
12 | resp "Hello, Prologue! "
13 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Prologue
2 | site_description: Prologue is an elegant and high performance web framework written in Nim language.
3 |
4 | theme:
5 | name: 'material'
6 |
7 | repo_name: planety/prologue
8 | repo_url: https://github.com/planety/prologue
9 | edit_uri: "https://github.com/planety/prologue/tree/devel/docs"
10 |
11 | nav:
12 | - Introduction: "index.md"
13 | - QuickStart: "quickstart.md"
14 | - Configure: "configure.md"
15 | - Event: "event.md"
16 | - Error Handler: "errorhandler.md"
17 | - Headers: "headers.md"
18 | - Requests: "request.md"
19 | - Response: "response.md"
20 | - Context: "context.md"
21 | - Routing: "routing.md"
22 | - Middleware: "middleware.md"
23 | - Static Files: "staticfiles.md"
24 | - Upload Files: "uploadfile.md"
25 | - Session: "session.md"
26 | - Mocking: "mocking.md"
27 | - Server Settings: "server.md"
28 | - Extend Context: "extendctx.md"
29 | - WebSocket: "websocket.md"
30 | - Command Line Tool: "cli.md"
31 | - Views: "views.md"
32 | - OpenAPI: "openapi.md"
33 | - Validation: "validation.md"
34 | - Deployment: "deployment.md"
35 | - FAQ: "faq.md"
36 |
37 |
38 | markdown_extensions:
39 | - pymdownx.highlight:
40 | anchor_linenums: true
41 | - pymdownx.inlinehilite
42 | - pymdownx.snippets
43 | - pymdownx.superfences
44 | - mkautodoc
45 |
--------------------------------------------------------------------------------
/prologue.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "0.6.6"
4 | author = "ringabout"
5 | description = "Prologue is an elegant and high performance web framework"
6 | license = "Apache-2.0"
7 | srcDir = "src"
8 |
9 |
10 | # Dependencies
11 | requires "nim >= 2.0.0"
12 | requires "regex >= 0.20.0"
13 | requires "nimcrypto >= 0.5.4"
14 | requires "cookiejar >= 0.2.0"
15 | requires "httpx >= 0.3.7"
16 | requires "logue >= 0.2.0"
17 |
18 |
19 | # tests
20 | task tests, "Run all tests":
21 | exec "testament all"
22 |
23 | task tstdbackend, "Test asynchttpserver backend":
24 | exec "nim c -r -d:release -d:usestd tests/server/tserver_application.nim"
25 |
26 | task texamples, "Test examples":
27 | exec "nim c -d:release tests/compile/test_examples/examples.nim"
28 | exec "nim c -d:release -d:usestd tests/compile/test_examples/examples.nim"
29 |
30 | task treadme, "Test Readme":
31 | exec "nim c -d:release tests/compile/test_readme/readme.nim"
32 |
33 | task tcompile, "Test Compile":
34 | exec "nim c -r -d:release tests/compile/test_compile/test_compile.nim"
35 |
36 | task docs, "Only for gh-pages, not for users":
37 | exec "mkdocs build"
38 | exec "mkdocs gh-deploy"
39 |
40 | task apis, "Only for api":
41 | exec "nim doc --verbosity:0 --warnings:off --project --index:on " &
42 | "--git.url:https://github.com/planety/prologue " &
43 | "--git.commit:devel " &
44 | "-o:docs/coreapi " &
45 | "src/prologue/core/application.nim"
46 |
47 | exec "nim buildIndex -o:docs/coreapi/theindex.html docs/coreapi"
48 |
49 | exec "nim doc --verbosity:0 --warnings:off --project --index:on " &
50 | "--git.url:https://github.com/planety/prologue " &
51 | "--git.commit:devel " &
52 | "-o:docs/plugin " &
53 | "src/prologue/plugin.nim"
54 |
55 | exec "nim buildIndex -o:docs/plugin/theindex.html docs/plugin"
56 |
57 | task redis, "Install redis":
58 | exec "nimble install redis@#c02d404 -y"
59 |
60 | task karax, "Install karax":
61 | exec """nimble install karax@">= 1.1.2" -y"""
62 |
63 | task websocketx, "Install websocketx":
64 | exec """nimble install websocketx@">= 0.1.2" -y"""
65 |
66 | task extension, "Install all extensions":
67 | exec "nimble redis"
68 | exec "nimble karax"
69 | exec "nimble websocketx"
70 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # MKDOCS Documentation
2 | mkdocs
3 | mkdocs-material
4 | mkautodoc
--------------------------------------------------------------------------------
/src/prologue.nim:
--------------------------------------------------------------------------------
1 | import ./prologue/core/application
2 |
3 | export application
4 |
--------------------------------------------------------------------------------
/src/prologue/auth.nim:
--------------------------------------------------------------------------------
1 | import ./auth/auth
2 | export auth
--------------------------------------------------------------------------------
/src/prologue/auth/auth.nim:
--------------------------------------------------------------------------------
1 | import std/[strutils, strformat]
2 |
3 | from ../core/context import Context, HandlerAsync
4 | from ../core/response import setHeader, hasHeader
5 | from ../core/encode import base64Decode
6 | import ../core/request
7 | import ../core/httpcore/httplogue
8 |
9 |
10 | type
11 | AuthMethod* = enum
12 | Basic = "Basic"
13 | Digest = "Digest"
14 | VerifyHandler* = proc(ctx: Context, username, password: string): bool {.gcsafe.}
15 |
16 |
17 | proc unauthenticate*(ctx: Context, authMethod: AuthMethod, realm: string,
18 | charset = "UTF-8") {.inline.} =
19 | ctx.response.code = Http401
20 | ctx.response.setHeader("WWW-Authenticate",
21 | fmt"{authMethod} realm={realm}, charset={charset}")
22 |
23 | proc basicAuth*(
24 | ctx: Context, realm: string, verify: VerifyHandler,
25 | charset = "UTF-8"
26 | ): tuple[hasValue: bool, username, password: string] =
27 | result = (false, "", "")
28 | if not ctx.request.hasHeader("Authorization"):
29 | unauthenticate(ctx, Basic, realm, charset)
30 | return
31 |
32 | let
33 | text = ctx.request.headers["Authorization", 0]
34 | authorization = text.split(' ', maxsplit = 1)
35 | authMethod = authorization[0]
36 | authData = authorization[1]
37 |
38 | if authMethod.toLowerAscii != "basic":
39 | unauthenticate(ctx, Basic, realm, charset)
40 | ctx.response.body = "Unsupported Authorization Method"
41 | return
42 |
43 | var decoded: string
44 | try:
45 | decoded = base64Decode(authData)
46 | except ValueError:
47 | ctx.response.code = Http403
48 | ctx.response.body = "Base64 Decode Fails"
49 | return
50 |
51 | let
52 | user = decoded.split(":", maxsplit = 1)
53 | username = user[0]
54 | password = user[1]
55 |
56 | if ctx.verify(username, password):
57 | return (true, username, password)
58 | else:
59 | unauthenticate(ctx, Basic, realm, charset)
60 | ctx.response.body = "Forbidden"
61 | return
62 |
--------------------------------------------------------------------------------
/src/prologue/auth/users.nim:
--------------------------------------------------------------------------------
1 | from ../core/types import SecretKey
2 |
3 |
4 | type
5 | User* = object of RootObj
6 | username: string
7 | password: SecretKey
8 | email: string
9 | firstName, lastName: string
10 |
11 | SuperUser* = object of User
12 |
13 | func initUser*(username: string, password: SecretKey, email, firstName,
14 | lastName = ""): User {.inline.} =
15 | User(username: username, password: password, email: email,
16 | firstName: firstName, lastName: lastName)
17 |
--------------------------------------------------------------------------------
/src/prologue/cache/cache.nim:
--------------------------------------------------------------------------------
1 | import ./lfucache, ./lrucache
2 |
3 | export lfucache, lrucache
4 |
--------------------------------------------------------------------------------
/src/prologue/cache/lfucache.nim:
--------------------------------------------------------------------------------
1 | import std/[tables, options, times]
2 |
3 |
4 | type
5 | ValuePair[B] = object
6 | valuePart: B
7 | hits: int
8 | expire: int # seconds
9 |
10 | LFUCache*[A, B] = object
11 | map: Table[A, ValuePair[B]]
12 | capacity: int
13 | defaultTimeout: int # seconds
14 |
15 |
16 | func capacity*[A, B](cache: LFUCache[A, B]): int {.inline.} =
17 | cache.capacity
18 |
19 | func len*[A, B](cache: LFUCache[A, B]): int {.inline.} =
20 | cache.map.len
21 |
22 | func isEmpty*[A, B](cache: LFUCache[A, B]): bool {.inline.} =
23 | cache.len == 0
24 |
25 | func isFull*[A, B](cache: LFUCache[A, B]): bool {.inline.} =
26 | cache.len == cache.capacity
27 |
28 | func initLFUCache*[A, B](capacity: Natural = 128, defaultTimeout: Natural = 1): LFUCache[A, B] {.inline.} =
29 | LFUCache[A, B](map: initTable[A, ValuePair[B]](), capacity: capacity, defaultTimeout: defaultTimeout)
30 |
31 | proc get*[A, B](cache: var LFUCache[A, B], key: A): Option[B] {.inline.} =
32 | if key in cache.map:
33 | inc cache.map[key].hits
34 | cache.map[key].expire = int(epochTime()) + cache.defaultTimeout
35 | return some(cache.map[key].valuePart)
36 | result = none(B)
37 |
38 | proc getOrDefault*[A, B](cache: var LFUCache[A, B], key: A, default: B): B {.inline.} =
39 | let value = cache.get(key)
40 |
41 | if value.isSome:
42 | result = value.get
43 | else:
44 | result = default
45 |
46 | proc put*[A, B](cache: var LFUCache[A, B], key: A, value: B, timeout: Natural = 1) =
47 | if key in cache.map:
48 | cache.map[key].hits += 1
49 | cache.map[key].valuePart = value
50 | cache.map[key].expire = int(cpuTime()) + timeout
51 | return
52 |
53 | if cache.map.len >= cache.capacity:
54 | var minValue = high(int)
55 | var minkey: B
56 | for key in cache.map.keys:
57 | if cache.map[key].hits < minValue:
58 | minValue = cache.map[key].hits
59 | minkey = key
60 | cache.map.del(minKey)
61 |
62 | var allDelKey: seq[A]
63 | let now = int(cpuTime())
64 | for key, value in cache.map.pairs:
65 | if value.expire >= now:
66 | allDelkey.add(key)
67 |
68 | for key in allDelKey:
69 | cache.map.del(key)
70 |
71 | cache.map[key] = ValuePair[B](valuePart: value, hits: 0, expire: int(epochTime()) + timeout)
72 |
73 | func hasKey*[A, B](cache: var LFUCache[A, B], key: A): bool {.inline.} =
74 | if cache.map.hasKey(key):
75 | result = true
76 |
77 | func contains*[A, B](cache: var LFUCache[A, B], key: A): bool {.inline.} =
78 | cache.hasKey(key)
79 |
80 |
81 | when isMainModule:
82 | import random, timeit, times, os
83 |
84 | randomize(128)
85 |
86 | var s = initLFUCache[int, int](128)
87 | for i in 1 .. 1000:
88 | s.put(rand(1 .. 200), rand(1 .. 126), rand(2 .. 4))
89 | s.put(5, 6, 3)
90 | echo s.get(12)
91 | echo s.get(14).isNone
92 | echo s.get(5)
93 | echo s.len
94 | sleep(5)
95 | echo s.map
96 |
--------------------------------------------------------------------------------
/src/prologue/cache/lrucache.nim:
--------------------------------------------------------------------------------
1 | import std/[tables, lists, options, times]
2 |
3 |
4 | type
5 | KeyPair[A, B] = tuple
6 | keyPart: A
7 | valuePart: B
8 | expire: int # seconds
9 |
10 | ListPair*[A, B] = DoublyLinkedList[KeyPair[A, B]]
11 | MapValue*[A, B] = DoublyLinkedNode[KeyPair[A, B]]
12 |
13 | LRUCache*[A, B] = object
14 | map: Table[A, MapValue[A, B]]
15 | list: ListPair[A, B]
16 | capacity: int
17 | defaultTimeout: int # seconds
18 |
19 |
20 | func capacity*[A, B](cache: LRUCache[A, B]): int {.inline.} =
21 | cache.capacity
22 |
23 | func len*[A, B](cache: LRUCache[A, B]): int {.inline.} =
24 | cache.map.len
25 |
26 | func isEmpty*[A, B](cache: LRUCache[A, B]): bool {.inline.} =
27 | cache.len == 0
28 |
29 | func isFull*[A, B](cache: LRUCache[A, B]): bool {.inline.} =
30 | cache.len == cache.capacity
31 |
32 | func initLRUCache*[A, B](capacity: Natural = 128, defaultTimeout: Natural = 1): LRUCache[A, B] {.inline.} =
33 | LRUCache[A, B](map: initTable[A, MapValue[A, B]](),
34 | list: initDoublyLinkedList[KeyPair[A, B]](),
35 | capacity: capacity,
36 | defaultTimeout: defaultTimeout
37 | )
38 |
39 | proc moveToFront*[A, B](cache: var LRUCache[A, B], node: MapValue[A, B]) {.inline.} =
40 | cache.list.remove(node)
41 | cache.list.prepend(node)
42 |
43 | proc get*[A, B](cache: var LRUCache[A, B], key: A): Option[B] {.inline.} =
44 | if key in cache.map:
45 | var node = cache.map[key]
46 | node.value.expire = int(epochTime()) + cache.defaultTimeout
47 | moveToFront(cache, node)
48 | return some(node.value.valuePart)
49 | result = none(B)
50 |
51 | proc getOrDefault*[A, B](cache: var LRUCache[A, B], key: A, default: B): B {.inline.} =
52 | let value = cache.get(key)
53 |
54 | if value.isSome:
55 | result = value.get
56 | else:
57 | result = default
58 |
59 | proc put*[A, B](cache: var LRUCache[A, B], key: A, value: B, timeout: Natural = 1) =
60 | if key in cache.map:
61 | var node = cache.map[key]
62 | node.value.valuePart = value
63 | node.value.expire = int(epochTime()) + timeout
64 | moveToFront(cache, node)
65 | return
66 |
67 | if cache.map.len >= cache.capacity:
68 | let node = cache.list.tail
69 | cache.list.remove(node)
70 | cache.map.del(node.value.keyPart)
71 |
72 | let now = int(epochTime())
73 | for cnode in nodes(cache.list):
74 | if now > cnode.value.expire:
75 | cache.list.remove(cnode)
76 | cache.map.del(cnode.value.keyPart)
77 |
78 | let node = newDoublyLinkedNode((keyPart: key, valuePart: value, expire: int(epochTime()) + timeout))
79 | cache.map[key] = node
80 | moveToFront(cache, node)
81 |
82 | proc `[]`*[A, B](cache: var LRUCache[A, B], key: A): B {.inline.} =
83 | cache.get(key)
84 |
85 | func hasKey*[A, B](cache: var LRUCache[A, B], key: A): bool {.inline.} =
86 | if cache.map.hasKey(key):
87 | result = true
88 |
89 | func contains*[A, B](cache: var LRUCache[A, B], key: A): bool {.inline.} =
90 | cache.hasKey(key)
91 |
92 |
93 | when isMainModule:
94 | import random, timeit, os
95 |
96 | randomize(128)
97 |
98 | timeOnce("list"):
99 | var s = initLRUCache[int, int](64)
100 | for i in 1 .. 100:
101 | s.put(rand(1 .. 64), rand(1 .. 126))
102 |
103 | echo s.list
104 | os.sleep(2000)
105 | s.put(5, 6)
106 | echo s.get(12)
107 | echo s.get(14)
108 | echo s.get(5)
109 | echo s.len
110 | echo s.list
111 |
--------------------------------------------------------------------------------
/src/prologue/core/basicregex.nim:
--------------------------------------------------------------------------------
1 | import pkg/regex
2 |
3 |
4 | export re, Regex, RegexMatch, match, groupNames, groupFirstCapture
--------------------------------------------------------------------------------
/src/prologue/core/beast/server.nim:
--------------------------------------------------------------------------------
1 | import std/[strtabs, json, asyncdispatch]
2 |
3 | from ./request import NativeRequest
4 | from ../nativesettings import Settings, CtxSettings, `[]`
5 | from ../context import Router, ReversedRouter, ReRouter, HandlerAsync,
6 | Event, ErrorHandlerTable, GlobalScope, execEvent
7 |
8 | import pkg/httpx except Settings, Request
9 |
10 |
11 | type
12 | Prologue* = ref object
13 | gScope*: GlobalScope
14 | middlewares*: seq[HandlerAsync]
15 | startup*: seq[Event]
16 | shutdown*: seq[Event]
17 | errorHandlerTable*: ErrorHandlerTable
18 | startupClosure: proc () {.closure, gcsafe.}
19 |
20 | proc execStartupEvent*(app: Prologue) =
21 | proc doStartup() {.gcsafe.} =
22 | for event in app.startup:
23 | execEvent(event)
24 |
25 | app.startupClosure = doStartup
26 |
27 | proc getSettings(app: Prologue): httpx.Settings =
28 | result = httpx.initSettings(app.gScope.settings.port, app.gScope.settings.address,
29 | app.gScope.settings["prologue"].getOrDefault("numThreads").getInt(0),
30 | app.startupClosure, app.gScope.settings.listener)
31 |
32 | proc serve*(app: Prologue,
33 | callback: proc (request: NativeRequest): Future[void] {.closure, gcsafe.},
34 | ) {.inline.} =
35 | ## Serves a new web application.
36 | run(callback, getSettings(app))
37 |
38 | proc serveAsync*(app: Prologue,
39 | callback: proc (request: NativeRequest): Future[void] {.closure, gcsafe.},
40 | ) {.inline, async.} =
41 | ## Serves a new web application.
42 | await runAsync(callback, getSettings(app))
43 |
44 | func newPrologue*(settings: Settings, ctxSettings: CtxSettings, router: Router,
45 | reversedRouter: ReversedRouter, reRouter: ReRouter, middlewares: openArray[HandlerAsync],
46 | startup: openArray[Event], shutdown: openArray[Event],
47 | errorHandlerTable: ErrorHandlerTable, appData: StringTableRef): Prologue {.inline.} =
48 | ## Creates a new application instance.
49 | Prologue(gScope: GlobalScope(settings: settings, ctxSettings: ctxSettings, router: router,
50 | reversedRouter: reversedRouter, reRouter: reRouter, appData: appData),
51 | middlewares: @middlewares, startup: @startup, shutdown: @shutdown,
52 | errorHandlerTable: errorHandlerTable)
53 |
--------------------------------------------------------------------------------
/src/prologue/core/constants.nim:
--------------------------------------------------------------------------------
1 | const
2 | PrologueVersion* = "0.6.6" ## The current version of Prologue.
3 | ProloguePrefix* = "PROLOGUE" ## The helper prefix for environment variables.
4 | useAsyncHTTPServer* = defined(windows) or defined(usestd) ## Uses `asynchttpserver`.
5 |
--------------------------------------------------------------------------------
/src/prologue/core/encode.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import std/[base64, strutils]
15 |
16 |
17 | proc base64Encode*[T: SomeInteger | char](s: openArray[T]): string {.inline.} =
18 | ## The base64 encode.
19 | s.encode
20 |
21 | proc base64Encode*(s: string): string {.inline.} =
22 | ## The base64 encode.
23 | s.encode
24 |
25 | proc base64Decode*(s: string): string {.inline.} =
26 | ## The base64 decode.
27 | s.decode
28 |
29 | proc urlsafeBase64Encode*[T: SomeInteger | char](s: openArray[T]): string {.inline.} =
30 | ## URL-safe and Cookie-safe encoding.
31 | s.encode.replace('+', '-').replace('/', '_')
32 |
33 | proc urlsafeBase64Encode*(s: string): string {.inline.} =
34 | ## URL-safe and Cookie-safe encoding.
35 | s.encode.replace('+', '-').replace('/', '_')
36 |
37 | proc urlsafeBase64Decode*(s: string): string {.inline.} =
38 | ## URL-safe and Cookie-safe decoding.
39 | s.replace('-', '+').replace('_', '/').decode
40 |
--------------------------------------------------------------------------------
/src/prologue/core/group.nim:
--------------------------------------------------------------------------------
1 | import std/sequtils
2 |
3 | import ./context
4 | import ./server
5 | import ./httpexception
6 |
7 |
8 | type
9 | Group* = ref object ## Grouping object
10 | app*: Prologue
11 | parent {.cursor.}: Group
12 | route: string
13 | middlewares: seq[HandlerAsync]
14 |
15 |
16 | func newGroup*(app: Prologue, route: string, middlewares: openArray[HandlerAsync] = @[],
17 | parent: Group = nil): Group =
18 | ## Creates a new `Group`.
19 | if route.len == 0:
20 | raise newException(RouteError, "Route can't be empty, at least use `/`!")
21 |
22 | if route[0] != '/':
23 | raise newException(RouteError, "Route must start with `/`!")
24 |
25 | if route.len > 1:
26 | if route[^1] == '/':
27 | raise newException(RouteError, "Route can't end with `/` except root directory!")
28 |
29 | Group(app: app, route: route, middlewares: @middlewares, parent: parent)
30 |
31 | func getAllInfos*(group: Group, route: string, middlewares: openArray[HandlerAsync]): (string, seq[HandlerAsync]) =
32 | ## Retrieves group infos regarding middlewares and route.
33 | var parent = group
34 | while parent != nil:
35 | if parent.route.len != 1:
36 | result[0].insert parent.route
37 | result[1].insert parent.middlewares
38 | parent = parent.parent
39 |
40 | result[0].add route
41 | result[1].add middlewares
42 |
--------------------------------------------------------------------------------
/src/prologue/core/httpexception.nim:
--------------------------------------------------------------------------------
1 | type
2 | PrologueError* = object of CatchableError
3 |
4 | HttpError* = object of PrologueError
5 | AbortError* = object of HttpError
6 |
7 | RouteError* = object of PrologueError
8 | RouteResetError* = object of RouteError
9 | DuplicatedRouteError* = object of RouteError
10 | DuplicatedReversedRouteError* = object of RouteError
11 |
--------------------------------------------------------------------------------
/src/prologue/core/middlewaresbase.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | import std/[asyncdispatch]
17 |
18 | from ./context import HandlerAsync, Context, size, incSize, first, `first=`,
19 | middlewares, `middlewares=`, addMiddlewares, newContextFrom,
20 | newContextTo
21 |
22 | from ./route import findHandler
23 |
24 |
25 | proc doNothingClosureMiddleware*(): HandlerAsync
26 |
27 | type
28 | SubContext* = concept ctx
29 | ctx is Context
30 |
31 |
32 | proc switch*(ctx: Context) {.async.} =
33 | ## Switch the control to the next handler.
34 | # TODO make middlewares checked
35 | if ctx.middlewares.len == 0:
36 | let
37 | handler = findHandler(ctx)
38 | next = handler.handler
39 |
40 | ctx.middlewares = handler.middlewares
41 | ctx.addMiddlewares next
42 | ctx.first = false
43 |
44 | incSize(ctx)
45 |
46 | if ctx.size <= ctx.middlewares.len.int8:
47 | let next = ctx.middlewares[ctx.size - 1]
48 | await next(ctx)
49 | elif ctx.first:
50 | let
51 | handler = findHandler(ctx)
52 | lastHandler = handler.handler
53 |
54 | ctx.addMiddlewares handler.middlewares
55 | ctx.addMiddlewares lastHandler
56 | ctx.first = false
57 |
58 | let next = ctx.middlewares[ctx.size - 1]
59 | await next(ctx)
60 |
61 | proc doNothingClosureMiddleware*(): HandlerAsync =
62 | ## Don't do anything, just for placeholder.
63 | result = proc(ctx: Context) {.async.} =
64 | await switch(ctx)
65 |
66 | import std/logging
67 |
68 | template handleCtxError(ctx: SubContext) =
69 | try:
70 | await switch(ctx)
71 | except RouteError as e:
72 | ctx.response.code = Http404
73 | ctx.response.body.setLen(0)
74 | logging.debug e.msg
75 | except HttpError as e:
76 | # catch general http error
77 | logging.debug e.msg
78 | except AbortError as e:
79 | # catch abort error
80 | logging.debug e.msg
81 | except Exception as e:
82 | logging.error e.msg
83 | ctx.response.code = Http500
84 | ctx.response.body = e.msg
85 | ctx.response.setHeader("content-type", "text/plain; charset=UTF-8")
86 |
87 | proc extendContextMiddleWare*[T: SubContext](ctxType: typedesc[T]): HandlerAsync {.deprecated.} =
88 | result = proc(ctx: Context) {.async.} =
89 | var userContext = new ctxType
90 | newContextFrom(userContext, ctx)
91 | handleCtxError(userContext)
92 | newContextTo(ctx, userContext)
93 |
--------------------------------------------------------------------------------
/src/prologue/core/naive/server.nim:
--------------------------------------------------------------------------------
1 | import std/[strtabs, json, asyncdispatch]
2 | from std/asynchttpserver import newAsyncHttpServer, serve, close, AsyncHttpServer
3 |
4 | from ./request import NativeRequest
5 | from ../nativesettings import Settings, CtxSettings, `[]`
6 | from ../context import Router, ReversedRouter, ReRouter, HandlerAsync,
7 | Event, ErrorHandlerTable, GlobalScope, execEvent
8 |
9 |
10 | type
11 | Server* = AsyncHttpServer
12 |
13 | Prologue* = ref object
14 | server: Server
15 | gScope*: GlobalScope
16 | middlewares*: seq[HandlerAsync]
17 | startup*: seq[Event]
18 | shutdown*: seq[Event]
19 | errorHandlerTable*: ErrorHandlerTable
20 |
21 |
22 | proc execStartupEvent*(app: Prologue) =
23 | for event in app.startup:
24 | execEvent(event)
25 |
26 | proc serveAsync*(app: Prologue,
27 | callback: proc (request: NativeRequest): Future[void] {.closure, gcsafe.},
28 | ) {.inline, async.} =
29 | ## Serves a new web application.
30 | await app.server.serve(app.gScope.settings.port, callback, app.gScope.settings.address)
31 |
32 | proc serve*(app: Prologue,
33 | callback: proc (request: NativeRequest): Future[void] {.closure, gcsafe.},
34 | ) {.inline.} =
35 | ## Serves a new web application.
36 | waitFor serveAsync(app, callback)
37 |
38 | func newPrologueServer(reuseAddr = true, reusePort = false,
39 | maxBody = 8388608): Server {.inline.} =
40 | newAsyncHttpServer(reuseAddr, reusePort, maxBody)
41 |
42 | func newPrologue*(
43 | settings: Settings, ctxSettings: CtxSettings, router: Router,
44 | reversedRouter: ReversedRouter, reRouter: ReRouter,
45 | middlewares: openArray[HandlerAsync], startup: openArray[Event],
46 | shutdown: openArray[Event], errorHandlerTable: ErrorHandlerTable,
47 | appData: StringTableRef
48 | ): Prologue {.inline.} =
49 | Prologue(server: newPrologueServer(true, settings.reusePort,
50 | settings["prologue"].getOrDefault("maxBody").getInt(8388608)),
51 | gScope: GlobalScope(settings: settings, ctxSettings: ctxSettings, router: router,
52 | reversedRouter: reversedRouter, reRouter: reRouter, appData: appData),
53 | middlewares: @middlewares, startup: @startup, shutdown: @shutdown,
54 | errorHandlerTable: errorHandlerTable)
55 |
--------------------------------------------------------------------------------
/src/prologue/core/pages.nim:
--------------------------------------------------------------------------------
1 | import std/htmlgen
2 | import ./constants
3 |
4 |
5 | func errorPage*(errorMsg: string): string {.inline.} =
6 | ## Error pages for HTTP 404.
7 | result = html(head(title(errorMsg)),
8 | body(h1(errorMsg),
9 | " ",
10 | p("Prologue " & PrologueVersion),
11 | style = "text-align: center;"),
12 | xmlns = "http://www.w3.org/1999/xhtml")
13 |
14 | func loginPage*(): string {.inline.} =
15 | ## Login pages.
16 | result = html(form(action = "/login",
17 | `method` = "post",
18 | "Username: ", input(name = "username", `type` = "text"),
19 | "Password: ", input(name = "password", `type` = "password"),
20 | input(value = "login", `type` = "submit")),
21 | xmlns = "http://www.w3.org/1999/xhtml")
22 |
23 | func multiPartPage*(): string {.inline.} =
24 | ## Multipart pages for uploading files.
25 | result = html(form(action = "/multipart?firstname=red green&lastname=tenth",
26 | `method` = "post", enctype = "multipart/form-data",
27 | input(name = "username", `type` = "text", value = "play game"),
28 | input(name = "password", `type` = "password", value = "start"),
29 | input(value = "submit", `type` = "submit")),
30 | xmlns = "http://www.w3.org/1999/xhtml")
31 |
32 | func internalServerErrorPage*(): string {.inline.} =
33 | ## Internal server error pages for HTTP 500.
34 | result = """
35 |
36 |
37 | 500 Internal Server Error
38 |
39 |
40 |
41 | 500 Internal Server Error
42 |
43 |
44 | The Server encountered an internal error and unable to complete
45 | you request.
46 |
47 |
48 |
49 |
50 |
51 | """
52 |
--------------------------------------------------------------------------------
/src/prologue/core/request.nim:
--------------------------------------------------------------------------------
1 | import std/httpcore
2 | import ./constants
3 |
4 |
5 | when useAsyncHTTPServer:
6 | import ./naive/request
7 | else:
8 | import ./beast/request
9 |
10 | export request
11 |
12 |
13 | func hasHeader*(request: Request, key: string): bool {.inline.} =
14 | ## Returns true if key is in `request.headers`.
15 | request.headers.hasKey(key)
16 |
17 | func getHeader*(request: Request, key: string): seq[string] {.inline.} =
18 | ## Retrieves value of `request.headers[key]`.
19 | seq[string](request.headers[key])
20 |
21 | func getHeaderOrDefault*(request: Request, key: string, default = @[""]): seq[string] {.inline.} =
22 | ## Retrieves value of `request.headers[key]`. Otherwise `default` will be returned.
23 | if request.headers.hasKey(key):
24 | result = getHeader(request, key)
25 | else:
26 | result = default
27 |
28 | func setHeader*(request: var Request, key, value: string) {.inline.} =
29 | ## Inserts a (key, value) pair into `request.headers`.
30 | request.headers[key] = value
31 |
32 | func setHeader*(request: var Request, key: string, value: seq[string]) {.inline.} =
33 | ## Inserts a (key, value) pair into `request.headers`.
34 | request.headers[key] = value
35 |
36 | func addHeader*(request: var Request, key, value: string) {.inline.} =
37 | ## Appends value to the existing key in `request.headers`.
38 | request.headers.add(key, value)
39 |
--------------------------------------------------------------------------------
/src/prologue/core/server.nim:
--------------------------------------------------------------------------------
1 | from std/nativesockets import Port
2 | import std/json
3 |
4 | import ./constants, ./nativesettings
5 |
6 | when useAsyncHTTPServer:
7 | import ./naive/server
8 | else:
9 | import ./beast/server
10 |
11 | export server
12 |
13 |
14 | func appAddress*(app: Prologue): string {.inline.} =
15 | ## Gets the address from the settings.
16 | app.gScope.settings.address
17 |
18 | func appDebug*(app: Prologue): bool {.inline.} =
19 | ## Gets the debug attributes from the settings.
20 | app.gScope.settings.debug
21 |
22 | func appName*(app: Prologue): string {.inline.} =
23 | ## Gets the appName attributes from the settings.
24 | app.gScope.settings.getOrDefault("appName").getStr("")
25 |
26 | func appPort*(app: Prologue): Port {.inline.} =
27 | ## Gets the port from the settings.
28 | app.gScope.settings.port
29 |
--------------------------------------------------------------------------------
/src/prologue/core/uid.nim:
--------------------------------------------------------------------------------
1 | import std/monotimes
2 |
3 | import ./urandom, ./utils
4 | from ./encode import urlsafeBase64Encode
5 |
6 |
7 | proc genUid*(): string =
8 | ## Generates a simple user id.
9 | # TODO ADD Mac/IP address
10 | let tseq = serialize(getMonoTime().ticks)
11 | let rseq = randomBytesSeq[8]()
12 | var res: array[16, byte]
13 | res[0 ..< 8] = tseq
14 | res[8 .. ^1] = rseq
15 | result = urlsafeBase64Encode(res)
16 |
--------------------------------------------------------------------------------
/src/prologue/core/urandom.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | from std/sysrand import urandom
15 |
16 | from ./types import SecretKey
17 | from ./utils import fromByteSeq
18 |
19 |
20 | const
21 | DefaultEntropy* = 32 ## The default length of random string.
22 |
23 |
24 | proc randomBytesSeq*(size = DefaultEntropy): seq[byte] {.inline.} =
25 | ## Generates a new system random sequence of bytes.
26 | result = newSeq[byte](size)
27 | discard urandom(result)
28 |
29 | proc randomBytesSeq*[size: static[int]](): array[size, byte] {.inline.} =
30 | ## Generates a new system random sequence of bytes.
31 | discard urandom(result)
32 |
33 | proc randomString*(size = DefaultEntropy): string {.inline.} =
34 | ## Generates a new system random strings.
35 | result = size.randomBytesSeq.fromByteSeq
36 |
37 | proc randomSecretKey*(size = DefaultEntropy): SecretKey {.inline.} =
38 | ## Generates a new system random secretKey.
39 | result = SecretKey(randomString(size))
40 |
41 |
42 | when isMainModule:
43 | for i in 1 .. 50:
44 | echo randomString(i).len
45 |
--------------------------------------------------------------------------------
/src/prologue/core/utils.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | template since*(version, body: untyped) {.dirty.} =
16 | ## limitation: can't be used to annotate a template (eg typetraits.get), would
17 | ## error: cannot attach a custom pragma.
18 | when (NimMajor, NimMinor) >= version:
19 | body
20 |
21 | template sinceAPI*(version, body: untyped) {.dirty.} =
22 | ## limitation: can't be used to annotate a template (eg typetraits.get), would
23 | ## error: cannot attach a custom pragma.
24 | when (NimMajor, NimMinor) >= version:
25 | body
26 |
27 | template beforeAPI*(version, body: untyped) {.dirty.} =
28 | ## limitation: can't be used to annotate a template (eg typetraits.get), would
29 | ## error: cannot attach a custom pragma.
30 | when (NimMajor, NimMinor) <= version:
31 | body
32 |
33 | func toByteSeq*(str: string): seq[byte] {.inline.} =
34 | ## Converts a string to the corresponding byte sequence.
35 | @(str.toOpenArrayByte(0, str.high))
36 |
37 | func fromByteSeq*(sequence: openArray[byte]): string {.inline.} =
38 | ## Converts a byte sequence to the corresponding string.
39 | let length = sequence.len
40 | if length > 0:
41 | result = newString(length)
42 | copyMem(result.cstring, sequence[0].unsafeAddr, length)
43 |
44 | template castNumber(result, number: typed): untyped =
45 | ## Casts ``number`` to array[byte] in system endianness order.
46 | cast[typeof(result)](number)
47 |
48 | func serialize*(number: int64): array[8, byte] {.inline.} =
49 | ## Serializes int64 to byte array.
50 | result = castNumber(result, number)
51 |
52 | func serialize*(number: int32): array[4, byte] {.inline.} =
53 | ## Serializes int32 to byte array.
54 | result = castNumber(result, number)
55 |
56 | func serialize*(number: int16): array[2, byte] {.inline.} =
57 | ## Serializes int16 to byte array.
58 | # result[0] = byte(number shr 8'u16)
59 | # result[1] = byte(number)
60 | result = castNumber(result, number)
61 |
62 | func escape*(src: string): string =
63 | result = newStringOfCap(src.len)
64 | for c in src:
65 | case c
66 | of '&': result.add("&")
67 | of '<': result.add("<")
68 | of '>': result.add(">")
69 | of '"': result.add(""")
70 | of '\'': result.add("'")
71 | of '/': result.add("/")
72 | else: result.add(c)
73 |
--------------------------------------------------------------------------------
/src/prologue/i18n.nim:
--------------------------------------------------------------------------------
1 | import ./i18n/i18n
2 | export i18n
--------------------------------------------------------------------------------
/src/prologue/i18n/i18n.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
16 | import std/[parsecfg, tables, strtabs, streams]
17 |
18 | from ../core/application import Prologue
19 | from ../core/context import Context, gScope
20 |
21 |
22 | type
23 | Translator* = object
24 | language*: string
25 | ctx*: Context
26 |
27 |
28 | proc loadTranslate*(
29 | stream: Stream,
30 | filename = "[stream]"
31 | ): TableRef[string, StringTableRef] =
32 | var
33 | currentSection = ""
34 | p: CfgParser
35 |
36 | result = newTable[string, StringTableRef]()
37 | open(p, stream, filename)
38 |
39 | while true:
40 | var e = p.next
41 | case e.kind
42 | of cfgEof:
43 | break
44 | of cfgSectionStart:
45 | currentSection = e.section
46 | of {cfgKeyValuePair, cfgOption}:
47 | var t = newStringTable(mode = modeStyleInsensitive)
48 | if result.hasKey(currentSection):
49 | t = result[currentSection]
50 | t[e.key] = e.value
51 | result[currentSection] = t
52 | of cfgError:
53 | break
54 | p.close()
55 |
56 | proc loadTranslate*(filename: string): TableRef[string, StringTableRef] {.inline.} =
57 | let
58 | file = open(filename, fmRead)
59 | fileStream = newFileStream(file)
60 | result = fileStream.loadTranslate(filename)
61 | fileStream.close()
62 |
63 | proc loadTranslate*(app: Prologue, filename: string) {.inline.} =
64 | let res = loadTranslate(filename)
65 | app.gScope.ctxSettings.config = res
66 |
67 | proc setLanguage*(ctx: Context, language: string): Translator {.inline.} =
68 | Translator(ctx: ctx, language: language)
69 |
70 | proc translate*(t: Translator, text: string): string {.inline.} =
71 | let config = t.ctx.gScope.ctxSettings.config
72 | if not config.hasKey(text):
73 | return text
74 | let trans = config[text]
75 | if not trans.hasKey(t.language):
76 | return text
77 | return trans[t.language]
78 |
79 | proc Tr*(t: Translator, text: string): string {.inline.} =
80 | t.translate(text)
81 |
82 | proc translate*(ctx: Context, text: string, language: string): string {.inline.} =
83 | ## Translates text by `language`.
84 | let config = ctx.gScope.ctxSettings.config
85 | if not config.hasKey(text):
86 | return text
87 | let trans = config[text]
88 | if not trans.hasKey(language):
89 | return text
90 | return trans[language]
91 |
92 | proc Tr*(ctx: Context, text: string, language: string): string {.inline.} =
93 | ## Helper function for ``translate``.
94 | ctx.translate(text, language)
95 |
--------------------------------------------------------------------------------
/src/prologue/middlewares.nim:
--------------------------------------------------------------------------------
1 | import ./middlewares/middlewares
2 | export middlewares
--------------------------------------------------------------------------------
/src/prologue/middlewares/auth.nim:
--------------------------------------------------------------------------------
1 | import std/[asyncdispatch, strtabs]
2 |
3 | from ../auth/auth import basicAuth, VerifyHandler
4 | from ../core/context import HandlerAsync, Context
5 | from ../core/middlewaresbase import switch
6 |
7 |
8 | proc basicAuthMiddleware*(realm: string, verifyHandler: VerifyHandler,
9 | charset = "UTF-8"): HandlerAsync =
10 | result = proc(ctx: Context) {.async.} =
11 | let (hasValue, username, password) = basicAuth(ctx, realm,
12 | verifyHandler, charset)
13 | if not hasValue:
14 | return
15 |
16 | ctx.ctxData["basic_auth_username"] = username
17 | ctx.ctxData["basic_auth_password"] = password
18 | await switch(ctx)
19 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/clickjacking.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import std/[json, strutils, asyncdispatch]
16 |
17 | from ../core/response import setHeader
18 | from ../core/context import Context, HandlerAsync, getSettings
19 | from ../core/middlewaresbase import switch
20 |
21 |
22 | proc clickjackingMiddleWare*(): HandlerAsync =
23 | result = proc(ctx: Context) {.async.} =
24 | await switch(ctx)
25 | var option = ctx.getSettings("prologue").getOrDefault("X-Frame-Options").getStr.toLowerAscii
26 |
27 | if option != "deny" and option != "sameorigin":
28 | option = "deny"
29 | ctx.response.setHeader("X-Frame-Options", option)
30 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/memorysession.nim:
--------------------------------------------------------------------------------
1 | import ./sessions/memorysession
2 |
3 | export memorysession
4 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/middlewares.nim:
--------------------------------------------------------------------------------
1 | import ./utils, ./cors, ./clickjacking, ./csrf, ./auth
2 |
3 | export utils, cors, clickjacking, csrf, auth
4 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/redissession.nim:
--------------------------------------------------------------------------------
1 | import ./sessions/redissession
2 |
3 | export redissession
4 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/sessions/memorysession.nim:
--------------------------------------------------------------------------------
1 | import std/[options, strtabs, tables, asyncdispatch]
2 |
3 | from ../../core/types import BadSecretKeyError, SecretKey, len, Session, newSession
4 | from ../../core/context import Context, HandlerAsync, getCookie, setCookie,
5 | deleteCookie
6 | from ../../core/urandom import randomBytesSeq
7 | from ../../core/response import addHeader
8 | from ../../core/middlewaresbase import switch
9 | from ../../core/nativesettings import Settings
10 | from ../../core/encode import urlsafeBase64Encode
11 |
12 | from pkg/cookiejar import SameSite
13 |
14 | export cookiejar
15 |
16 |
17 | proc sessionMiddleware*(
18 | settings: Settings,
19 | sessionName = "session",
20 | maxAge: int = 14 * 24 * 60 * 60, # 14 days, in seconds
21 | path = "",
22 | domain = "",
23 | sameSite = Lax,
24 | httpOnly = false,
25 | secure = false
26 | ): HandlerAsync =
27 |
28 | var memorySessionTable = newTable[string, Session]()
29 |
30 | result = proc(ctx: Context) {.async.} =
31 | var
32 | data = ctx.getCookie(sessionName)
33 |
34 | if data.len != 0 and memorySessionTable.hasKey(data):
35 | ctx.session = memorySessionTable[data]
36 | else:
37 | ctx.session = newSession(data = newStringTable(modeCaseSensitive))
38 |
39 | data = urlsafeBase64Encode(randomBytesSeq(16))
40 | ctx.setCookie(sessionName, data,
41 | maxAge = some(maxAge), path = path, domain = domain,
42 | sameSite = sameSite, httpOnly = httpOnly, secure = secure)
43 | memorySessionTable[data] = ctx.session
44 |
45 | await switch(ctx)
46 |
47 | if ctx.session.len == 0: # empty or modified(del or clear)
48 | if ctx.session.modified: # modified
49 | memorySessionTable.del(data)
50 | ctx.deleteCookie(sessionName, domain = domain,
51 | path = path) # delete session data in cookie
52 | return
53 |
54 | if ctx.session.accessed:
55 | ctx.response.addHeader("vary", "Cookie")
56 |
57 | if ctx.session.modified:
58 | memorySessionTable[data] = ctx.session
59 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/sessions/redissession.nim:
--------------------------------------------------------------------------------
1 | import std/[options, strtabs, asyncdispatch]
2 |
3 | from ../../core/types import BadSecretKeyError, SecretKey, len, Session, newSession, pairs
4 | from ../../core/context import Context, HandlerAsync, getCookie, setCookie, deleteCookie
5 | from ../../core/response import addHeader
6 | from ../../core/middlewaresbase import switch
7 | from ../../core/uid import genUid
8 | from ../../core/nativesettings import Settings
9 |
10 | from pkg/cookiejar import SameSite
11 |
12 |
13 | when (compiles do: import redis):
14 | import pkg/redis
15 | else:
16 | {.error: "Please use `logue extension redis` to install!".}
17 |
18 | export cookiejar
19 |
20 |
21 | func divide(info: RedisList): StringTableRef =
22 | result = newStringTable(modeCaseSensitive)
23 | for idx in countup(0, info.high, 2):
24 | result[info[idx]] = info[idx + 1]
25 |
26 | proc sessionMiddleware*(
27 | settings: Settings,
28 | sessionName = "session",
29 | maxAge: int = 14 * 24 * 60 * 60, # 14 days, in seconds
30 | path = "",
31 | domain = "",
32 | sameSite = Lax,
33 | httpOnly = false,
34 | secure = false
35 | ): HandlerAsync =
36 |
37 | var redisClient = waitFor openAsync()
38 |
39 | result = proc(ctx: Context) {.async.} =
40 | var
41 | data = ctx.getCookie(sessionName)
42 |
43 | if data.len != 0:
44 | {.gcsafe.}:
45 | let info = await redisClient.hGetAll(data)
46 |
47 | if info.len != 0:
48 | ctx.session = newSession(data = divide(info))
49 | else:
50 | ctx.session = newSession(data = newStringTable(modeCaseSensitive))
51 | else:
52 | ctx.session = newSession(data = newStringTable(modeCaseSensitive))
53 |
54 | data = genUid()
55 | ctx.setCookie(sessionName, data,
56 | maxAge = some(maxAge), path = path, domain = domain,
57 | sameSite = sameSite, httpOnly = httpOnly, secure = secure)
58 |
59 | await switch(ctx)
60 |
61 | if ctx.session.len == 0: # empty or modified(del or clear)
62 | if ctx.session.modified: # modified
63 | {.gcsafe.}:
64 | discard await redisClient.del(@[data])
65 | ctx.deleteCookie(sessionName, domain = domain,
66 | path = path) # delete session data in cookie
67 | return
68 |
69 | if ctx.session.accessed:
70 | ctx.response.addHeader("vary", "Cookie")
71 |
72 | if ctx.session.modified:
73 | let length = ctx.session.len
74 | var temp = newSeqOfCap[(string, string)](length)
75 | for (key, val) in ctx.session.pairs:
76 | temp.add (key, val)
77 | {.gcsafe.}:
78 | await redisClient.hMset(data, temp)
79 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/sessions/sessionsbase.nim:
--------------------------------------------------------------------------------
1 | type
2 | Backend* = enum
3 | SignedCookie, File, DataBase, Redis
4 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/sessions/signedcookiesession.nim:
--------------------------------------------------------------------------------
1 | import std/[options, strtabs, asyncdispatch, json]
2 |
3 |
4 | from ../../core/types import BadSecretKeyError, SecretKey, loads, dumps, len, newSession
5 | from ../../core/context import Context, HandlerAsync, getCookie, setCookie,
6 | deleteCookie
7 | from ../../core/response import addHeader
8 | from ../../signing/signing import DefaultSep, DefaultKeyDerivation,
9 | BadTimeSignatureError, SignatureExpiredError, DefaultDigestMethodType,
10 | initTimedSigner, unsign, sign
11 | from ../../core/middlewaresbase import switch
12 | from ../../core/urandom import randomString
13 | from ../../core/nativesettings import Settings, `[]`
14 |
15 | from pkg/cookiejar import SameSite
16 |
17 | export cookiejar
18 |
19 |
20 | proc sessionMiddleware*(
21 | settings: Settings,
22 | sessionName = "session",
23 | maxAge: int = 14 * 24 * 60 * 60, # 14 days, in seconds
24 | path = "",
25 | domain = "",
26 | sameSite = Lax,
27 | httpOnly = false,
28 | secure = false
29 | ): HandlerAsync =
30 |
31 | var secretKey = settings["prologue"].getOrDefault("secretKey").getStr
32 | if secretKey.len == 0:
33 | secretKey = randomString(16)
34 |
35 | let
36 | salt = "prologue.signedcookiesession"
37 | sep = DefaultSep
38 | keyDerivation = DefaultKeyDerivation
39 | digestMethodType = DefaultDigestMethodType
40 | signer = initTimedSigner(SecretKey(secretKey), salt, sep, keyDerivation, digestMethodType)
41 |
42 | result = proc(ctx: Context) {.async.} =
43 | ctx.session = newSession(data = newStringTable(modeCaseSensitive))
44 | let
45 | data = ctx.getCookie(sessionName)
46 |
47 | if data.len != 0:
48 | try:
49 | ctx.session.loads(signer.unsign(data, maxAge))
50 | except BadTimeSignatureError, SignatureExpiredError, ValueError, IndexDefect:
51 | ctx.deleteCookie(sessionName, domain = domain,
52 | path = path) # delete session data in cookie
53 | except Exception as e:
54 | ctx.deleteCookie(sessionName, domain = domain,
55 | path = path) # delete session data in cookie
56 | raise e
57 |
58 | await switch(ctx)
59 |
60 | if ctx.session.len == 0: # empty or modified(del or clear)
61 | if ctx.session.modified: # modified
62 | ctx.deleteCookie(sessionName, domain = domain,
63 | path = path) # delete session data in cookie
64 | return
65 |
66 | if ctx.session.accessed:
67 | ctx.response.addHeader("vary", "Cookie")
68 |
69 | # TODO add refresh every request[in permanent session]
70 | if ctx.session.modified:
71 | ctx.setCookie(sessionName, signer.sign(dumps(ctx.session)),
72 | maxAge = some(maxAge), path = path, domain = domain,
73 | sameSite = sameSite, httpOnly = httpOnly, secure = secure)
74 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/sessions/sqlsession.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 blue
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/signedcookiesession.nim:
--------------------------------------------------------------------------------
1 | import ./sessions/signedcookiesession
2 |
3 | export signedcookiesession
4 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/staticfile.nim:
--------------------------------------------------------------------------------
1 | import std/[asyncdispatch, strutils, os, uri]
2 |
3 | import ../core/context, ../core/middlewaresbase, ../core/request
4 | import ./utils
5 |
6 | func normalizedStaticDirs(dirs: openArray[string]): seq[string] =
7 | ## Normalizes the path of static directories.
8 | result = newSeqOfCap[string](dirs.len)
9 | for item in dirs:
10 | let dir = item.strip(chars = {'/'}, trailing = false)
11 | if dir.len != 0:
12 | result.add dir
13 | normalizePath(result[^1])
14 |
15 | proc staticFileMiddleware*(staticDirs: varargs[string]): HandlerAsync =
16 | ## A middleware that serves files from the directories `specified` in `staticDirs`
17 | ## if request.path matches the path to a file in one of the directories in `staticDirs`.
18 | ## The paths in `staticDirs` are interpreted as relative to the binary.
19 | let staticDirs = normalizedStaticDirs(staticDirs)
20 | result = proc(ctx: Context) {.async.} =
21 | let staticFileFlag =
22 | if staticDirs.len != 0:
23 | isStaticFile(ctx.request.path.decodeUrl, staticDirs)
24 | else:
25 | (false, "", "")
26 |
27 | if staticFileFlag.hasValue:
28 | # serve static files
29 | await staticFileResponse(ctx, staticFileFlag.filename,
30 | staticFileFlag.dir,
31 | bufSize = ctx.gScope.settings.bufSize)
32 | else:
33 | await switch(ctx)
34 |
35 | proc redirectTo*(
36 | dest: string, mimetype = "",
37 | downloadName = "", charset = "utf-8"
38 | ): HandlerAsync =
39 | var dest = dest.strip(trailing = false, chars = {'/'})
40 | normalizePath(dest)
41 | let res = splitFile(dest)
42 | let dir = res.dir
43 | let file = res.name & res.ext
44 | result = proc(ctx: Context) {.async.} =
45 | await staticFileResponse(ctx, file, dir, mimetype,
46 | downloadName, charset,
47 | ctx.gScope.settings.bufSize)
48 |
--------------------------------------------------------------------------------
/src/prologue/middlewares/staticfilevirtualpath.nim:
--------------------------------------------------------------------------------
1 | import std/[asyncdispatch, strutils, strformat, os, uri, sugar, logging]
2 |
3 | import ../core/context, ../core/middlewaresbase, ../core/request
4 | import ./utils
5 |
6 |
7 | func normalizeStaticDir(dir: string): string =
8 | ## Normalizes the path of static directory.
9 | result = dir.strip(chars = {'/'}, trailing = false)
10 | normalizePath(result)
11 |
12 | proc staticFileVirtualPathMiddleware*(staticDir: string,
13 | virtualPath: string): HandlerAsync =
14 | # whether request.path in the static path of settings.
15 | let staticDir = normalizeStaticDir(staticDir)
16 | let virtualPath = normalizeStaticDir(virtualPath)
17 | result = proc(ctx: Context) {.async.} =
18 | let virtualRequestFile =
19 | ctx.request.path.decodeUrl.
20 | normalizeStaticDir().
21 | dup(removePrefix(_, virtualPath))
22 | let realFile = staticDir / virtualRequestFile
23 | let staticFileFlag =
24 | if staticDir.len != 0 and virtualPath.len != 0:
25 | isStaticFile(realFile, [staticDir])
26 | else:
27 | (false, "", "")
28 |
29 | if staticFileFlag.hasValue:
30 | logging.debug fmt"Reading virtual path {ctx.request.path.decodeUrl} from real path {realFile}"
31 | # serve static files
32 | await staticFileResponse(ctx, staticFileFlag.filename,
33 | staticFileFlag.dir,
34 | bufSize = ctx.gScope.settings.bufSize)
35 | else:
36 | await switch(ctx)
--------------------------------------------------------------------------------
/src/prologue/mocking.nim:
--------------------------------------------------------------------------------
1 | import ./mocking/mocking
2 | export mocking
--------------------------------------------------------------------------------
/src/prologue/mocking/mocking.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import std/strformat
16 |
17 | import ../core/application
18 |
19 |
20 | proc mockingMiddleware*(): HandlerAsync =
21 | result = proc(ctx: Context) {.async.} =
22 | ctx.handled = true
23 | await switch(ctx)
24 |
25 | proc mockApp*(app: Prologue) {.inline.} =
26 | ## Adds mocking middleware to global middlewares.
27 | app.middlewares.add mockingMiddleware()
28 |
29 | func debugResponse*(ctx: Context) {.inline.} =
30 | debugEcho &"{ctx.response.code} {ctx.response.headers} \n {ctx.response.body}"
31 |
32 | proc runOnce*(app: Prologue, request: Request): Context =
33 | ## Starts an Application.
34 | new result
35 | init(result, request, initResponse(HttpVer11, Http200), app.gScope)
36 | waitFor handleContext(app, result)
37 |
38 | proc runOnce*(app: Prologue, ctx: Context) =
39 | ## Starts an Application.
40 | waitFor handleContext(app, ctx)
41 |
--------------------------------------------------------------------------------
/src/prologue/openapi.nim:
--------------------------------------------------------------------------------
1 | import ./openapi/openapi
2 | export openapi
--------------------------------------------------------------------------------
/src/prologue/openapi/openapi.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import std/[json, strtabs, asyncdispatch]
16 |
17 | from ../core/application import Prologue, addRoute, appDebug
18 | from ../core/response import htmlResponse, resp, jsonResponse
19 | from ../core/context import Context, staticFileResponse, gScope
20 | from ../core/request import setHeader
21 |
22 |
23 | const
24 | swaggerDocs* = """
25 |
26 |
27 |
28 |
29 |
30 | Prologue API - Swagger UI
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
51 |
52 |
53 |
54 | """
55 |
56 | redocs* = """
57 |
58 |
59 | ReDoc
60 |
61 |
62 |
63 |
64 |
65 |
68 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | """
81 |
82 | proc openapiHandler*(ctx: Context) {.async.} =
83 | resp jsonResponse(parseJson(readFile(ctx.gScope.appData["openApiDocsPath"])))
84 |
85 | proc swaggerHandler*(ctx: Context) {.async.} =
86 | resp htmlResponse(swaggerDocs)
87 |
88 | proc redocsHandler*(ctx: Context) {.async.} =
89 | resp htmlResponse(redocs)
90 |
91 | proc serveDocs*(app: Prologue, source: string, onlyDebug = false) {.inline.} =
92 | if onlyDebug and not app.appDebug:
93 | return
94 | app.gScope.appData["openApiDocsPath"] = source
95 | app.addRoute("/openapi.json", openapiHandler)
96 | app.addRoute("/docs", swaggerHandler)
97 | app.addRoute("/redocs", redocsHandler)
98 |
--------------------------------------------------------------------------------
/src/prologue/plog/plog.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 xflywind
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
--------------------------------------------------------------------------------
/src/prologue/plugin.nim:
--------------------------------------------------------------------------------
1 | when not defined(nimdoc):
2 | {.error: """"prologue/plugin" is for documentation purposes only.
3 | Please import the package you need directly. For example:
4 | import prologue/openapi""".}
5 | import ./signing
6 | import ./validater
7 | import ./security
8 | import ./openapi
9 | import ./middlewares
10 | import ./i18n
11 | import ./auth
12 | import ./middlewares/memorysession
13 | import ./middlewares/redissession
14 | import ./middlewares/signedcookiesession
15 |
16 |
17 | export signing, validater, security, openapi, middlewares, i18n, auth, memorysession, redissession, signedcookiesession
18 |
--------------------------------------------------------------------------------
/src/prologue/security.nim:
--------------------------------------------------------------------------------
1 | import ./security/security
2 | export security
--------------------------------------------------------------------------------
/src/prologue/security/hasher.nim:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Zeshen Xing
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | {.deprecated.}
16 |
17 | import std/[strformat, strutils]
18 |
19 | import pkg/nimcrypto/[pbkdf2, sha, sha2]
20 |
21 | from ../core/types import SecretKey
22 | from ../core/encode import base64Encode
23 |
24 |
25 | const
26 | outLen = 64 ## Default length.
27 |
28 |
29 | proc pbkdf2_sha256encode*(password: SecretKey, salt: string,
30 | iterations = 24400): string {.inline.} =
31 | doAssert salt.len != 0 and '$' notin salt
32 | let output = base64Encode(pbkdf2(sha256, string(password), salt, iterations, outLen))
33 | result = fmt"pdkdf2_sha256${salt}${iterations}${output}"
34 |
35 | proc pbkdf2_sha256verify*(password: SecretKey, encoded: string): bool =
36 | let
37 | collections = encoded.split('$', maxSplit = 3)
38 |
39 | if collections.len < 4:
40 | return false
41 |
42 | let
43 | algorithm = collections[0]
44 | salt = collections[1]
45 | iterations = collections[2]
46 | if algorithm != "pdkdf2_sha256":
47 | return false
48 |
49 | return encoded == pbkdf2_sha256encode(password, salt, parseInt(iterations))
50 |
51 | proc pbkdf2_sha1encode*(password: SecretKey, salt: string,
52 | iterations = 24400): string {.inline.} =
53 | doAssert salt.len != 0 and '$' notin salt
54 | let output = base64Encode(pbkdf2(sha1, string(password), salt, iterations, outLen))
55 | result = fmt"pdkdf2_sha1${salt}${iterations}${output}"
56 |
57 | proc pbkdf2_sha1verify*(password: SecretKey, encoded: string): bool =
58 | let
59 | collections = encoded.split('$', maxSplit = 3)
60 |
61 | if collections.len < 4:
62 | return false
63 | let
64 | algorithm = collections[0]
65 | salt = collections[1]
66 | iterations = collections[2]
67 | if algorithm != "pdkdf2_sha1":
68 | return false
69 | return encoded == pbkdf2_sha1encode(password, salt, parseInt(iterations))
70 |
71 |
72 | when isMainModule:
73 | let r1 = pbkdf2_sha256encode(SecretKey("flywind"), "prologue")
74 | doAssert pbkdf2_sha256verify(SecretKey("flywind"), r1)
75 |
76 | let r2 = pbkdf2_sha1encode(SecretKey("flywind"), "prologue")
77 | doAssert pbkdf2_sha1verify(SecretKey("flywind"), r2)
78 |
--------------------------------------------------------------------------------
/src/prologue/security/security.nim:
--------------------------------------------------------------------------------
1 | import ./hasher
2 |
3 | export hasher
4 |
--------------------------------------------------------------------------------
/src/prologue/signing.nim:
--------------------------------------------------------------------------------
1 | import ./signing/signing
2 | export signing
--------------------------------------------------------------------------------
/src/prologue/signing/signingbase.nim:
--------------------------------------------------------------------------------
1 | type
2 | BadDataError* = object of CatchableError
3 | BadSignatureError* = object of BadDataError
4 | BadTimeSignatureError* = object of BadDataError
5 | SignatureExpiredError* = object of BadTimeSignatureError
6 |
--------------------------------------------------------------------------------
/src/prologue/validater.nim:
--------------------------------------------------------------------------------
1 | import ./validater/validater
2 | export validater
3 |
--------------------------------------------------------------------------------
/src/prologue/validater/basic.nim:
--------------------------------------------------------------------------------
1 | import std/[strutils, parseutils]
2 |
3 |
4 | func isInt*(value: string): bool {.inline.} =
5 | if value.len == 0:
6 | return false
7 | var ignoreMe = 0
8 | result = parseInt(value, ignoreMe) == value.len
9 |
10 | func isNumeric*(value: string): bool {.inline.} =
11 | if value.len == 0:
12 | return false
13 | var ignoreMe = 0.0
14 | result = parseFloat(value, ignoreMe) == value.len
15 |
16 | func isBool*(value: string): bool {.inline.} =
17 | result = true
18 | try:
19 | discard parseBool(value)
20 | except ValueError:
21 | result = false
22 |
--------------------------------------------------------------------------------
/src/prologue/websocket.nim:
--------------------------------------------------------------------------------
1 | import ./websocket/websocket
2 | export websocket
--------------------------------------------------------------------------------
/src/prologue/websocket/websocket.nim:
--------------------------------------------------------------------------------
1 | when (compiles do: import websocketx):
2 | import pkg/websocketx
3 | export websocketx
4 | else:
5 | {.error: "Please use `logue extension websocketx` to install!".}
6 |
7 | import ../core/context
8 |
9 | import std/asyncdispatch
10 |
11 |
12 | proc newWebSocket*(ctx: Context): Future[WebSocket] =
13 | ## Creates a new `Websocket`.
14 | result = newWebSocket(ctx.request.nativeRequest)
15 |
--------------------------------------------------------------------------------
/tests/assets/i18n/trans.ini:
--------------------------------------------------------------------------------
1 | [Hello]
2 | zh_CN=你好
3 | ja=こんにちは
4 |
--------------------------------------------------------------------------------
/tests/assets/static/test.txt:
--------------------------------------------------------------------------------
1 | Hello Nim!
--------------------------------------------------------------------------------
/tests/assets/static/upload.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/assets/tassets.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/assets/tassets.nim
--------------------------------------------------------------------------------
/tests/benchmark/b_httpx.nim:
--------------------------------------------------------------------------------
1 | import options, json
2 |
3 | import httpx, asyncdispatch
4 |
5 | proc onRequest(req: Request): Future[void] =
6 | if req.httpMethod == some(HttpGet):
7 | case req.path.get
8 | of "/json":
9 | const data = $(%*{"message": "Hello, World!"})
10 | req.send(Http200, data)
11 | of "/hello":
12 | const data = "Hello, World!"
13 | const headers = "Content-Type: text/plain"
14 | req.send(Http200, data, headers)
15 | else:
16 | req.send(Http404)
17 |
18 | run(onRequest)
--------------------------------------------------------------------------------
/tests/benchmark/b_jester.nim:
--------------------------------------------------------------------------------
1 | import jester
2 |
3 | settings:
4 | port = Port(8080)
5 |
6 | routes:
7 | get "/hello":
8 | resp "Hello, jester!"
9 |
--------------------------------------------------------------------------------
/tests/benchmark/b_logue.nim:
--------------------------------------------------------------------------------
1 | # app.nim
2 | import ../../src/prologue
3 |
4 | proc hello*(ctx: Context) {.async.} =
5 | resp "Hello, Prologue! "
6 |
7 | var app = newApp(settings = newSettings(debug = false))
8 | app.addRoute("/hello", hello)
9 | app.run()
10 |
--------------------------------------------------------------------------------
/tests/benchmark/b_stdlib.nim:
--------------------------------------------------------------------------------
1 | import asynchttpserver, asyncdispatch, json
2 |
3 | var server = newAsyncHttpServer()
4 |
5 |
6 | proc onRequest(req: Request): Future[void] {.async, gcsafe.} =
7 | if req.reqMethod == HttpGet:
8 | case req.url.path
9 | of "/json":
10 | const data = $(%*{"message": "Hello, World!"})
11 | await req.respond(Http200, data)
12 | of "/hello":
13 | const data = "Hello, World!"
14 | let headers = newHttpHeaders([("Content-Type","text/plain")])
15 | await req.respond(Http200, data, headers)
16 | else:
17 | await req.respond(Http404, "")
18 |
19 | waitFor server.serve(Port(8080), onRequest)
20 | # 13000
--------------------------------------------------------------------------------
/tests/benchmark/readme.md:
--------------------------------------------------------------------------------
1 | ## Compilation options
2 |
3 | ```bash
4 | nim c -r --d:release --threads:on b_*.nim
5 | ```
6 |
7 | ## Benchmark
8 | Benchmark with `wrk`:
9 |
10 | ```bash
11 | curl http://localhost:8080/hello && echo "\n" && ./wrk -t15 -c250 -d5s http://localhost:8080/hello
12 | ```
13 |
--------------------------------------------------------------------------------
/tests/benchmark/tbenchmark.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/benchmark/tbenchmark.nim
--------------------------------------------------------------------------------
/tests/compile/test_compile/test_compile.nim:
--------------------------------------------------------------------------------
1 | import std/[os, osproc, strformat]
2 |
3 |
4 | # Test Examples
5 | block:
6 | let
7 | todoappDir = "./examples/todoapp"
8 | app = "app.nim"
9 | execCommand = "nim c --d:release"
10 |
11 | # app can compile
12 | block:
13 | let (outp, errC) = execCmdEx(fmt"{execCommand} {todoappDir / app}")
14 | doAssert errC == 0, outp
15 |
--------------------------------------------------------------------------------
/tests/compile/test_examples/examples.nim:
--------------------------------------------------------------------------------
1 | import ../../../examples/helloworld/app as a1
2 | import ../../../examples/todolist/app as a2
3 | import ../../../examples/todoapp/app as a3
4 | import ../../../examples/blog/app as a4
5 | import ../../../examples/basic/app as a5
6 | import ../../../examples/websocket/app as a6
7 | import ../../../examples/csrf/app as a7
8 | import ../../../examples/memorysession/app as a8
9 | import ../../../examples/redissession/app as a9
10 | import ../../../examples/signedcookiesession/app as a10
11 | import ../test_readme/example1 as a11
12 | import ../test_readme/example2 as a12
13 | import ../../local/basic_auth/local_basic_auth_test as a15
14 | import ../../local/staticFile/local_bigFile_test as a16
15 | import ../../local/staticFile/local_download_test as a17
16 | import ../../local/staticFile/local_staticFile_test as a18
17 | import ../../local/uploadFile/local_uploadFile_test as a19
18 | import ../../start_server as a20
19 | import ../../server/tserver_application as a21
20 |
--------------------------------------------------------------------------------
/tests/compile/test_readme/example1.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 |
3 | proc hello*(ctx: Context) {.async.} =
4 | resp "Hello, Prologue! "
5 |
6 | let app = newApp()
7 | app.get("/", hello)
8 | app.run()
9 |
--------------------------------------------------------------------------------
/tests/compile/test_readme/example2.nim:
--------------------------------------------------------------------------------
1 | # app.nim
2 | import ../../../src/prologue
3 | import ../../../src/prologue/middlewares
4 |
5 |
6 | # Async Function
7 | proc home*(ctx: Context) {.async.} =
8 | resp "Home "
9 |
10 | proc helloName*(ctx: Context) {.async.} =
11 | resp "Hello, " & ctx.getPathParams("name", "Prologue") & " "
12 |
13 | proc doRedirect*(ctx: Context) {.async.} =
14 | resp redirect("/hello")
15 |
16 | proc login*(ctx: Context) {.async.} =
17 | resp loginPage()
18 |
19 | proc do_login*(ctx: Context) {.async.} =
20 | resp redirect("/hello/Nim")
21 |
22 |
23 | let settings = newSettings(appName = "Prologue", debug = false)
24 | var app = newApp(settings = settings)
25 | app.use(debugRequestMiddleware())
26 | app.addRoute("/", home, @[HttpGet, HttpPost])
27 | app.addRoute("/home", home, HttpGet)
28 | app.addRoute("/redirect", doRedirect, HttpGet)
29 | app.addRoute("/login", login, HttpGet)
30 | app.addRoute("/login", do_login, HttpPost, middlewares = @[debugRequestMiddleware()])
31 | app.addRoute("/hello/{name}", helloName, HttpGet)
32 | app.run()
33 |
--------------------------------------------------------------------------------
/tests/compile/test_readme/readme.nim:
--------------------------------------------------------------------------------
1 | import ./example1
2 | import ./example2
3 |
--------------------------------------------------------------------------------
/tests/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../src")
2 |
--------------------------------------------------------------------------------
/tests/issue/tissue.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/issue/tissue.nim
--------------------------------------------------------------------------------
/tests/local/basic_auth/local_basic_auth_test.nim:
--------------------------------------------------------------------------------
1 | import std/logging
2 |
3 | import ../../../src/prologue
4 | import ../../../src/prologue/middlewares/auth
5 | import ../../../src/prologue/middlewares/utils
6 |
7 |
8 | proc verify(ctx: Context, username, password: string): bool =
9 | if username == "prologue" and password == "starlight":
10 | result = true
11 | else:
12 | result = false
13 |
14 | proc home(ctx: Context) {.async.} =
15 | debug ctx.ctxData.getOrDefault("basic_auth_username")
16 | debug ctx.ctxData.getOrDefault("basic_auth_password")
17 | resp "You logged in."
18 |
19 |
20 | var app = newApp()
21 | app.addRoute("/home", home, middlewares = @[debugRequestMiddleware(), basicAuthMiddleware(realm = "home", verify)])
22 | app.run()
23 |
--------------------------------------------------------------------------------
/tests/local/extend_config/.config/config.custom.toml:
--------------------------------------------------------------------------------
1 | name = "custom"
2 |
3 | [prologue]
4 | address = ""
5 | port = 8_080
6 | debug = true
7 | reusePort = true
8 | appName = ""
9 | secretKey = "Set by yourself"
10 | bufSize = 40_960
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config/.config/config.debug.toml:
--------------------------------------------------------------------------------
1 | name = "debug"
2 |
3 | [prologue]
4 | address = ""
5 | port = 8_080
6 | debug = true
7 | reusePort = true
8 | appName = ""
9 | secretKey = "Set by yourself"
10 | bufSize = 40_960
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config/.config/config.production.toml:
--------------------------------------------------------------------------------
1 | name = "production"
2 |
3 | [prologue]
4 | address = ""
5 | port = 8_080
6 | debug = false
7 | reusePort = true
8 | appName = ""
9 | secretKey = "Set by yourself"
10 | bufSize = 40_960
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config/.config/config.toml:
--------------------------------------------------------------------------------
1 | name = "default"
2 |
3 | [prologue]
4 | address = ""
5 | port = 8_080
6 | debug = true
7 | reusePort = true
8 | appName = ""
9 | secretKey = "Set by yourself"
10 | bufSize = 40_960
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config/app.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ./utils
3 | import pkg/parsetoml
4 |
5 |
6 | proc toml2json(configPath: string): JsonNode =
7 | result = parsetoml.parseFile(configPath).toRealJson
8 |
9 | proc hello(ctx: Context) {.async.} =
10 | resp "Hello"
11 |
12 | var app = newAppQueryEnv(Toml, toml2json)
13 | app.get("/", hello)
14 | app.run()
15 |
--------------------------------------------------------------------------------
/tests/local/extend_config/utils.nim:
--------------------------------------------------------------------------------
1 | import parsetoml, json, sequtils
2 |
3 |
4 | proc toRealJson*(value: TomlValueRef): JsonNode
5 |
6 | proc toRealJson*(table: TomlTableRef): JsonNode =
7 | result = newJObject()
8 | for key, value in pairs(table):
9 | result[key] = value.toRealJson
10 |
11 | proc toRealJson*(value: TomlValueRef): JsonNode =
12 | case value.kind
13 | of TomlValueKind.Int:
14 | %* value.intVal
15 | of TomlValueKind.Float:
16 | %* value.floatVal
17 | of TomlValueKind.Bool:
18 | %* value.boolVal
19 | of TomlValueKind.Datetime:
20 | %* $value.datetimeVal
21 | of TomlValueKind.Date:
22 | %* $value.dateVal
23 | of TomlValueKind.Time:
24 | %* $value.timeVal
25 | of TomlValueKind.String:
26 | %* value.stringVal
27 | of TomlValueKind.Array:
28 | if value.arrayVal.len == 0:
29 | %* []
30 | else:
31 | %* value.arrayVal.map(toRealJson)
32 | of TomlValueKind.Table:
33 | value.tableVal.toRealJson
34 | of TomlValueKind.None:
35 | %* "ERROR"
36 |
--------------------------------------------------------------------------------
/tests/local/extend_config_yaml/.config/config.custom.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | prologue:
3 | address: ''
4 | port: 8080
5 | debug: true
6 | reusePort: true
7 | appName: ''
8 | secretKey: Set by yourself
9 | bufSize: 40960
10 | name: custom
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config_yaml/.config/config.debug.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | prologue:
3 | address: ''
4 | port: 8080
5 | debug: true
6 | reusePort: true
7 | appName: ''
8 | secretKey: Set by yourself
9 | bufSize: 40960
10 | name: debug
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config_yaml/.config/config.production.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | prologue:
3 | address: ''
4 | port: 8080
5 | debug: false
6 | reusePort: true
7 | appName: ''
8 | secretKey: Set by yourself
9 | bufSize: 40960
10 | name: roduction
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config_yaml/.config/config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | prologue:
3 | address: ''
4 | port: 8080
5 | debug: true
6 | reusePort: true
7 | appName: ''
8 | secretKey: Set by yourself
9 | bufSize: 40960
10 | name: default
11 |
--------------------------------------------------------------------------------
/tests/local/extend_config_yaml/app.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import pkg/yaml
3 |
4 |
5 | proc yaml2json(configPath: string): JsonNode =
6 | result = yaml.loadToJson(readFile(configPath))[0]
7 |
8 | proc hello(ctx: Context) {.async.} =
9 | resp "Hello"
10 |
11 | var app = newAppQueryEnv(Yaml, yaml2json)
12 | app.get("/", hello)
13 | app.run()
14 |
--------------------------------------------------------------------------------
/tests/local/extend_context/tmiddleware_general.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 |
3 | # middleware for general purpose
4 | type
5 | ExperimentContext = concept ctx
6 | ctx is Context
7 | ctx.data is int
8 |
9 | proc init[T: ExperimentContext](ctx: T) =
10 | ctx.data = 12
11 |
12 | proc experimentMiddleware[T: ExperimentContext](ctxType: typedesc[T]): HandlerAsync =
13 | result = proc(ctx: Context) {.async.} =
14 | let ctx = ctxType(ctx)
15 | doAssert ctx.data == 12
16 | inc ctx.data
17 | await switch(ctx)
18 |
19 |
20 | type
21 | UserContext = ref object of Context
22 | data: int
23 |
24 | method extend(ctx: UserContext) {.gcsafe.} =
25 | init(ctx)
26 |
27 | proc hello*(ctx: Context) {.async.} =
28 | let ctx = UserContext(ctx)
29 | assert ctx.data == 13
30 | echo ctx.data
31 | resp "Hello, Prologue! "
32 |
33 | var app = newApp()
34 | app.use(experimentMiddleware(UserContext))
35 | app.get("/", hello)
36 | # app.run(UserContext)
--------------------------------------------------------------------------------
/tests/local/extend_context/tmiddleware_personal.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 |
3 | type
4 | UserContext = ref object of Context
5 | data: int
6 |
7 | proc init(ctx: UserContext) =
8 | ctx.data = 12
9 |
10 | proc experimentMiddleware(): HandlerAsync =
11 | result = proc(ctx: Context) {.async.} =
12 | let ctx = UserContext(ctx)
13 | doAssert ctx.data == 12
14 | inc ctx.data
15 | await switch(ctx)
16 |
17 | method extend(ctx: UserContext) {.gcsafe.} =
18 | init(ctx)
19 |
20 | proc hello*(ctx: Context) {.async.} =
21 | let ctx = UserContext(ctx)
22 | assert ctx.data == 13
23 | echo ctx.data
24 | resp "Hello, Prologue! "
25 |
26 | var app = newApp()
27 | app.use(experimentMiddleware())
28 | app.get("/", hello)
29 | # app.run(UserContext)
--------------------------------------------------------------------------------
/tests/local/extend_context/tsimple.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 |
3 | type
4 | UserContext = ref object of Context
5 | data: int
6 |
7 | # initialize data
8 | method extend(ctx: UserContext) {.gcsafe.} =
9 | ctx.data = 999
10 |
11 | proc hello*(ctx: Context) {.async.} =
12 | let ctx = UserContext(ctx)
13 | doAssert ctx.data == 999
14 | resp "Hello, Prologue! "
15 |
16 | var app = newApp()
17 | app.get("/", hello)
18 | # app.run(UserContext)
--------------------------------------------------------------------------------
/tests/local/staticFile/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello
4 |
5 |
6 | Do something for fun.
7 | This is the hello page.
8 |
9 |
--------------------------------------------------------------------------------
/tests/local/staticFile/local_bigFile_test.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares/staticfile
3 |
4 |
5 | var app = newApp(newSettings(debug = false))
6 |
7 | app.use(staticFileMiddleware("public"))
8 | app.run()
9 |
--------------------------------------------------------------------------------
/tests/local/staticFile/local_download_test.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares
3 |
4 |
5 | proc index(ctx: Context) {.async.} =
6 | resp "Hello, Nim!"
7 |
8 | proc home(ctx: Context) {.async.} =
9 | await ctx.staticFileResponse("hello.html", "", "download.html")
10 |
11 |
12 | let app = newApp()
13 | app.addRoute("/", index)
14 | app.addRoute("/home", home, middlewares = @[debugRequestMiddleware(),
15 | debugResponseMiddleware()])
16 | app.run()
17 |
--------------------------------------------------------------------------------
/tests/local/staticFile/local_staticFile_test.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares
3 |
4 | import std/json
5 |
6 |
7 | proc index(ctx: Context) {.async.} =
8 | resp "Hello, Nim!"
9 |
10 | proc home(ctx: Context) {.async.} =
11 | await ctx.staticFileResponse("hello.html", "")
12 |
13 |
14 | let node = %* {
15 | "prologue": {
16 | "secretKey": "hello, world",
17 | "maxBody": 1000
18 | }
19 | }
20 |
21 | let settings = loadSettings(node)
22 | var app = newApp(settings)
23 | app.addRoute("/", index)
24 | app.addRoute("/home", home, middlewares = @[debugRequestMiddleware(),
25 | debugResponseMiddleware()])
26 | app.run()
27 |
--------------------------------------------------------------------------------
/tests/local/staticFile/local_staticFile_virtualPath_test.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares/staticfilevirtualpath
3 |
4 |
5 | var app = newApp(newSettings(debug = false))
6 |
7 | app.use(staticFileVirtualPathMiddleware("public", "virtual/path/something/public"))
8 | app.run()
9 |
--------------------------------------------------------------------------------
/tests/local/tlocal.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/local/tlocal.nim
--------------------------------------------------------------------------------
/tests/local/uploadFile/local_uploadFile_test.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares
3 | import std/[strformat, json]
4 |
5 |
6 | proc upload(ctx: Context) {.async.} =
7 | if ctx.request.reqMethod == HttpGet:
8 | await ctx.staticFileResponse("tests/local/uploadFile/upload.html", "")
9 | elif ctx.request.reqMethod == HttpPost:
10 | let file = ctx.getUploadFile("file")
11 | file.save("tests/assets/temp")
12 | file.save("tests/assets/temp", "set.txt")
13 | doAssertRaises(OSError):
14 | file.save("not/exists/dir")
15 | resp fmt"{file.filename} {file.body}
"
16 |
17 | let node = %* {
18 | "prologue": {
19 | "secretKey": "hello, world",
20 | "maxBody": 1000
21 | }
22 | }
23 |
24 | let settings = loadSettings(node)
25 | var app = newApp(settings)
26 | app.use([debugRequestMiddleware()])
27 | app.addRoute("/upload", upload, @[HttpGet, HttpPost])
28 | app.run()
29 |
--------------------------------------------------------------------------------
/tests/local/uploadFile/upload.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/mock/tmock_docs/tmock_doc_errorhandler.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/mocking
3 |
4 | import std/uri
5 |
6 |
7 | proc prepareApp(debug = true): Prologue =
8 | result = newApp(settings = newSettings(debug = debug))
9 | mockApp(result)
10 |
11 | proc prepareRequest(path: string, httpMethod = HttpGet): Request =
12 | result = initMockingRequest(
13 | httpMethod = httpMethod,
14 | headers = newHttpHeaders(),
15 | url = parseUri(path),
16 | cookies = initCookieJar(),
17 | postParams = newStringTable(),
18 | queryParams = newStringTable(),
19 | formParams = initFormPart(),
20 | pathParams = newStringTable()
21 | )
22 |
23 | block:
24 | block first:
25 | proc hello(ctx: Context) {.async.} =
26 | ctx.ctxData["test"] = "true"
27 | resp "Something is wrong, please retry.", Http404
28 |
29 | var app = prepareApp()
30 | app.addRoute("/hello", hello)
31 | let ctx = app.runOnce(prepareRequest("/hello"))
32 | doAssert ctx.ctxData["test"] == "true"
33 | doAssert ctx.response.code == Http404
34 | doAssert ctx.response.body == "Something is wrong, please retry."
35 |
36 | block second:
37 | proc hello(ctx: Context) {.async.} =
38 | ctx.ctxData["test"] = "true"
39 | resp error404(headers = ctx.response.headers)
40 |
41 | var app = prepareApp()
42 | app.addRoute("/hello", hello)
43 | let ctx = app.runOnce(prepareRequest("/hello"))
44 | doAssert ctx.ctxData["test"] == "true"
45 | doAssert ctx.response.code == Http404
46 | doAssert ctx.response.body == "404 Not Found! "
47 |
48 | block three:
49 | proc hello(ctx: Context) {.async.} =
50 | ctx.ctxData["test"] = "true"
51 | resp errorPage("Something is wrong"), Http404
52 |
53 | var app = prepareApp()
54 | app.addRoute("/hello", hello)
55 | let ctx = app.runOnce(prepareRequest("/hello"))
56 | doAssert ctx.ctxData["test"] == "true"
57 | doAssert ctx.response.code == Http404
58 | doAssert ctx.response.body == errorPage("Something is wrong")
59 |
60 | block four:
61 | proc hello(ctx: Context) {.async.} =
62 | ctx.ctxData["test"] = "true"
63 | respDefault(Http404)
64 |
65 | let settings = newSettings(appName = "Prologue")
66 | var app = newApp(settings)
67 | mockApp(app)
68 | app.addRoute("/hello", hello)
69 | let ctx = app.runOnce(prepareRequest("/hello"))
70 | doAssert ctx.ctxData["test"] == "true"
71 | doAssert ctx.response.code == Http404
72 | doAssert ctx.response.body == errorPage("404 Not Found!"), ctx.response.body
73 |
74 | block five:
75 | proc hello(ctx: Context) {.async.} =
76 | ctx.ctxData["test"] = "true"
77 | respDefault(Http404)
78 |
79 | var app = newApp(errorHandlerTable = newErrorHandlerTable())
80 | mockApp(app)
81 | app.addRoute("/hello", hello)
82 | let ctx = app.runOnce(prepareRequest("/hello"))
83 | doAssert ctx.ctxData["test"] == "true"
84 | doAssert ctx.response.code == Http404
85 | doAssert ctx.response.body.len == 0
86 |
--------------------------------------------------------------------------------
/tests/mock/tmock_docs/tmock_doc_headers.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/mocking
3 |
4 | import std/uri
5 |
6 | proc prepareApp(debug = true): Prologue =
7 | result = newApp(settings = newSettings(debug = debug))
8 | mockApp(result)
9 |
10 |
11 | proc prepareRequest(path: string, httpMethod = HttpGet): Request =
12 | result = initMockingRequest(
13 | httpMethod = httpMethod,
14 | headers = newHttpHeaders(),
15 | url = parseUri(path),
16 | cookies = initCookieJar(),
17 | postParams = newStringTable(),
18 | queryParams = newStringTable(),
19 | formParams = initFormPart(),
20 | pathParams = newStringTable()
21 | )
22 |
23 |
24 | block first:
25 | proc hello(ctx: Context) {.async.} =
26 | if ctx.request.hasHeader("cookie"):
27 | let values = ctx.request.getHeader("cookie")
28 | resp $values
29 | elif ctx.request.hasHeader("content-type"):
30 | let values = ctx.request.getHeaderOrDefault("content")
31 | resp $values
32 |
33 | block:
34 | var app = prepareApp()
35 | app.addRoute("/hello", hello)
36 | var req = prepareRequest("/hello")
37 | req.addHeader("cookie", "name=prologue&value=nim")
38 | let ctx = app.runOnce(req)
39 | doAssert ctx.response.body == "@[\"name=prologue&value=nim\"]", ctx.response.body
40 |
41 | block:
42 | var app = prepareApp()
43 | app.addRoute("/hello", hello)
44 | var req = prepareRequest("/hello")
45 | req.addHeader("content-type", "text")
46 | let ctx = app.runOnce(req)
47 | doAssert ctx.response.body == "@[\"\"]", ctx.response.body
48 |
49 |
50 | block second:
51 | proc hello(ctx: Context) {.async.} =
52 | ctx.ctxData["test"] = "true"
53 |
54 | ctx.response.addHeader("Content-Type", "text/plain")
55 |
56 | doAssert ctx.response.getHeader("CONTENT-TYPE") == @[
57 | "text/html; charset=UTF-8", "text/plain"]
58 |
59 | ctx.response.setHeader("Content-Type", "text/plain")
60 |
61 | doAssert ctx.response.getHeader("CONTENT-TYPE") == @[
62 | "text/html; charset=UTF-8", "text/plain"]
63 |
64 | var app = prepareApp()
65 | app.addRoute("/hello", hello)
66 | let ctx = app.runOnce(prepareRequest("/hello"))
67 | doAssert ctx.ctxData["test"] == "true"
68 |
--------------------------------------------------------------------------------
/tests/mock/tmock_mocking/tmock_flash.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/middlewares/signedcookiesession
3 | import ../../../src/prologue/mocking
4 |
5 | import std/[with, uri, strutils]
6 |
7 |
8 | proc prepareRequest(path: string, httpMethod = HttpGet, cookies = initCookieJar()): Request =
9 | result = initMockingRequest(
10 | httpMethod = httpMethod,
11 | headers = newHttpHeaders(),
12 | url = parseUri(path),
13 | cookies = cookies,
14 | postParams = newStringTable(),
15 | queryParams = newStringTable(),
16 | formParams = initFormPart(),
17 | pathParams = newStringTable()
18 | )
19 |
20 |
21 | proc hello(ctx: Context) {.async.} =
22 | ctx.flash("Please retry again!")
23 | resp "Hello, world"
24 |
25 | proc tea(ctx: Context) {.async.} =
26 | let msg = ctx.getFlashedMsg(FlashLevel.Info)
27 | if msg.isSome:
28 | resp msg.get
29 | else:
30 | resp "My tea"
31 |
32 | let settings = newSettings()
33 | var app = newApp(settings)
34 |
35 | proc getLastSession(ctx: Context): CookieJar =
36 | result = initCookieJar()
37 | if ctx.response.hasHeader("Set-Cookie"):
38 | let value = ctx.response.headers["Set-Cookie", 0]
39 | result["session"] = value.split(';')[0][8 .. ^1]
40 |
41 |
42 | with app:
43 | mockApp()
44 | use(sessionMiddleware(settings))
45 | get("/", hello)
46 | get("/hello", hello)
47 | get("/tea", tea)
48 |
49 | block:
50 | let ctx1 = app.runOnce(prepareRequest("/"))
51 | doAssert ctx1.response.body == "Hello, world"
52 |
53 | let ctx2 = app.runOnce(prepareRequest("/tea", cookies = getLastSession(ctx1)))
54 | doAssert ctx2.response.body == "Please retry again!"
55 |
56 | let ctx3 = app.runOnce(prepareRequest("/tea", cookies = getLastSession(ctx2)))
57 | doAssert ctx3.response.body == "My tea"
58 |
59 | let ctx4 = app.runOnce(prepareRequest("/tea", cookies = getLastSession(ctx1)))
60 | doAssert ctx4.response.body == "Please retry again!"
61 |
--------------------------------------------------------------------------------
/tests/mock/tmock_mocking/tmock_simple.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 | import ../../../src/prologue/mocking
3 |
4 | import std/uri
5 |
6 |
7 | proc hello*(ctx: Context) {.async.} =
8 | resp "Hello, Prologue! "
9 |
10 |
11 | let settings = newSettings(debug = true)
12 | var app = newApp(settings = settings)
13 | app.addRoute("/", hello)
14 | mockApp(app)
15 |
16 |
17 | let url = parseUri("/")
18 |
19 | let req = initMockingRequest(
20 | httpMethod = HttpGet,
21 | headers = newHttpHeaders(),
22 | url = url,
23 | cookies = initCookieJar(),
24 | postParams = newStringTable(),
25 | queryParams = newStringTable(),
26 | formParams = initFormPart(),
27 | pathParams = newStringTable()
28 | )
29 |
30 | let ctx = runOnce(app, req)
31 | doAssert ctx.response.code == Http200
32 | doAssert ctx.response.getHeader("content-type") == @["text/html; charset=UTF-8"]
33 | doAssert ctx.response.body == "Hello, Prologue! "
34 |
--------------------------------------------------------------------------------
/tests/server/utils.nim:
--------------------------------------------------------------------------------
1 | import std/htmlgen
2 |
3 |
4 | func loginPage*(): string =
5 | return html(form(action = "/login",
6 | `method` = "post",
7 | "Username: ", input(name = "username", `type` = "text"),
8 | "Password: ", input(name = "password", `type` = "password"),
9 | input(value = "login", `type` = "submit")),
10 | xmlns = "http://www.w3.org/1999/xhtml")
11 |
12 | func loginGetPage*(): string =
13 | return html(form(action = "/loginpage",
14 | `method` = "get",
15 | "Username: ", input(name = "username", `type` = "text"),
16 | "Password: ", input(name = "password", `type` = "password"),
17 | input(value = "login", `type` = "submit")),
18 | xmlns = "http://www.w3.org/1999/xhtml")
19 |
--------------------------------------------------------------------------------
/tests/static/A/B/C/important_text.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/static/A/B/C/important_text.txt
--------------------------------------------------------------------------------
/tests/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/static/favicon.ico
--------------------------------------------------------------------------------
/tests/static/tstatic.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/static/tstatic.nim
--------------------------------------------------------------------------------
/tests/unit/tunit_cache/tunit_lfucache.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/cache/lfucache
2 | import std/options
3 |
4 |
5 |
6 | proc putNeverExpired[A, B](cache: var LFUCache[A, B], key: A, value: B) =
7 | cache.put(key, value, 100000)
8 |
9 |
10 | block:
11 | var s = initLFUCache[int, int](64)
12 | doAssert s.isEmpty
13 |
14 | s.putNeverExpired(1, 1)
15 | s.putNeverExpired(2, 2)
16 | s.putNeverExpired(3, 3)
17 |
18 | doAssert s.hasKey(1)
19 | doAssert s.hasKey(2)
20 | doAssert s.hasKey(3)
21 |
22 | doAssert 1 in s
23 | doAssert 2 in s
24 | doAssert 3 in s
25 |
26 | doAssert s.capacity == 64
27 | doAssert s.len == 3
28 | doAssert not s.isEmpty
29 | doAssert not s.isFull
30 |
31 | doAssert s.get(1).get == 1
32 | doAssert s.get(2).get == 2
33 | doAssert s.get(3).get == 3
34 |
35 | doAssert s.getOrDefault(1, 0) == 1
36 | doAssert s.getOrDefault(2, 0) == 2
37 | doAssert s.getOrDefault(3, 0) == 3
38 |
39 | doAssert s.getOrDefault(4, 999) == 999
40 |
--------------------------------------------------------------------------------
/tests/unit/tunit_cache/tunit_lrucache.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/cache/lrucache
2 | import std/options
3 |
4 |
5 | proc putNeverExpired[A, B](cache: var LRUCache[A, B], key: A, value: B) =
6 | cache.put(key, value, 100000)
7 |
8 |
9 | block:
10 | var s = initLRUCache[int, int](64)
11 | doAssert s.isEmpty
12 |
13 | s.putNeverExpired(1, 1)
14 | s.putNeverExpired(2, 2)
15 | s.putNeverExpired(3, 3)
16 |
17 | doAssert s.hasKey(1)
18 | doAssert s.hasKey(2)
19 | doAssert s.hasKey(3)
20 |
21 | doAssert 1 in s
22 | doAssert 2 in s
23 | doAssert 3 in s
24 |
25 | doAssert s.capacity == 64
26 | doAssert s.len == 3
27 | doAssert not s.isEmpty
28 | doAssert not s.isFull
29 |
30 | doAssert s.get(1).get == 1
31 | doAssert s.get(2).get == 2
32 | doAssert s.get(3).get == 3
33 |
34 | doAssert s.getOrDefault(1, 0) == 1
35 | doAssert s.getOrDefault(2, 0) == 2
36 | doAssert s.getOrDefault(3, 0) == 3
37 |
38 | doAssert s.getOrDefault(4, 999) == 999
39 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_application.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue
2 |
3 |
4 | proc hello*(ctx: Context) {.async.} =
5 | resp "Hello, Prologue! "
6 |
7 | proc helloName*(ctx: Context) {.async.} =
8 | resp "Hello, " & ctx.getPathParams("name", "Prologue") & " "
9 |
10 | proc articles*(ctx: Context) {.async.} =
11 | resp $ctx.getPathParams("num", 1)
12 |
13 | proc go404*(ctx: Context) {.async.} =
14 | resp "Something wrong!", Http404
15 |
16 | proc go20x*(ctx: Context) {.async.} =
17 | resp "Ok!", Http200
18 |
19 | proc go30x*(ctx: Context) {.async.} =
20 | resp "EveryThing else?", Http301
21 |
22 |
23 | # "Application Func Test"
24 | block:
25 | # "registErrorHandler can work"
26 | block:
27 | var app = newApp()
28 | app.registerErrorHandler(Http404, go404)
29 | app.registerErrorHandler({Http200 .. Http204}, go20x)
30 | app.registerErrorHandler(@[Http301, Http304, Http307], go30x)
31 |
32 | doAssert app.errorHandlerTable[Http404] == go404
33 | doAssert app.errorHandlerTable[Http202] == go20x
34 | doAssert app.errorHandlerTable[Http304] == go30x
35 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_configure.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/configure
2 |
3 |
4 | import std/os
5 |
6 |
7 | # "Test Config"
8 | block:
9 | let filename = "tests/.env"
10 |
11 | # "can write config"
12 | block:
13 | let
14 | env = initEnv()
15 | env.setPrologueEnv("debug", "true")
16 | env.setPrologueEnv("port", "8080")
17 | env.setPrologueEnv("appName", "Prologue")
18 | env.setPrologueEnv("staticDir", "static")
19 | writePrologueEnv(filename, env)
20 | doAssert fileExists(filename)
21 |
22 | # "can load config"
23 | block:
24 | let env = loadPrologueEnv(filename)
25 | doAssert env["debug"] == "true"
26 | doAssert env["port"] == "8080"
27 | doAssert env["appName"] == "Prologue"
28 | doAssert env["staticDir"] == "static"
29 | doAssert env.getOrDefault("address", "127.0.0.2") == "127.0.0.2"
30 |
31 | if fileExists(filename):
32 | removeFile(filename)
33 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_constants.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/constants
2 |
3 |
4 | doAssert ProloguePrefix == "PROLOGUE"
5 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_encode.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/encode
2 |
3 |
4 | block:
5 | static:
6 | doAssert base64Encode("a") == "YQ=="
7 | doAssert base64Encode("a") == "YQ=="
8 |
9 | block:
10 | doAssert base64Encode("Hello World") == "SGVsbG8gV29ybGQ="
11 | doAssert base64Encode("leasure.") == "bGVhc3VyZS4="
12 | doAssert base64Encode("easure.") == "ZWFzdXJlLg=="
13 | doAssert base64Encode("asure.") == "YXN1cmUu"
14 | doAssert base64Encode("sure.") == "c3VyZS4="
15 | doAssert base64Encode([1,2,3]) == "AQID"
16 | doAssert base64Encode(['h','e','y']) == "aGV5"
17 |
18 | doAssert base64Encode("") == ""
19 | doAssert base64Decode("") == ""
20 |
21 | const testInputExpandsTo76 = "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
22 | const testInputExpands = "++++++++++++++++++++++++++++++"
23 | const longText = """Man is distinguished, not only by his reason, but by this
24 | singular passion from other animals, which is a lust of the mind,
25 | that by a perseverance of delight in the continued and indefatigable
26 | generation of knowledge, exceeds the short vehemence of any carnal
27 | pleasure."""
28 | const tests = ["", "abc", "xyz", "man", "leasure.", "sure.", "easure.",
29 | "asure.", longText, testInputExpandsTo76, testInputExpands]
30 |
31 | doAssert base64Decode("Zm9v\r\nYmFy\r\nYmF6") == "foobarbaz"
32 |
33 | for t in items(tests):
34 | doAssert base64Decode(base64Encode(t)) == t
35 |
36 | const invalid = "SGVsbG\x008gV29ybGQ="
37 | try:
38 | doAssert base64Decode(invalid) == "will throw error"
39 | except ValueError:
40 | discard
41 |
42 | block base64urlSafe:
43 | doAssert urlsafeBase64Encode("c\xf7>") == "Y_c-"
44 | doAssert base64Encode("c\xf7>") == "Y/c+"
45 | doAssert base64Decode("Y/c+") == base64Decode("Y_c-")
46 | # Output must not change with safe=true
47 | doAssert urlsafeBase64Encode("Hello World") == "SGVsbG8gV29ybGQ="
48 | doAssert urlsafeBase64Encode("leasure.") == "bGVhc3VyZS4="
49 | doAssert urlsafeBase64Encode("easure.") == "ZWFzdXJlLg=="
50 | doAssert urlsafeBase64Encode("asure.") == "YXN1cmUu"
51 | doAssert urlsafeBase64Encode("sure.") == "c3VyZS4="
52 | doAssert urlsafeBase64Encode([1,2,3]) == "AQID"
53 | doAssert urlsafeBase64Encode(['h','e','y']) == "aGV5"
54 | doAssert urlsafeBase64Encode("") == ""
55 | doAssert urlsafeBase64Encode("the quick brown dog jumps over the lazy fox") == "dGhlIHF1aWNrIGJyb3duIGRvZyBqdW1wcyBvdmVyIHRoZSBsYXp5IGZveA=="
56 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_form.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/form
2 | import tables, strutils
3 |
4 | block:
5 | const testmime =
6 | "-----------------------------263701891623491983764541468\13\10" &
7 | "Content-Disposition: form-data; name=\"howLongValid\"\13\10" &
8 | "\13\10" &
9 | "3600\13\10" &
10 | "-----------------------------263701891623491983764541468\13\10" &
11 | "Content-Disposition: form-data; name=\"upload\"; filename=\"testfile.txt\"\13\10" &
12 | "Content-Type: text/plain\13\10" &
13 | "\13\10" &
14 | "1234\13\10" &
15 | "5678\13\10" &
16 | "abcd\13\10" &
17 | "-----------------------------263701891623491983764541468--\13\10"
18 | const testfile =
19 | "1234\13\10" &
20 | "5678\13\10" &
21 | "abcd"
22 | const contenttype = "multipart/form-data; boundary=---------------------------263701891623491983764541468"
23 | let formPart = parseFormPart(testmime, contenttype)
24 | doAssert formPart.data["upload"].body.len == testfile.len
25 | doAssert formPart.data["upload"].body == testfile
26 | doAssert parseInt(formPart.data["howLongValid"].body) == 3600
27 |
28 | block:
29 | # check that quoted boundary values work
30 | const testfile =
31 | "data"
32 | const testmime =
33 | "--boundary\13\10" &
34 | "Content-Disposition: form-data; name=\"upload\"\13\10" &
35 | "\13\10" &
36 | testfile &
37 | "\13\10" &
38 | "--boundary--\13\10"
39 | const contenttype = "multipart/form-data; boundary=\"boundary\""
40 | let formPart = parseFormPart(testmime, contenttype)
41 | doAssert formPart.data["upload"].body.len == testfile.len
42 | doAssert formPart.data["upload"].body == testfile
43 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/.config/config.custom.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | },
11 | "name": "custom"
12 | }
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/.config/config.debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | },
11 | "name": "debug"
12 | }
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/.config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": true,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | },
11 | "name": "default"
12 | }
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/.config/config.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "prologue": {
3 | "address": "",
4 | "port": 8080,
5 | "debug": false,
6 | "reusePort": true,
7 | "appName": "",
8 | "secretKey": "Set by yourself",
9 | "bufSize": 40960
10 | },
11 | "name": "production"
12 | }
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/tunit_framework_config.nim:
--------------------------------------------------------------------------------
1 | import ../../../../src/prologue
2 |
3 | import std/[os, json]
4 |
5 |
6 | let cur = getCurrentDir()
7 | setCurrentDir(parentDir(currentSourcePath()))
8 |
9 | block:
10 | block:
11 | delEnv("PROLOGUE")
12 |
13 | block:
14 | putEnv("PROLOGUE", "debug")
15 | var app = newAppQueryEnv()
16 |
17 | doAssert app.gScope.settings.getOrDefault("name").getStr == "debug"
18 |
19 | block:
20 | putEnv("PROLOGUE", "production")
21 | var app = newAppQueryEnv()
22 |
23 | doAssert app.gScope.settings.getOrDefault("name").getStr == "production"
24 |
25 | block:
26 | putEnv("PROLOGUE", "custom")
27 | var app = newAppQueryEnv()
28 |
29 | doAssert app.gScope.settings.getOrDefault("name").getStr == "custom"
30 |
31 | block:
32 | putEnv("PROLOGUE", "default")
33 | var app = newAppQueryEnv()
34 |
35 | doAssert app.gScope.settings.getOrDefault("name").getStr == "default"
36 |
37 | block:
38 | delEnv("PROLOGUE")
39 |
40 |
41 | setCurrentDir(cur)
42 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_framework_config/tunit_loadsettings.nim:
--------------------------------------------------------------------------------
1 | import ../../../../src/prologue
2 |
3 | import std/[os, json]
4 |
5 | let cur = getCurrentDir()
6 | setCurrentDir(parentDir(currentSourcePath()))
7 |
8 |
9 | block:
10 | block:
11 | var app = newApp(loadSettings(".config/config.debug.json"))
12 |
13 | doAssert app.gScope.settings.getOrDefault("name").getStr == "debug"
14 |
15 | block:
16 | var app = newApp(loadSettings(".config/config.json"))
17 |
18 | doAssert app.gScope.settings.getOrDefault("name").getStr == "default"
19 |
20 | block:
21 | var app = newApp(loadSettings(".config/config.custom.json"))
22 |
23 | doAssert app.gScope.settings.getOrDefault("name").getStr == "custom"
24 |
25 | block:
26 | var app = newApp(loadSettings(".config/config.production.json"))
27 |
28 | doAssert app.gScope.settings.getOrDefault("name").getStr == "production"
29 |
30 |
31 | setCurrentDir(cur)
32 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_group.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/application
2 | import ../../../src/prologue/middlewares/middlewares
3 |
4 |
5 | block:
6 | var app = newApp()
7 | doAssertRaises(RouteError):
8 | discard newGroup(app, "")
9 |
10 | discard newGroup(app, "/")
11 |
12 | doAssertRaises(RouteError):
13 | discard newGroup(app, "//")
14 |
15 | doAssertRaises(RouteError):
16 | discard newGroup(app, "x")
17 |
18 | doAssertRaises(RouteError):
19 | discard newGroup(app, "hello")
20 |
21 | doAssertRaises(RouteError):
22 | discard newGroup(app, "/hello/")
23 |
24 | block:
25 | var app = newApp()
26 | var base = newGroup(app, "/apiv2", @[debugRequestMiddleware()])
27 | var level1 = newGroup(app,"/level1", @[debugRequestMiddleware(), debugRequestMiddleware()], base)
28 | var level2 = newGroup(app, "/level2", @[debugRequestMiddleware()], level1)
29 | var level3 = newGroup(app, "/level3", @[debugRequestMiddleware()], level2)
30 |
31 | block:
32 | let (r, m) = getAllInfos(base, "/home", @[debugRequestMiddleware()])
33 | doAssert r == "/apiv2/home"
34 | doAssert m.len == 2
35 |
36 | block:
37 | let (r, m) = getAllInfos(level1, "/home", @[debugRequestMiddleware()])
38 | doAssert r == "/apiv2/level1/home"
39 | doAssert m.len == 4
40 |
41 | block:
42 | let (r, m) = getAllInfos(level2, "/home", @[debugRequestMiddleware()])
43 | doAssert r == "/apiv2/level1/level2/home"
44 | doAssert m.len == 5
45 |
46 | block:
47 | let (r, m) = getAllInfos(level3, "/home", @[debugRequestMiddleware()])
48 | doAssert r == "/apiv2/level1/level2/level3/home"
49 | doAssert m.len == 6
50 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_httpexception.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/httpexception
2 |
3 |
4 | block:
5 | doAssert HttpError is PrologueError
6 | doAssert AbortError is PrologueError
7 | doAssert RouteError is PrologueError
8 | doAssert RouteResetError is PrologueError
9 | doAssert DuplicatedRouteError is PrologueError
10 | doAssert DuplicatedReversedRouteError is PrologueError
11 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_nativesettings.nim:
--------------------------------------------------------------------------------
1 | include ../../../src/prologue/core/nativesettings
2 |
3 |
4 | block:
5 | let settings = newSettings()
6 | doAssert settings.address == ""
7 | doAssert settings.port.int == 8080
8 | doAssert settings.debug == true
9 | doAssert settings.reusePort == true
10 |
11 | doAssert settings.bufSize == 40960
12 | doAssert settings["prologue"].hasKey("secretKey")
13 | doAssert settings["prologue"]["secretKey"].getStr.len == 8
14 | doAssert settings["prologue"].getOrDefault("secretKey").getStr == settings["prologue"]["secretKey"].getStr
15 | doAssert settings.getOrDefault("empty").getStr.len == 0
16 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_response.nim:
--------------------------------------------------------------------------------
1 | from ../../../src/prologue/core/response import initResponse, setHeader, getHeader, addHeader, setCookie
2 | import ../../../src/prologue/core/httpcore/httplogue
3 |
4 | import strutils
5 |
6 |
7 | # "Test Response"
8 | block:
9 | let
10 | version = HttpVer11
11 | code = Http200
12 | body = "Hello, Prologue! "
13 |
14 | # "can init response"
15 | block:
16 | let
17 | response = initResponse(version, code, body = body)
18 |
19 | doAssert response.httpVersion == version
20 | doAssert response.code == code
21 | doAssert response.getHeader("Content-Type") == @["text/html; charset=UTF-8"]
22 | doAssert response.body == body
23 |
24 | # "can set response header"
25 | block:
26 | var
27 | response = initResponse(version, code, body = body)
28 |
29 | response.setHeader("Content-Type", "text/plain")
30 |
31 | doAssert response.getHeader("content-type") == @["text/plain"]
32 |
33 | # "can add response header"
34 | block:
35 | var
36 | response = initResponse(version, code, body = body)
37 |
38 | response.addHeader("Content-Type", "text/plain")
39 |
40 | doAssert response.getHeader("CONTENT-TYPE") == @[
41 | "text/html; charset=UTF-8", "text/plain"]
42 |
43 | # "can set response cookie"
44 | block:
45 | var
46 | response = initResponse(version, code, body = body)
47 |
48 | response.setCookie("username", "xxx")
49 | response.setCookie("password", "root")
50 | doAssert response.getHeader("set-cookie").join("; ") ==
51 | "username=xxx; password=root"
52 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_route.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/unit/tunit_core/tunit_route.nim
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_uid.nim:
--------------------------------------------------------------------------------
1 | import ../../../src/prologue/core/uid
2 |
3 | doAssert genUid().len == 24
4 |
--------------------------------------------------------------------------------
/tests/unit/tunit_core/tunit_urandom.nim:
--------------------------------------------------------------------------------
1 | from ../../../src/prologue/core/types import SecretKey, len
2 | from ../../../src/prologue/core/urandom import randomString, randomSecretKey
3 |
4 |
5 | # "Test Urandom"
6 | block:
7 | # "randomString can work"
8 | block:
9 | doAssert randomString(8).len == 8
10 |
11 | # "randomSecretKey can work"
12 | block:
13 | doAssert randomSecretKey(8).len == 8
14 |
--------------------------------------------------------------------------------
/tests/unit/tunit_security/tunit_hasher.nim:
--------------------------------------------------------------------------------
1 | from ../../../src/prologue/security/hasher import pbkdf2_sha256encode,
2 | pbkdf2_sha1encode, pbkdf2_sha256verify, pbkdf2_sha1verify
3 | from ../../../src/prologue/core/types import SecretKey
4 |
5 |
6 | # "Test Hasher"
7 | block:
8 | # "pbkdf2_sha256 can verify correct password"
9 | block:
10 | let res = pbkdf2_sha256encode(SecretKey("flywind"), "prologue")
11 | doAssert pbkdf2_sha256verify(SecretKey("flywind"), res)
12 |
13 | # "pbkdf2_sha256 can verify wrong password"
14 | block:
15 | let res = pbkdf2_sha256encode(SecretKey("flywind"), "prologue")
16 | doAssert not pbkdf2_sha256verify(SecretKey("flywin"), res)
17 |
18 | # "pbkdf2_sha1 can verify correct password"
19 | block:
20 | let res = pbkdf2_sha1encode(SecretKey("flywind"), "prologue")
21 | doAssert pbkdf2_sha1verify(SecretKey("flywind"), res)
22 |
23 | # "pbkdf2_sha1 can verify wrong password"
24 | block:
25 | let res = pbkdf2_sha1encode(SecretKey("flywind"), "prologue")
26 | doAssert not pbkdf2_sha1verify(SecretKey("flywin"), res)
27 |
--------------------------------------------------------------------------------
/tests/unit/tunit_security/tunit_signing.nim:
--------------------------------------------------------------------------------
1 | from ../../../src/prologue/core/types import SecretKey
2 | from ../../../src/prologue/signing import initSigner, initTimedSigner,
3 | BaseDigestMethodType, BadSignatureError, sign, unsign, validate
4 |
5 | import std/json
6 |
7 |
8 | # "Test signing"
9 | block:
10 | # "can sign with Signer"
11 | block:
12 | let
13 | key = SecretKey("secret-key")
14 | s = initSigner(key, salt = "itsdangerous.Signer",
15 | digestMethod = Sha512Type)
16 | sig = s.sign("my string")
17 |
18 |
19 | doAssert sig == "my string.Xu9Up4-UV7C46bh-AlQk86olom2irJLJJ" &
20 | "2wMMe5j5iuv4WKBgByR3wT3sWE3Pt6fqdqEANrO7sTwUvupadyPow"
21 | doAssert s.unsign(sig) == "my string"
22 | doAssert validate(s, sig)
23 |
24 | # "can sign with TimedSigner"
25 | block:
26 | let
27 | key = SecretKey("secret-key")
28 | s = initTimedSigner(key, salt = "activate",
29 | digestMethod = Sha1Type)
30 | sig = s.sign("my string")
31 | doAssert s.unsign(sig, 0) == "my string"
32 |
33 | # "can sign with json string"
34 | block:
35 | let
36 | key = SecretKey("secret-key")
37 | s = initSigner(key, salt = "activate",
38 | digestMethod = Blake2_256Type)
39 | discard s.sign( $ %*[1, 2, 3])
40 | doAssertRaises(BadSignatureError):
41 | discard s.unsign("[1, 2, 3].sdhfghjkjhdfghjigf")
42 |
--------------------------------------------------------------------------------
/tests/unit/tunit_staticfile/tunit_utils/static/css/basic.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/unit/tunit_staticfile/tunit_utils/static/css/basic.css
--------------------------------------------------------------------------------
/tests/unit/tunit_staticfile/tunit_utils/temp/basic.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/unit/tunit_staticfile/tunit_utils/temp/basic.html
--------------------------------------------------------------------------------
/tests/unit/tunit_staticfile/tunit_utils/templates/basic.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/unit/tunit_staticfile/tunit_utils/templates/basic.html
--------------------------------------------------------------------------------
/tests/unit/tunit_staticfile/tunit_utils/tunit_utils.nim:
--------------------------------------------------------------------------------
1 | include ../../../../src/prologue/middlewares/staticfile
2 | import std/os
3 |
4 |
5 | # "Test Utils"
6 | block:
7 | # "isStaticFile can work"
8 | block:
9 | let
10 | s1 = isStaticFile("tests/unit/tunit_staticfile/tunit_utils/static/css/basic.css", @["static", "tests"])
11 | s2 = isStaticFile("tests/unit/tunit_staticfile/tunit_utils/static/css/basic.css", @["tests"])
12 | s3 = isStaticFile("tests/unit/tunit_staticfile/tunit_utils/templates/basic.html", @["templates", "tests"])
13 | s4 = isStaticFile("tests/unit/tunit_staticfile/tunit_utils/temp/basic.html", @["templates", "static"])
14 |
15 | doAssert s1.hasValue
16 | doAssert s1.filename == "basic.css"
17 | doAssert s1.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/static/css")
18 | doAssert s2.hasValue
19 | doAssert s2.filename == "basic.css"
20 | doAssert s2.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/static/css")
21 | doAssert s3.hasValue
22 | doAssert s3.filename == "basic.html"
23 | doAssert s3.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/templates")
24 | doAssert not s4.hasValue
25 | doAssert s4.filename.len == 0
26 | doAssert s4.dir.len == 0
27 |
28 | # "Test Utils"
29 | block:
30 | # "isStaticFile can work"
31 | block:
32 | let
33 | s1 = isStaticFile("/tests//unit/tunit_staticfile/tunit_utils////static/////css////basic.css", @["/static", "tests"])
34 | s2 = isStaticFile("///////////tests/unit/tunit_staticfile/tunit_utils/static/css/basic.css", @["tests"])
35 | s3 = isStaticFile("//tests/unit/tunit_staticfile/tunit_utils/templates///////basic.html", @["//templates", "tests"])
36 | s4 = isStaticFile("tests/unit/tunit_staticfile/tunit_utils/temp/basic.html", @["templates", "static"])
37 |
38 | doAssert s1.hasValue
39 | doAssert s1.filename == "basic.css"
40 | doAssert s1.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/static/css")
41 | doAssert s2.hasValue
42 | doAssert s2.filename == "basic.css"
43 | doAssert s2.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/static/css")
44 | doAssert s3.hasValue
45 | doAssert s3.filename == "basic.html"
46 | doAssert s3.dir == normalizedPath("tests/unit/tunit_staticfile/tunit_utils/templates")
47 | doAssert not s4.hasValue
48 | doAssert s4.filename.len == 0
49 | doAssert s4.dir.len == 0
50 |
--------------------------------------------------------------------------------
/tests/unit/tunit_validate/test_basic.nim:
--------------------------------------------------------------------------------
1 | from ../../../src/prologue/validater/basic import isInt, isNumeric, isBool
2 |
3 |
4 | # "Test Is Utils"
5 | block:
6 | # "isInt can work"
7 | block:
8 | doAssert isInt("12")
9 | doAssert isInt("-753")
10 | doAssert isInt("0")
11 | doAssert not isInt("")
12 | doAssert not isInt("912.6")
13 | doAssert not isInt("a912")
14 |
15 | # "isNumeric can work"
16 | block:
17 | doAssert isNumeric("12")
18 | doAssert isNumeric("-753")
19 | doAssert isNumeric("0")
20 | doAssert isNumeric("0.5")
21 | doAssert isNumeric("-912.6")
22 | doAssert not isNumeric("")
23 | doAssert not isNumeric("a912")
24 | doAssert not isNumeric("0.91.2")
25 |
26 | # "isBool can work"
27 | block:
28 | doAssert isBool("true")
29 | doAssert isBool("1")
30 | doAssert isBool("yes")
31 | doAssert isBool("n")
32 | doAssert isBool("False")
33 | doAssert isBool("Off")
34 | doAssert not isBool("")
35 | doAssert not isBool("wrong")
--------------------------------------------------------------------------------
/tests/webdriver/twebdriver.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planety/prologue/0980261b81b9fa1d9ef6abd2b8a616e6ae79925b/tests/webdriver/twebdriver.nim
--------------------------------------------------------------------------------