├── tests ├── int │ ├── test_data │ │ ├── test_file_1.txt │ │ ├── test_file_2.txt │ │ ├── test_file_3.txt │ │ └── test_file_4.txt │ ├── config.nims │ └── test_yahttp.nim └── unit │ ├── config.nims │ └── test_yahttp.nim ├── examples ├── config.nims └── examples.nim ├── src ├── yahttp │ ├── exceptions.nim │ └── internal │ │ └── utils.nim └── yahttp.nim ├── .gitattributes ├── .gitignore ├── .github ├── release.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── yahttp.nimble ├── LICENSE └── README.md /tests/int/test_data/test_file_1.txt: -------------------------------------------------------------------------------- 1 | test content 2 | -------------------------------------------------------------------------------- /tests/int/test_data/test_file_2.txt: -------------------------------------------------------------------------------- 1 | test content 2 2 | -------------------------------------------------------------------------------- /tests/int/test_data/test_file_3.txt: -------------------------------------------------------------------------------- 1 | test content 3 2 | -------------------------------------------------------------------------------- /tests/int/test_data/test_file_4.txt: -------------------------------------------------------------------------------- 1 | test content 4 2 | -------------------------------------------------------------------------------- /examples/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | -------------------------------------------------------------------------------- /src/yahttp/exceptions.nim: -------------------------------------------------------------------------------- 1 | type HttpError* = object of IOError 2 | -------------------------------------------------------------------------------- /tests/int/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../../src") 2 | --d:ssl 3 | -------------------------------------------------------------------------------- /tests/unit/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../../src") 2 | --d:ssl 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | nimblecache/ 3 | htmldocs/ 4 | 5 | *.exe 6 | nim.cfg 7 | 8 | testresults/ 9 | testresults.html 10 | 11 | test_yahttp 12 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 2 | 3 | changelog: 4 | categories: 5 | - title: ⛵ Features 6 | labels: 7 | - enhancement 8 | exclude: 9 | labels: 10 | - dependencies 11 | - title: Other Changes 12 | labels: 13 | - "*" 14 | -------------------------------------------------------------------------------- /.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/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /yahttp.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.13.0" 4 | author = "Denis Mishankov" 5 | description = "Awesome simple HTTP client" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 2.0.0" 13 | 14 | task docs, "Genreate docs": 15 | exec "nim doc src/yahttp.nim" 16 | 17 | task pretty, "Pretty": 18 | exec "nimpretty src/yahttp.nim" 19 | 20 | task examples, "Run examples": 21 | exec "nim c --run examples/examples.nim" 22 | 23 | task unittests, "Run unit tests": 24 | exec "testament pattern \"tests/unit/*.nim\"" 25 | 26 | task inttests, "Run integation tests": 27 | exec "docker run -d --name yahttp-httpbin -p 8080:8080 mccutchen/go-httpbin" 28 | exec "testament pattern \"tests/int/*.nim\"" 29 | exec "docker stop yahttp-httpbin" 30 | exec "docker remove yahttp-httpbin" 31 | -------------------------------------------------------------------------------- /examples/examples.nim: -------------------------------------------------------------------------------- 1 | import json 2 | import yahttp 3 | 4 | # Getting cat tags as JSON 5 | let catTags = get("https://cataas.com/api/tags").json() 6 | 7 | # Loop through first 5 cat tags and if tag is not empty string get data of a couple of cats with this tag 8 | for catTag in catTags[0..4]: 9 | let tag = catTag.getStr() 10 | 11 | if tag.len() > 0: 12 | echo "====" 13 | echo "Working with tag " & tag 14 | let catData = get("https://cataas.com/api/cats", query = {"tags": tag, "limit": "10"}) 15 | 16 | echo "Request method and URL: ", catData.request.httpMethod, " ", catData.request.url 17 | echo "Response status: ", catData.status 18 | echo "Response headers: ", catData.headers 19 | echo "Response body: ", catData.body 20 | 21 | # Send file 22 | echo post("https://validator.w3.org/check", files = @[("uploaded_file", "test.html", "text/html", "

test

