diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2981f32..e4cced6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, macOS-latest, windows-latest ] - nim-version: [ "2.0.0", "2.0.2", "2.0.4", "2.0.6", "2.0.8" ] + nim-version: [ "2.0.x" ] steps: - name: Checkout repository @@ -45,6 +45,8 @@ jobs: needs: - build + if: github.ref_type == 'tag' + environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -52,8 +54,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: github.event_name == 'release' && github.event.action == 'created' - runs-on: ubuntu-latest steps: - name: Checkout diff --git a/README.md b/README.md index 8e5484c..5c7c35e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Arguments: - `query` - request query params. Example: `{"param1": "val", "param2": "val2"}` - `encodeQueryParams` - parameters for `encodeQuery` function that encodes query params. [More](https://nim-lang.org/docs/uri.html#encodeQuery%2CopenArray%5B%5D%2Cchar) - `body` - request body as a string. Example: `"{\"key\": \"value\"}"`. Is not available for `get`, `head` and `options` procedures +- `files` - array of files to upload. Every file is a tuple of multipart name, file name, content type and content +- `sreamingFiles` - array of files to stream from disc and upload. Every file is a tuple of multipart name and file path - `auth` - login and password for basic authorization. Example: `("login", "password")` - `timeout` - stop waiting for a response after a given number of milliseconds. `-1` for no timeout, which is default value - `ignoreSsl` - no certificate verification if `true` diff --git a/examples/examples.nim b/examples/examples.nim index 7ca77bc..7ecb5da 100644 --- a/examples/examples.nim +++ b/examples/examples.nim @@ -17,3 +17,6 @@ for catTag in catTags[0..4]: echo "Response status: ", catData.status echo "Response headers: ", catData.headers echo "Response body: ", catData.body + +# Send file +echo post("https://validator.w3.org/check", files = @[("uploaded_file", "test.html", "text/html", "
test
")]).body diff --git a/src/yahttp.nim b/src/yahttp.nim index a7a17a2..57d88d9 100644 --- a/src/yahttp.nim +++ b/src/yahttp.nim @@ -14,6 +14,11 @@ type omitEq*: bool sep*: char + MultipartFile* = tuple[multipartName, fileName, contentType, + content: string] ## Type for uploaded file + + StreamingMultipartFile* = tuple[name, file: string] ## Type for streaming file + Method* = enum ## Supported HTTP methods GET, PUT, POST, PATCH, DELETE, HEAD, OPTIONS @@ -85,8 +90,10 @@ const defaultEncodeQueryParams = EncodeQueryParams(usePlus: false, omitEq: true, proc request*(url: string, httpMethod: Method = Method.GET, headers: openArray[ RequestHeader] = [], query: openArray[QueryParam] = [], encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, - body: string = "", - auth: BasicAuth = ("", ""), timeout = -1, ignoreSsl = false, sslContext: SslContext = nil): Response = + body: string = "", files: openArray[MultipartFile] = [], + streamingFiles: openArray[StreamingMultipartFile] = [], + auth: BasicAuth = ("", ""), timeout = -1, ignoreSsl = false, + sslContext: SslContext = nil): Response = ## Genreal proc to make HTTP request with every HTTP method # Prepare client @@ -131,7 +138,26 @@ proc request*(url: string, httpMethod: Method = Method.GET, headers: openArray[ # Make request - let response = client.request(innerUrl, httpMethod = innerMethod, body = body) + let response = if files.len() > 0: + # Prepare multipart data for files + + var multipartData = newMultipartData() + for file in files: + multipartData[file.multipartName] = (file.fileName, file.contentType, file.content) + client.request(innerUrl, httpMethod = innerMethod, + multipart = multipartData) + elif streamingFiles.len() > 0: + # Prepare multipart data for streaming files + + var multipartData = newMultipartData() + # for file in streamingFiles: + multipartData.addFiles(streamingFiles) + client.request(innerUrl, httpMethod = innerMethod, + multipart = multipartData) + + else: + client.request(innerUrl, httpMethod = innerMethod, body = body) + client.close() return response.toResp(requestUrl = innerUrl, requestHeaders = innerHeaders, diff --git a/src/yahttp/internal/utils.nim b/src/yahttp/internal/utils.nim index d8d2844..bb3cdec 100644 --- a/src/yahttp/internal/utils.nim +++ b/src/yahttp/internal/utils.nim @@ -6,7 +6,7 @@ macro http_method_gen*(name: untyped): untyped = let comment = newCommentStmtNode(fmt"Proc for {methodUpper} HTTP method") quote do: proc `name`*(url: string, headers: openArray[RequestHeader] = [], query: openArray[ - QueryParam] = [], encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, body: string = "", auth: BasicAuth = ("", ""), timeout = -1, + QueryParam] = [], encodeQueryParams: EncodeQueryParams = defaultEncodeQueryParams, body: string = "", files: openArray[MultipartFile] = [], streamingFiles: openArray[StreamingMultipartFile] = [], auth: BasicAuth = ("", ""), timeout = -1, ignoreSsl = false, sslContext: SslContext = nil): Response = `comment` return request( @@ -15,6 +15,8 @@ macro http_method_gen*(name: untyped): untyped = headers = headers, query = query, body = body, + files = files, + streamingFiles = streamingFiles, auth = auth, timeout = timeout, ignoreSsl = ignoreSsl, diff --git a/tests/int/test_data/test_file_1.txt b/tests/int/test_data/test_file_1.txt new file mode 100644 index 0000000..d670460 --- /dev/null +++ b/tests/int/test_data/test_file_1.txt @@ -0,0 +1 @@ +test content diff --git a/tests/int/test_data/test_file_2.txt b/tests/int/test_data/test_file_2.txt new file mode 100644 index 0000000..b13c288 --- /dev/null +++ b/tests/int/test_data/test_file_2.txt @@ -0,0 +1 @@ +test content 2 diff --git a/tests/int/test_yahttp.nim b/tests/int/test_yahttp.nim index a3d60d7..6c6ff1b 100644 --- a/tests/int/test_yahttp.nim +++ b/tests/int/test_yahttp.nim @@ -5,8 +5,11 @@ include yahttp const BASE_URL = "http://localhost:8080" +const INT_TESTS_BASE_PATH = "tests/int" test "Test HTTP methods": + check get(BASE_URL & "/get").ok() + check head(BASE_URL & "/head").ok() check put(BASE_URL & "/put").ok() check post(BASE_URL & "/post").ok() check patch(BASE_URL & "/patch").ok() @@ -39,7 +42,7 @@ test "Test JSON body": check jsonResp["json"]["key"].getStr() == "value" -test "Test body with toJson helper": +test "Test body with toJsonString helper": type TestReq = object field1: string field2: int @@ -55,3 +58,51 @@ test "Test timeout": # No exception discard get(BASE_URL & "/delay/5", timeout = -1) + + +test "Test sending single file": + let resp = post(BASE_URL & "/post", files = @[("my_file", "test.txt", "text/plain", "some file content")]).json() + + check resp["files"]["my_file"][0].getStr() == "some file content" + check resp["data"].getStr().contains("test.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains("some file content") + +test "Test sending multiple files": + 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() + + check resp["files"]["my_file"][0].getStr() == "some file content" + check resp["files"]["my_second_file"][0].getStr() == "second file content" + check resp["data"].getStr().contains("test.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains("some file content") + check resp["data"].getStr().contains("test2.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains("second file content") + + +const TEST_FILE_PATH_1 = INT_TESTS_BASE_PATH & "/test_data/test_file_1.txt" +const TEST_FILE_CONTENT_1 = readFile(TEST_FILE_PATH_1) + +const TEST_FILE_PATH_2 = INT_TESTS_BASE_PATH & "/test_data/test_file_2.txt" +const TEST_FILE_CONTENT_2 = readFile(TEST_FILE_PATH_2) + +test "Test streaming single file": + let resp = post(BASE_URL & "/post", streamingFiles = @[("my_file", TEST_FILE_PATH_1)]).json() + + check resp["files"]["my_file"][0].getStr() == TEST_FILE_CONTENT_1 + check resp["data"].getStr().contains("test_file_1.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains(TEST_FILE_CONTENT_1) + +test "Test streaming multiple files": + let resp = post(BASE_URL & "/post", streamingFiles = @[("my_file", TEST_FILE_PATH_1), ("my_second_file", TEST_FILE_PATH_2)]).json() + + check resp["files"]["my_file"][0].getStr() == TEST_FILE_CONTENT_1 + check resp["files"]["my_second_file"][0].getStr() == TEST_FILE_CONTENT_2 + check resp["data"].getStr().contains("test_file_1.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains(TEST_FILE_CONTENT_1) + check resp["data"].getStr().contains("test_file_2.txt") + check resp["data"].getStr().contains("text/plain") + check resp["data"].getStr().contains(TEST_FILE_CONTENT_2) diff --git a/yahttp.nimble b/yahttp.nimble index fa2923c..5e9ee0a 100644 --- a/yahttp.nimble +++ b/yahttp.nimble @@ -1,6 +1,6 @@ # Package -version = "0.12.0" +version = "0.13.0" author = "Denis Mishankov" description = "Awesome simple HTTP client" license = "MIT"