├── .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 | ![hello world](assets/openapi/docs.jpg) 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 |
7 | 8 | 9 |
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 | ![screenshot](screenshot/screenshot.jpg) 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 |
7 | $tok 8 | <-- All other inputs --> 9 |
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 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /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 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /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 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------