")]).body 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Denis Mishankov 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. 22 | -------------------------------------------------------------------------------- /src/yahttp/internal/utils.nim: -------------------------------------------------------------------------------- 1 | import macros, strutils, strformat 2 | 3 | macro http_method_gen*(name: untyped): untyped = 4 | let methodUpper = name.strVal().toUpper() 5 | let methodName = newIdentNode(methodUpper) 6 | let comment = newCommentStmtNode(fmt"Proc for {methodUpper} HTTP method") 7 | quote do: 8 | proc `name`*(url: string, headers: openArray[RequestHeader] = [], query: openArray[ 9 | QueryParam] = [], encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, body: string = "", files: openArray[MultipartFile] = [], streamingFiles: openArray[StreamingMultipartFile] = [], auth: BasicAuth = ("", ""), timeout = -1, 10 | ignoreSsl = false, sslContext: SslContext = nil): Response = 11 | `comment` 12 | return request( 13 | url = url, 14 | httpMethod = Method.`methodName`, 15 | headers = headers, 16 | query = query, 17 | body = body, 18 | files = files, 19 | streamingFiles = streamingFiles, 20 | auth = auth, 21 | timeout = timeout, 22 | ignoreSsl = ignoreSsl, 23 | sslContext = sslContext 24 | ) 25 | 26 | 27 | macro http_method_no_body_gen*(name: untyped): untyped = 28 | let methodUpper = name.strVal().toUpper() 29 | let methodName = newIdentNode(methodUpper) 30 | let comment = newCommentStmtNode(fmt"Proc for {methodUpper} HTTP method") 31 | quote do: 32 | proc `name`*(url: string, headers: openArray[RequestHeader] = [], query: openArray[ 33 | QueryParam] = [], encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, auth: BasicAuth = ("", ""), timeout = -1, ignoreSsl = false, sslContext: SslContext = nil): Response = 34 | `comment` 35 | return request( 36 | url = url, 37 | httpMethod = Method.`methodName`, 38 | headers = headers, 39 | query = query, 40 | auth = auth, 41 | timeout = timeout, 42 | ignoreSsl = ignoreSsl, 43 | sslContext = sslContext 44 | ) 45 | -------------------------------------------------------------------------------- /tests/unit/test_yahttp.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | include yahttp 4 | 5 | 6 | test "Generate correct basic auth header": 7 | check ("test login", "test_pass").basicAuthHeader() == "Basic dGVzdCBsb2dpbjp0ZXN0X3Bhc3M=" 8 | 9 | test "OK response": 10 | check Response(status: 204).ok() 11 | 12 | test "Not OK response": 13 | check not Response(status: 404).ok() 14 | 15 | test "Exception for 4xx and 5xx": 16 | expect HttpError: 17 | Response(status: 400).raiseForStatus() 18 | 19 | expect HttpError: 20 | Response(status: 599).raiseForStatus() 21 | 22 | test "Convert to object": 23 | type T = object 24 | key: string 25 | 26 | check Response(status: 200, body: "{\"key\": \"value\"}").to(T) == T(key: "value") 27 | 28 | test "Parsing an object into a string": 29 | type MyJson = object 30 | code: int 31 | error: bool 32 | message: string 33 | 34 | let actual = MyJson(code: 400, error: false, message: "test") 35 | let expected = """{"code":400,"error":false,"message":"test"}""" 36 | 37 | # ToJsonString needs an actual object, it will not work with a JsonNode (%*{}) 38 | check actual.toJsonString() == expected 39 | 40 | test "Parse a valid json body from a response": 41 | let response = Response(status: 200, body: """{"key": "value"}""") 42 | 43 | let actual = response.json() 44 | let expected = %*{"key": "value"} 45 | 46 | check actual == expected 47 | 48 | test "Parsing an invalid json body from a respones raises an error": 49 | let response = Response(status: 200, body: """{"key": "value""") 50 | 51 | expect JsonParsingError: 52 | discard response.json() 53 | 54 | test "Parse a valid json body and unmarshal it": 55 | type MyJson = object 56 | code: int 57 | error: bool 58 | message: string 59 | 60 | let response = Response(status: 200, body: """{"code": 200, "error": false, "message": "test"}""") 61 | 62 | let actual = MyJson(code: 200, error: false, message: "test") 63 | let expected = response.to(MyJson) 64 | 65 | check actual == expected 66 | check actual is MyJson 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: ["v*.*.*"] 7 | pull_request: 8 | branches: [ main ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | jobs: 17 | build: 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [ ubuntu-latest, macOS-latest, windows-latest ] 25 | nim-version: [ "2.0.x", "2.2.x" ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v6 30 | 31 | - uses: jiro4989/setup-nim-action@v2 32 | with: 33 | nim-version: ${{ matrix.nim-version }} 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Run unit tests 37 | run: nimble unittests 38 | 39 | - name: Run integation tests 40 | # go-httpbin container does not run on windows runner for some reason. macos runner does not has docker by default 41 | if: ${{ matrix.os == 'ubuntu-latest' }} 42 | run: nimble inttests 43 | 44 | deploy-docs: 45 | needs: 46 | - build 47 | 48 | if: github.ref_type == 'tag' 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v6 61 | 62 | - uses: jiro4989/setup-nim-action@v2 63 | with: 64 | nim-version: "2.2.x" 65 | repo-token: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Generate docs 68 | run: nim -d:ssl --outdir:./htmldocs doc --project src/yahttp.nim 69 | 70 | - name: Setup Pages 71 | uses: actions/configure-pages@v5 72 | 73 | - name: Upload artifact 74 | uses: actions/upload-pages-artifact@v4 75 | with: 76 | path: './htmldocs' 77 | 78 | - name: Deploy to GitHub Pages 79 | id: deployment 80 | uses: actions/deploy-pages@v4 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛵ yahttp - Awesome simple HTTP client for Nim 2 | 3 | [![GitHub Release](https://img.shields.io/github/v/release/mishankov/yahttp?sort=semver&display_name=tag&logo=nim&label=latest%20release&color=%23FFE953)](https://github.com/mishankov/yahttp/releases/latest) 4 | [![CI](https://github.com/mishankov/yahttp/actions/workflows/ci.yml/badge.svg)](https://github.com/mishankov/yahttp/actions/workflows/ci.yml) 5 | 6 | 7 | - Based on Nim [std/httpclient](https://nim-lang.org/docs/httpclient.html) 8 | - No additional dependencies 9 | - API focused on DX 10 | 11 | # Installation 12 | 13 | ```shell 14 | nimble install yahttp 15 | ``` 16 | 17 | # Examples 18 | 19 | > more examples [here](examples/examples.nim) 20 | 21 | ## Get HTTP status code 22 | 23 | ```nim 24 | import yahttp 25 | 26 | echo get("https://www.google.com/").status 27 | ``` 28 | ## Send query params and parse response to JSON 29 | 30 | ```nim 31 | import json 32 | import yahttp 33 | 34 | let laptopsJson = get("https://dummyjson.com/products/search", query = {"q": "Laptop"}).json() 35 | echo laptopsJson["products"][0]["title"].getStr() 36 | ``` 37 | # API 38 | 39 | ## Method procedures 40 | 41 | ```nim 42 | get("http://api") 43 | put("http://api") 44 | post("http://api") 45 | patch("http://api") 46 | delete("http://api") 47 | head("http://api") 48 | options("http://api") 49 | ``` 50 | Arguments: 51 | - `url` - request URL. The only required argument 52 | - `headers` - request HTTP headers. Example: `{"header1": "val", "header2": "val2"}` 53 | - `query` - request query params. Example: `{"param1": "val", "param2": "val2"}` 54 | - `encodeQueryParams` - parameters for `encodeQuery` function that encodes query params. [More](https://nim-lang.org/docs/uri.html#encodeQuery%2CopenArray%5B%5D%2Cchar) 55 | - `body` - request body as a string. Example: `"{\"key\": \"value\"}"`. Is not available for `get`, `head` and `options` procedures 56 | - `files` - array of files to upload. Every file is a tuple of multipart name, file name, content type and content 57 | - `sreamingFiles` - array of files to stream from disc and upload. Every file is a tuple of multipart name and file path 58 | - `auth` - login and password for basic authorization. Example: `("login", "password")` 59 | - `timeout` - stop waiting for a response after a given number of milliseconds. `-1` for no timeout, which is default value 60 | - `ignoreSsl` - no certificate verification if `true` 61 | - `sslContext` - SSL context for TLS/SSL connections. See [newContext](https://nim-lang.org/docs/net.html#newContext%2Cstring%2Cstring%2Cstring%2Cstring) 62 | 63 | ## General procedure 64 | 65 | ```nim 66 | request("http://api") 67 | ``` 68 | 69 | Has the same arguments as method procedures and one additional: 70 | - `httpMethod` - HTTP method. `Method.GET` by default. Example: `Method.POST` 71 | 72 | ## Response object 73 | 74 | All procedures above return `Response` object with fields: 75 | - `status` - HTTP status code 76 | - `body` - response body as a string 77 | - `headers` - table, where keys are header keys and values are sequences of header values for a key 78 | - `request` - object with request data processed by `yahttp` 79 | - `url` - stores full url with query params 80 | - `headers` - stores HTTP headers with `Authorization` for basic authorization 81 | - `httpMethod` - HTTP method 82 | - `body` - request body as a string 83 | 84 | `Response` object has some helper procedures: 85 | - `Response.json()` - returns response body as JSON 86 | - `Response.html()` - returns response body as HTML 87 | - `Response.to(t)` - converts response body to JSON and unmarshals it to type `t` 88 | - `Response.ok()` - returns `true` if `status` is greater than 0 and less than 400 89 | - `Response.raiseForStatus()` - throws `HttpError` exceptions if status is 400 or above 90 | 91 | ## Other helper functions 92 | 93 | `object.toJsonString()` - converts object of any type to json string. Helpful to use for `body` argument 94 | -------------------------------------------------------------------------------- /src/yahttp.nim: -------------------------------------------------------------------------------- 1 | import base64, httpclient, net, json, uri, strutils, tables, htmlparser, xmltree 2 | 3 | import yahttp/internal/utils 4 | import yahttp/exceptions 5 | 6 | type 7 | ## Types without methods 8 | QueryParam* = tuple[key: string, value: string] ## Type for URL query params 9 | RequestHeader* = tuple[key: string, value: string] ## Type for HTTP header 10 | 11 | EncodeQueryParams* = object 12 | ## Parameters for encodeQuery procedure 13 | usePlus*: bool 14 | omitEq*: bool 15 | sep*: char 16 | 17 | MultipartFile* = tuple[multipartName, fileName, contentType, 18 | content: string] ## Type for uploaded file 19 | 20 | StreamingMultipartFile* = tuple[name, file: string] ## Type for streaming file 21 | 22 | Method* = enum 23 | ## Supported HTTP methods 24 | GET, PUT, POST, PATCH, DELETE, HEAD, OPTIONS 25 | 26 | 27 | type 28 | BasicAuth* = tuple[login: string, password: string] ## Basic auth type 29 | 30 | proc basicAuthHeader(auth: BasicAuth): string = 31 | return "Basic " & encode(auth.login & ":" & auth.password) 32 | 33 | type 34 | Request* = object 35 | ## Type to store request information in response 36 | url*: string 37 | headers*: seq[tuple[key: string, val: string]] 38 | httpMethod*: Method 39 | body*: string 40 | 41 | Response* = object 42 | ## Type for HTTP response 43 | status*: int 44 | body*: string 45 | headers*: TableRef[string, seq[string]] 46 | request*: Request 47 | 48 | proc toResp(response: httpclient.Response, requestUrl: string, 49 | requestHeaders: seq[tuple[key: string, val: string]], 50 | requestHttpMethod: Method, requestBody: string): Response = 51 | ## Converts httpclient.Response to yahttp.Response 52 | return Response( 53 | status: parseInt(response.status.strip()[0..2]), 54 | headers: response.headers.table, 55 | body: response.body, 56 | request: Request(url: requestUrl, headers: requestHeaders, 57 | httpMethod: requestHttpMethod, body: requestBody) 58 | ) 59 | 60 | proc json*(response: Response): JsonNode = 61 | ## Parses response body to json 62 | return parseJson(response.body) 63 | 64 | proc html*(response: Response): XmlNode = 65 | ## Parses response body to html 66 | return parseHtml(response.body) 67 | 68 | proc to*[T](response: Response, t: typedesc[T]): T = 69 | ## Parses response body to json and then casts it to passed type 70 | return to(response.json(), t) 71 | 72 | proc ok*(response: Response): bool = 73 | ## Is HTTP status in OK range (> 0 and < 400)? 74 | return response.status > 0 and response.status < 400 75 | 76 | proc raiseForStatus*(response: Response) {.raises: [HttpError].} = 77 | ## Throws `HttpError` exceptions if status is 400 or above 78 | if response.status >= 400: raise HttpError.newException("Status is: " & 79 | $response.status) 80 | 81 | 82 | proc toJsonString*(obj: object): string = 83 | ## Converts object of any type to json. Helpful to use for `body` argument 84 | return $ %*obj 85 | 86 | 87 | const defaultEncodeQueryParams = EncodeQueryParams(usePlus: false, omitEq: true, sep: '&') 88 | 89 | 90 | proc request*(url: string, httpMethod: Method = Method.GET, headers: openArray[ 91 | RequestHeader] = [], query: openArray[QueryParam] = [], 92 | encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, 93 | body: string = "", files: openArray[MultipartFile] = [], 94 | streamingFiles: openArray[StreamingMultipartFile] = [], 95 | auth: BasicAuth = ("", ""), timeout = -1, ignoreSsl = false, 96 | sslContext: SslContext = nil): Response = 97 | ## Genreal proc to make HTTP request with every HTTP method 98 | 99 | # Prepare client 100 | 101 | let client: HttpClient = if sslContext != nil: 102 | newHttpClient(timeout = timeout, sslContext = sslContext) 103 | elif ignoreSsl: 104 | newHttpClient(timeout = timeout, sslContext = newContext( 105 | verifyMode = CVerifyNone)) 106 | else: 107 | newHttpClient(timeout = timeout) 108 | 109 | # Prepare headers 110 | 111 | var innerHeaders: seq[tuple[key: string, val: string]] = @[] 112 | 113 | for header in headers: 114 | innerHeaders.add((header.key, header.value)) 115 | 116 | if auth.login != "" and auth.password != "": 117 | innerHeaders.add({"Authorization": auth.basicAuthHeader()}) 118 | 119 | if innerHeaders.len() > 0: 120 | client.headers = newHttpHeaders(innerHeaders) 121 | 122 | # Prepare url 123 | 124 | let innerUrl = if query.len() > 0: url & "?" & encodeQuery(query, 125 | usePlus = encodeQueryParams.usePlus, omitEq = encodeQueryParams.omitEq, 126 | sep = encodeQueryParams.sep) else: url 127 | 128 | # Prepare HTTP method 129 | 130 | let innerMethod: HttpMethod = case httpMethod: 131 | of Method.GET: HttpGet 132 | of Method.PUT: HttpPut 133 | of Method.POST: HttpPost 134 | of Method.PATCH: HttpPatch 135 | of Method.DELETE: HttpDelete 136 | of Method.HEAD: HttpHead 137 | of Method.OPTIONS: HttpOptions 138 | 139 | # Make request 140 | 141 | let response = if files.len() > 0 or streamingFiles.len() > 0: 142 | # Prepare multipart data for files 143 | var multipartData = newMultipartData() 144 | 145 | if files.len() > 0: 146 | for file in files: 147 | multipartData[file.multipartName] = (file.fileName, file.contentType, file.content) 148 | 149 | if streamingFiles.len() > 0: 150 | multipartData.addFiles(streamingFiles) 151 | 152 | 153 | client.request(innerUrl, httpMethod = innerMethod, 154 | multipart = multipartData) 155 | else: 156 | client.request(innerUrl, httpMethod = innerMethod, body = body) 157 | 158 | client.close() 159 | 160 | return response.toResp(requestUrl = innerUrl, requestHeaders = innerHeaders, 161 | requestHttpMethod = httpMethod, requestBody = body) 162 | 163 | 164 | # Gnerating procs for individual HTTP methods 165 | 166 | http_method_no_body_gen get 167 | http_method_no_body_gen head 168 | http_method_no_body_gen options 169 | http_method_gen put 170 | http_method_gen post 171 | http_method_gen patch 172 | http_method_gen delete 173 | -------------------------------------------------------------------------------- /tests/int/test_yahttp.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | include yahttp 4 | 5 | 6 | const BASE_URL = "http://localhost:8080" 7 | const INT_TESTS_BASE_PATH = "tests/int" 8 | 9 | test "Test HTTP methods": 10 | check get(BASE_URL & "/get").ok() 11 | check head(BASE_URL & "/head").ok() 12 | check put(BASE_URL & "/put").ok() 13 | check post(BASE_URL & "/post").ok() 14 | check patch(BASE_URL & "/patch").ok() 15 | check delete(BASE_URL & "/delete").ok() 16 | 17 | test "Test query params": 18 | let jsonResp = get(BASE_URL & "/get", query={"param1": "value1", "param2": "value2"}).json() 19 | 20 | check jsonResp["args"]["param1"][0].getStr() == "value1" 21 | check jsonResp["args"]["param2"][0].getStr() == "value2" 22 | 23 | test "Test headers": 24 | let jsonResp = get(BASE_URL & "/headers", headers={"header1": "value1", "header2": "value2"}).json() 25 | 26 | check jsonResp["headers"]["Header1"][0].getStr() == "value1" 27 | check jsonResp["headers"]["Header2"][0].getStr() == "value2" 28 | 29 | test "Test auth header": 30 | let jsonResp = get(BASE_URL & "/headers", auth=("test login", "test_pass")).json() 31 | 32 | check jsonResp["headers"]["Authorization"][0].getStr() == "Basic dGVzdCBsb2dpbjp0ZXN0X3Bhc3M=" 33 | 34 | test "Test body": 35 | let jsonResp = put(BASE_URL & "/put", headers = {"Content-Type": "text/plain"}, body = "some body").json() 36 | 37 | check jsonResp["data"].getStr() == "some body" 38 | 39 | test "Test JSON body": 40 | let jsonResp = put(BASE_URL & "/put", headers = {"Content-Type": "application/json"}, body = $ %*{"key": "value"}).json() 41 | 42 | check jsonResp["json"]["key"].getStr() == "value" 43 | 44 | test "Test body with toJsonString helper": 45 | type TestReq = object 46 | field1: string 47 | field2: int 48 | 49 | let jsonResp = put(BASE_URL & "/put", headers = {"Content-Type": "application/json"}, body = TestReq(field1: "value1", field2: 123).toJsonString()).json() 50 | 51 | check jsonResp["json"]["field1"].getStr() == "value1" 52 | check jsonResp["json"]["field2"].getInt() == 123 53 | 54 | test "Test timeout": 55 | expect TimeoutError: 56 | discard get(BASE_URL & "/delay/5", timeout = 100) 57 | 58 | # No exception 59 | discard get(BASE_URL & "/delay/5", timeout = -1) 60 | 61 | 62 | test "Test sending single file": 63 | let resp = post(BASE_URL & "/post", files = @[("my_file", "test.txt", "text/plain", "some file content")]).json() 64 | 65 | check resp["files"]["my_file"][0].getStr() == "some file content" 66 | check resp["data"].getStr().contains("test.txt") 67 | check resp["data"].getStr().contains("text/plain") 68 | check resp["data"].getStr().contains("some file content") 69 | 70 | test "Test sending multiple files": 71 | let resp = post(BASE_URL & "/post", files = @[("my_file", "test.txt", "text/plain", "some file content"), ("my_second_file", "test2.txt", "text/plain", "second file content")]).json() 72 | 73 | check resp["files"]["my_file"][0].getStr() == "some file content" 74 | check resp["files"]["my_second_file"][0].getStr() == "second file content" 75 | check resp["data"].getStr().contains("test.txt") 76 | check resp["data"].getStr().contains("text/plain") 77 | check resp["data"].getStr().contains("some file content") 78 | check resp["data"].getStr().contains("test2.txt") 79 | check resp["data"].getStr().contains("text/plain") 80 | check resp["data"].getStr().contains("second file content") 81 | 82 | 83 | const TEST_FILE_PATH_1 = INT_TESTS_BASE_PATH & "/test_data/test_file_1.txt" 84 | const TEST_FILE_CONTENT_1 = readFile(TEST_FILE_PATH_1) 85 | 86 | const TEST_FILE_PATH_2 = INT_TESTS_BASE_PATH & "/test_data/test_file_2.txt" 87 | const TEST_FILE_CONTENT_2 = readFile(TEST_FILE_PATH_2) 88 | 89 | const TEST_FILE_PATH_3 = INT_TESTS_BASE_PATH & "/test_data/test_file_3.txt" 90 | const TEST_FILE_CONTENT_3 = readFile(TEST_FILE_PATH_3) 91 | 92 | const TEST_FILE_PATH_4 = INT_TESTS_BASE_PATH & "/test_data/test_file_4.txt" 93 | const TEST_FILE_CONTENT_4 = readFile(TEST_FILE_PATH_4) 94 | 95 | test "Test streaming single file": 96 | let resp = post(BASE_URL & "/post", streamingFiles = @[("my_file", TEST_FILE_PATH_1)]).json() 97 | 98 | check resp["files"]["my_file"][0].getStr() == TEST_FILE_CONTENT_1 99 | check resp["data"].getStr().contains("test_file_1.txt") 100 | check resp["data"].getStr().contains("text/plain") 101 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_1) 102 | 103 | test "Test streaming multiple files": 104 | let resp = post(BASE_URL & "/post", streamingFiles = @[("my_file", TEST_FILE_PATH_1), ("my_second_file", TEST_FILE_PATH_2)]).json() 105 | 106 | check resp["files"]["my_file"][0].getStr() == TEST_FILE_CONTENT_1 107 | check resp["files"]["my_second_file"][0].getStr() == TEST_FILE_CONTENT_2 108 | check resp["data"].getStr().contains("test_file_1.txt") 109 | check resp["data"].getStr().contains("text/plain") 110 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_1) 111 | check resp["data"].getStr().contains("test_file_2.txt") 112 | check resp["data"].getStr().contains("text/plain") 113 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_2) 114 | 115 | test "Test sending and streaming multiple files": 116 | let resp = post(BASE_URL & "/post", streamingFiles = @[("my_file", TEST_FILE_PATH_1), ("my_second_file", TEST_FILE_PATH_2)], files = @[("my_third_file", "test_file_3.txt", "text/plain", TEST_FILE_CONTENT_3), ("my_fourth_file", "test_file_4.txt", "text/plain", TEST_FILE_CONTENT_4)]).json() 117 | 118 | check resp["files"]["my_file"][0].getStr() == TEST_FILE_CONTENT_1 119 | check resp["files"]["my_second_file"][0].getStr() == TEST_FILE_CONTENT_2 120 | check resp["files"]["my_third_file"][0].getStr() == TEST_FILE_CONTENT_3 121 | check resp["files"]["my_fourth_file"][0].getStr() == TEST_FILE_CONTENT_4 122 | check resp["data"].getStr().contains("test_file_1.txt") 123 | check resp["data"].getStr().contains("text/plain") 124 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_1) 125 | check resp["data"].getStr().contains("test_file_2.txt") 126 | check resp["data"].getStr().contains("text/plain") 127 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_2) 128 | check resp["data"].getStr().contains("test_file_3.txt") 129 | check resp["data"].getStr().contains("text/plain") 130 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_3) 131 | check resp["data"].getStr().contains("test_file_4.txt") 132 | check resp["data"].getStr().contains("text/plain") 133 | check resp["data"].getStr().contains(TEST_FILE_CONTENT_4) 134 | --------------------------------------------------------------------------------