From ff0841d405198177685e224b92ef710fb25fbee7 Mon Sep 17 00:00:00 2001 From: Daniel Rudnitski Date: Tue, 20 Aug 2024 20:41:46 -0400 Subject: [PATCH] update to 0.3.0 (#13) * fix: version compatibility (#12) * fix: compilation issues includes: - bump to zig from 0.9 to 0.11 - `Jason` package dependency - better libcurl library linking * chore: bump to 0.3.0 * chore(ci): updated version matrix * fix(compatibility): set elixir 1.14 as minimum zigler depends on `Keyword.replace_lazy` which was introduced in Elixir 1.14. * chore(deps): bump zigler to 0.12.x dev branch * fix: working with zigler and zig 0.12 * fix(ci): install zig and update version matrix * chore(ci): update deprecated action steps * feat: add devenv * chore(deps): bump ex_doc to 0.34 * chore(deps): update zigler version * feat(ci): add macos-latest and use ubuntu-latest * fix(macos-ci): caching and rebar3 * fix(ci): macos installs zig * chore(deps): bump bypass deps * fix(ci): macos erlang 27.0 -> 27.0.1 * chore(macos-ci): specify elixir otp version * fix(macos-ci): use asdf to install zig instead of zig.get * fix(macos-ci): less specific latest elixir/otp versions * chore(deps): bump to stable zigler * chore(devenv): bump to zig 0.13 * chore(ci): use zig.get * chore(ci): add publish action * chore(ci): use separate action for zig * chore: use metrics struct instead of inline fields * chore(docs): minor docs corrections * chore(docs): updated repo urls * chore(lint): use Zig.Formatter --- .formatter.exs | 3 +- .github/workflows/ci.yml | 65 ++++- .github/workflows/publish.yml | 30 +++ .gitignore | 6 + README.md | 8 +- devenv.lock | 139 +++++++++++ devenv.nix | 14 ++ devenv.yaml | 7 + lib/client.ex | 4 +- lib/curl_error_codes.ex | 2 +- lib/ex_curl.ex | 24 +- lib/request.ex | 446 ++++++++++++++++------------------ lib/request_configuration.ex | 2 +- lib/response.ex | 39 ++- lib/response_metrics.ex | 32 +++ lib/zig_multiplatform_curl.ex | 31 --- mix.exs | 13 +- mix.lock | 34 +-- test/ex_curl_test.exs | 14 +- test/headers_test.exs | 2 +- 20 files changed, 556 insertions(+), 359 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 devenv.lock create mode 100644 devenv.nix create mode 100644 devenv.yaml create mode 100644 lib/response_metrics.ex delete mode 100644 lib/zig_multiplatform_curl.ex diff --git a/.formatter.exs b/.formatter.exs index d2cda26..61c1f6d 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,5 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + plugins: [Zig.Formatter] ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7cb74f..a9edd02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,24 +5,65 @@ on: branches: - master +env: + MIX_ENV: test + jobs: - ci: - runs-on: ubuntu-20.04 - env: - MIX_ENV: test + ci-macos: + runs-on: macos-latest strategy: fail-fast: false matrix: include: - pair: - elixir: "1.11.4" - otp: "23.0" + elixir: "1.14" + otp: "25.3" + zig: "0.13.0" + - pair: + elixir: "1.17" + otp: "27.0" + zig: "0.13.0" + steps: + - uses: actions/checkout@v4 + - name: Generate .tool-versions file + run: | + echo "zig ${{ matrix.pair.zig }}" >> .tool-versions + echo "elixir ${{ matrix.pair.elixir }}" >> .tool-versions + echo "erlang ${{ matrix.pair.otp }}" >> .tool-versions + cp .tool-versions ~/. + cat .tool-versions + - uses: asdf-vm/actions/install@v3 + - name: Install Hex package manager + run: mix local.hex --force && mix local.rebar --force + - run: mix deps.get + - uses: actions/cache@v4 + with: + path: | + ~/.asdf + deps + key: mix-deps-${{ hashFiles('**/mix.lock') }}-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }} + - run: mix test + ci-ubuntu: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: - pair: elixir: "1.14" - otp: "25.0" + otp: "25.3" + - pair: + elixir: "1.15" + otp: "26.2" + - pair: + elixir: "1.16" + otp: "26.2" + - pair: + elixir: "1.17" + otp: "27.0" lint: lint steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install test dependencies run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev krb5-{user,kdc,admin-server} @@ -30,13 +71,17 @@ jobs: - name: Setup Kerberos environment and initialize configuration run: sudo ./test/support/files/setup_kerberos.sh - - uses: erlef/setup-beam@main + - uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.13.0 + + - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.pair.otp}} elixir-version: ${{matrix.pair.elixir}} version-type: strict - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: deps key: mix-deps-${{ hashFiles('**/mix.lock') }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f54fb34 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish + +on: + push: + branches: [master] + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + name: Publish + env: + HEX_API_KEY: ${{secrets.HEX_API_KEY}} + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.0" + elixir-version: "1.17" + - uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.13.0 + - run: mix deps.get + - name: Publish Documentation + run: mix hex.publish docs --yes + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + - name: Publish Package + run: mix hex.publish --yes + if: github.event_name == 'release' && github.event.action == 'published' diff --git a/.gitignore b/.gitignore index bda795f..1a2a054 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ ex_curl-*.tar # Temporary files, for example, from tests. /tmp/ + +lib/.Elixir.*.zig + +# devenv +.devenv* +devenv.local.nix diff --git a/README.md b/README.md index 5c99f88..45681f7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ExCurl [![CI](https://github.com/open-status/ex_curl/actions/workflows/ci.yml/badge.svg)](https://github.com/open-status/ex_curl/actions/workflows/ci.yml) [![Hex.pm](https://img.shields.io/hexpm/v/ex_curl.svg)](https://hex.pm/packages/ex_curl) [![Hexdocs.pm](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_curl/) +# ExCurl [![CI](https://github.com/danielrudn/ex_curl/actions/workflows/ci.yml/badge.svg)](https://github.com/danielrudn/ex_curl/actions/workflows/ci.yml) [![Hex.pm](https://img.shields.io/hexpm/v/ex_curl.svg)](https://hex.pm/packages/ex_curl) [![Hexdocs.pm](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_curl/) Elixir bindings for libcurl using Zig's C interoperability. @@ -9,7 +9,7 @@ The package can be installed by adding `ex_curl` to your list of dependencies in ```elixir def deps do [ - {:ex_curl, "~> 0.2.1"} + {:ex_curl, "~> 0.3.0"} ] end ``` @@ -18,7 +18,7 @@ To install `ex_curl` in a Livebook, you can use `Mix.install/2`: ```elixir Mix.install([ - {:ex_curl, "~> 0.2.1"} + {:ex_curl, "~> 0.3.0"} ]) ``` @@ -30,7 +30,7 @@ ExCurl.get("https://httpbin.org/get") # view request metrics ExCurl.get!("https://httpbin.org/get", return_metrics: true) -# => %ExCurl.Response{status_code: 200, total_time: 0.2, namelookup_time: 0.01, appconnect_time: 0.05, ...}} +# => %ExCurl.Response{status_code: 200, metrics: %ExCurl.ResponseMetrics{total_time: 0.2, ...}, ...}} # submit a form ExCurl.post("https://httpbin.org/post", body: "text=#{URI.encode_www_form("some value")}") diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..49d611a --- /dev/null +++ b/devenv.lock @@ -0,0 +1,139 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1722262342, + "owner": "cachix", + "repo": "devenv", + "rev": "11a1ca0ad80bc172d2efda34ae542494442dcf48", + "treeHash": "c1be883f8fad6adb0369cef0ac6e6c9bd7f3ec66", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "treeHash": "ca14199cabdfe1a06a7b1654c76ed49100a689f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716977621, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", + "treeHash": "6d9f1f7ca0faf1bc2eeb397c78a49623260d3412", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1722221733, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "12bf09802d77264e441f48e25459c10c93eada2e", + "treeHash": "e959ebf2e25b21ec31266bef769b447e4b907916", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1722185531, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "52ec9ac3b12395ad677e8b62106f0b98c1f8569d", + "treeHash": "561285c3e9ff92b7fff8f6111828711f012cb158", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1721042469, + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", + "treeHash": "91f40b7a3b9f6886bd77482cba5b5cd890415a2e", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", + "pre-commit-hooks": "pre-commit-hooks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..6013d80 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,14 @@ +{ pkgs, config, inputs, ... }: +let unstable-pkgs = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; }; +in { + packages = [ + unstable-pkgs.zig_0_13 + unstable-pkgs.elixir_1_17 + pkgs.curl + ]; + + enterTest = '' + mix deps.get + mix test + ''; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..6712569 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + nixpkgs-unstable: + url: github:NixOS/nixpkgs/nixos-unstable + diff --git a/lib/client.ex b/lib/client.ex index 48e8e11..23f9bad 100644 --- a/lib/client.ex +++ b/lib/client.ex @@ -9,7 +9,7 @@ defmodule ExCurl.Client do ```elixir defmodule GitHubClient do - use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"authentication" => "Bearer some-secret-token"}] + use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"Authorization" => "Bearer some-secret-token"}] def user_repos(username), do: get("/users/\#{username}/repos") end @@ -33,7 +33,7 @@ defmodule ExCurl.Client do ```elixir defmodule GitHubClient do - use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"authentication" => "Bearer some-secret-token"}] + use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"Authorization" => "Bearer some-secret-token"}] def handle_response(%ExCurl.Response{body: body}) do case Jason.decode(body) do diff --git a/lib/curl_error_codes.ex b/lib/curl_error_codes.ex index 6dfd7fd..453e2fb 100644 --- a/lib/curl_error_codes.ex +++ b/lib/curl_error_codes.ex @@ -1,6 +1,6 @@ defmodule ExCurl.CurlErrorCodes do @moduledoc """ - Helper functions to map [curl error codes](https://curl.se/libcurl/c/libcurl-errors.html) from their integer value to the string value and vice versa. + Helper functions to map [libcurl error codes](https://curl.se/libcurl/c/libcurl-errors.html) from their integer value to the string value and vice versa. """ @all_error_codes [ diff --git a/lib/ex_curl.ex b/lib/ex_curl.ex index c0d7a80..7faba5e 100644 --- a/lib/ex_curl.ex +++ b/lib/ex_curl.ex @@ -4,7 +4,7 @@ defmodule ExCurl do ## Shared options - * `:headers` - a map of headers to include in the request, defaults to `%{"user-agent" => "curl/7.85.0"}` + * `:headers` - a map of headers to include in the request, defaults to `%{"user-agent" => "ex_curl/0.3.0"}` * `:body` - a string to send as the request body, defaults to `""` * `:follow_location` - if redirects should be followed, defaults to `true` * `:ssl_verifyhost` - if SSL certificates should be verified, defaults to `true` @@ -16,7 +16,7 @@ defmodule ExCurl do ## Error messages - Error messages refer to error codes on the [curl error codes documentation page](https://curl.se/libcurl/c/libcurl-errors.html). + Error messages refer to error codes on the [libcurl error codes documentation page](https://curl.se/libcurl/c/libcurl-errors.html). For example, when we try to send a request to an invalid url: @@ -25,7 +25,7 @@ defmodule ExCurl do {:error, "URL_MALFORMAT"} The returned error tuple includes the error message `"URL_MALFORMAT"`. This corresponds to the `CURLE_URL_MALFORMAT` (error code 3) error listed - in the [curl error codes documentation](https://curl.se/libcurl/c/libcurl-errors.html). + in the [libcurl error codes documentation](https://curl.se/libcurl/c/libcurl-errors.html). """ alias ExCurl.{CurlErrorCodes, Request, RequestConfiguration, Response} @@ -101,9 +101,6 @@ defmodule ExCurl do iex> {:ok, %ExCurl.Response{status_code: status_code}} = ExCurl.get("https://google.com", follow_location: false) iex> status_code 301 - - iex> ExCurl.get("https://\\n\\n") - {:error, "URL_MALFORMAT"} """ def get(url, opts \\ []), do: request("GET", url, opts) @@ -121,9 +118,6 @@ defmodule ExCurl do iex> {:ok, %ExCurl.Response{body: body}} = ExCurl.post("https://httpbin.org/post", body: "some-value=true") iex> Jason.decode!(body)["form"] %{"some-value" => "true"} - - iex> ExCurl.post("https://\\n\\n") - {:error, "URL_MALFORMAT"} """ def post(url, opts \\ []), do: request("POST", url, opts) @@ -172,7 +166,7 @@ defmodule ExCurl do ## Examples - iex> ExCurl.delete("https://httpbin.org/delete", headers: %{"authentication" => "bearer secret"}) + iex> ExCurl.delete("https://httpbin.org/delete", headers: %{"Authorization" => "bearer secret"}) """ def delete(url, opts \\ []), do: request("DELETE", url, opts) @@ -189,15 +183,13 @@ defmodule ExCurl do iex> ExCurl.request("GET", "https://google.com") - - iex> ExCurl.request("POST", "https://google.com") """ def request(method, url, opts \\ []) do RequestConfiguration.build(method, url, opts) |> do_request(opts) |> case do {:ok, resp} -> - {:ok, Response.from_keyword_list_response(resp)} + {:ok, Response.parse(resp)} {:error, error_code} -> {:error, CurlErrorCodes.get_message(error_code)} @@ -226,11 +218,9 @@ defmodule ExCurl do end defp do_request(%RequestConfiguration{} = config, opts) do - json_config = Jason.encode!(config) - case Keyword.get(opts, :dirty_cpu, false) do - true -> Request.request_dirty_cpu(json_config) - _ -> Request.request(json_config) + true -> Request.request_dirty_cpu(config) + _ -> Request.request(config) end end end diff --git a/lib/request.ex b/lib/request.ex index 43d274a..bcc64d6 100644 --- a/lib/request.ex +++ b/lib/request.ex @@ -1,270 +1,240 @@ defmodule ExCurl.Request do @moduledoc false - use ExCurl.Zig.MultiPlatformCurl + use Zig, + otp_app: :ex_curl, + c: [link_lib: {:system, "curl"}], + nifs: [request: [], request_dirty_cpu: [:dirty_cpu]] ~Z""" + const beam = @import("beam"); + const std = @import("std"); const cURL = @cImport({ - @cInclude("curl.h"); + @cInclude("curl/curl.h"); }); - const Header = struct { - key: []u8, - value: []u8 + pub const Header = struct { key: []u8, value: []u8 }; + + pub const RequestFlags = struct { + follow_location: bool, + ssl_verifyhost: bool, + ssl_verifypeer: bool, + return_metrics: bool, + verbose: bool, + http_auth_negotiate: bool, }; - const RequestFlags = struct { - follow_location: bool, - ssl_verifyhost: bool, - ssl_verifypeer: bool, - return_metrics: bool, - verbose: bool, - http_auth_negotiate: bool, + pub const RequestConfiguration = struct { + headers: []Header, + url: []u8, + method: []u8, + body: []u8, + flags: RequestFlags, }; - const RequestConfiguration = struct { - headers: []Header, - url: []u8, - method: []u8, - body: []u8, - flags: RequestFlags, + pub const ResponseMetrics = struct { + namelookup_time: f64, + connect_time: f64, + appconnect_time: f64, + pretransfer_time: f64, + starttransfer_time: f64, + total_time: f64, }; - /// nif: request_dirty_cpu/1 dirty_cpu - fn request_dirty_cpu(env: beam.env, json: []u8) !beam.term { - return request(env, json); - } + pub const Response = struct { + body: []u8, + status_code: u64, + headers: []u8, + metrics: ?ResponseMetrics, + }; - /// nif: request/1 - fn request(env: beam.env, json: []u8) !beam.term { - // initialize curl and vars - var arena_state = std.heap.ArenaAllocator.init(std.heap.c_allocator); - defer arena_state.deinit(); - - const allocator = arena_state.allocator(); - - const handle = cURL.curl_easy_init() orelse return beam.make_error_atom(env, "init_failed"); - defer cURL.curl_easy_cleanup(handle); - - var response_buffer = std.ArrayList(u8).init(allocator); - var headers_buffer = std.ArrayList(u8).init(allocator); - - // superfluous when using an arena allocator, but - // important if the allocator implementation changes - defer response_buffer.deinit(); - defer headers_buffer.deinit(); - - // set curl opts & callbacks - var gpa = std.heap.GeneralPurposeAllocator(.{}){.backing_allocator = allocator}; - var config: RequestConfiguration = try parseRequestConfiguration(gpa.allocator(), json); - defer std.json.parseFree(RequestConfiguration, config, .{ - .allocator = gpa.allocator(), - }); - try setCurlOpts(allocator, handle, response_buffer, headers_buffer, config); - // set headers - var header_slist: [*c]cURL.curl_slist = null; - defer cURL.curl_slist_free_all(header_slist); - for (config.headers) |header| { - var buf = try allocator.alloc(u8, header.key.len + 3 + header.value.len); - _ = try std.fmt.bufPrint(buf, "{s}: {s}\x00", .{ header.key, header.value }); - header_slist = cURL.curl_slist_append(header_slist, buf.ptr); - allocator.free(buf); - } - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK) - unreachable; - - // 3. perform request - var result = cURL.curl_easy_perform(handle); - if (result != cURL.CURLE_OK) - return beam.make_error_term(env, beam.make_u64(env, result)); - - // 4. getinfo and create response - var response_list = try makeKeywordListResponse(env, handle, response_buffer, headers_buffer, config); - return beam.make_ok_term(env, response_list); + pub fn request_dirty_cpu(config: RequestConfiguration) !beam.term { + return request(config); } - fn parseRequestConfiguration(allocator: std.mem.Allocator, json: []u8) !RequestConfiguration { - var stream = std.json.TokenStream.init(json); - var parsedData = try std.json.parse(RequestConfiguration, &stream, .{ - .allocator = allocator, - .ignore_unknown_fields = true, - }); - return parsedData; + pub fn request(config: RequestConfiguration) !beam.term { + // initialize curl and vars + var arena_state = std.heap.ArenaAllocator.init(std.heap.c_allocator); + defer arena_state.deinit(); + + const allocator = arena_state.allocator(); + + const handle = cURL.curl_easy_init() orelse return beam.make_error_pair("init_failed", .{}); + defer cURL.curl_easy_cleanup(handle); + + var response_buffer = std.ArrayList(u8).init(allocator); + var headers_buffer = std.ArrayList(u8).init(allocator); + + // superfluous when using an arena allocator, but + // important if the allocator implementation changes + defer response_buffer.deinit(); + defer headers_buffer.deinit(); + + // set curl opts & callbacks + try setCurlOpts(allocator, handle, config); + // set headers + var header_slist: [*c]cURL.curl_slist = null; + defer cURL.curl_slist_free_all(header_slist); + for (config.headers) |header| { + const buf = try allocator.alloc(u8, header.key.len + 3 + header.value.len); + _ = try std.fmt.bufPrint(buf, "{s}: {s}\x00", .{ header.key, header.value }); + header_slist = cURL.curl_slist_append(header_slist, buf.ptr); + allocator.free(buf); + } + + // Response body callback + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK) + unreachable; + + // Response body callback + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK) + unreachable; + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK) + unreachable; + + // Headers callback + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK) + unreachable; + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERDATA, &headers_buffer) != cURL.CURLE_OK) + unreachable; + + // Request body + if (!std.mem.eql(u8, config.body, "")) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READFUNCTION, readFn) != cURL.CURLE_OK) + unreachable; + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READDATA, &config) != cURL.CURLE_OK) + unreachable; + } + + // 3. perform request + const result = cURL.curl_easy_perform(handle); + if (result != cURL.CURLE_OK) + return beam.make_error_pair(result, .{}); + + // 4. getinfo and create response + const response = try makeResponse(handle, response_buffer, headers_buffer, config); + return beam.make(.{ .ok, response }, .{}); } - fn makeKeywordListResponse(env: beam.env, handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !beam.term { - // metrics - var total_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_TOTAL_TIME_T, &total_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var total_time_term = beam.make_f64(env, total_time); - var total_time_tuple = try makeKeywordListTuple(env, "total_time", total_time_term); - - var namelookup_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_NAMELOOKUP_TIME_T, &namelookup_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var namelookup_time_term = beam.make_f64(env, namelookup_time); - var name_lookup_tuple = try makeKeywordListTuple(env, "namelookup_time", namelookup_time_term); - - var connect_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_CONNECT_TIME_T, &connect_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var connect_time_term = beam.make_f64(env, connect_time); - var connect_time_tuple = try makeKeywordListTuple(env, "connect_time", connect_time_term); - - var appconnect_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_APPCONNECT_TIME_T, &appconnect_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var appconnect_time_term = beam.make_f64(env, appconnect_time); - var appconnect_time_tuple = try makeKeywordListTuple(env, "appconnect_time", appconnect_time_term); - - var pretransfer_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_PRETRANSFER_TIME_T, &pretransfer_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var pretransfer_time_term = beam.make_f64(env, pretransfer_time); - var pretransfer_time_tuple = try makeKeywordListTuple(env, "pretransfer_time", pretransfer_time_term); - - var starttransfer_time: f64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_STARTTRANSFER_TIME_T, &starttransfer_time) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var starttransfer_time_term = beam.make_f64(env, starttransfer_time); - var starttransfer_time_tuple = try makeKeywordListTuple(env, "starttransfer_time", starttransfer_time_term); - - var status_code: u64 = 0; - if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_RESPONSE_CODE, &status_code) != cURL.CURLE_OK) - return error.CURLGETINFO_FAILED; - var status_code_term = beam.make_u64(env, status_code); - var status_code_tuple = try makeKeywordListTuple(env, "status_code", status_code_term); - - var response_body_term = beam.make_slice(env, response_buffer.items); - var response_body_tuple = try makeKeywordListTuple(env, "body", response_body_term); - - var headers_term = beam.make_slice(env, headers_buffer.items); - var headers_tuple = try makeKeywordListTuple(env, "headers", headers_term); - - // response list - var response_tuple_slice: []beam.term = undefined; - if (config.flags.return_metrics) { - response_tuple_slice = try beam.allocator.alloc(beam.term, 10); - response_tuple_slice[0] = response_body_tuple; - response_tuple_slice[1] = total_time_tuple; - response_tuple_slice[2] = name_lookup_tuple; - response_tuple_slice[3] = connect_time_tuple; - response_tuple_slice[4] = appconnect_time_tuple; - response_tuple_slice[5] = pretransfer_time_tuple; - response_tuple_slice[6] = starttransfer_time_tuple; - response_tuple_slice[7] = status_code_tuple; - response_tuple_slice[8] = try makeKeywordListTuple(env, "metrics_returned", beam.make_bool(env, true)); - response_tuple_slice[9] = headers_tuple; - } else { - response_tuple_slice = try beam.allocator.alloc(beam.term, 3); - response_tuple_slice[0] = response_body_tuple; - response_tuple_slice[1] = status_code_tuple; - response_tuple_slice[2] = headers_tuple; - } - defer beam.allocator.free(response_tuple_slice); - - return beam.make_term_list(env, response_tuple_slice); + fn makeResponse(handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !Response { + var status_code: u64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_RESPONSE_CODE, &status_code) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + return .{ + .body = response_buffer.items, + .status_code = status_code, + .headers = headers_buffer.items, + .metrics = if (config.flags.return_metrics) try makeResponseMetrics(handle) else null, + }; } - fn makeKeywordListTuple(env: beam.env, key: []const u8, value: beam.term) !beam.term { - var key_atom = beam.make_atom(env, key); - var tuple_slice: []beam.term = try beam.allocator.alloc(beam.term, 2); - defer beam.allocator.free(tuple_slice); - tuple_slice[0] = key_atom; - tuple_slice[1] = value; - return beam.make_tuple(env, tuple_slice); + fn makeResponseMetrics(handle: *cURL.CURL) !ResponseMetrics { + var total_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_TOTAL_TIME_T, &total_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + var namelookup_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_NAMELOOKUP_TIME_T, &namelookup_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + var connect_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_CONNECT_TIME_T, &connect_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + var appconnect_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_APPCONNECT_TIME_T, &appconnect_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + var pretransfer_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_PRETRANSFER_TIME_T, &pretransfer_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + var starttransfer_time: f64 = 0; + if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_STARTTRANSFER_TIME_T, &starttransfer_time) != cURL.CURLE_OK) + return error.CURLGETINFO_FAILED; + + return .{ + .namelookup_time = namelookup_time, + .connect_time = connect_time, + .appconnect_time = appconnect_time, + .pretransfer_time = pretransfer_time, + .starttransfer_time = starttransfer_time, + .total_time = total_time, + }; } - fn setCurlOpts(allocator: std.mem.Allocator, handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !void { - if (config.flags.verbose) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_VERBOSE), @as(c_long, 1)) != cURL.CURLE_OK) - unreachable; - } - - if (config.flags.follow_location) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_FOLLOWLOCATION), @as(c_long, 1)) != cURL.CURLE_OK) - unreachable; - } - - if (config.flags.ssl_verifypeer) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYPEER), @as(c_long, 1)) != cURL.CURLE_OK) - unreachable; - } else { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYPEER), @as(c_long, 0)) != cURL.CURLE_OK) - unreachable; - } - - if (config.flags.ssl_verifyhost) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYHOST), @as(c_long, 1)) != cURL.CURLE_OK) - unreachable; - } else { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYHOST), @as(c_long, 0)) != cURL.CURLE_OK) - unreachable; - } - - // Set options to support RFC 4559 for SPNEGO-based Kerberos authentication - // when this flag is enabled - if (config.flags.http_auth_negotiate) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_HTTPAUTH), cURL.CURLAUTH_NEGOTIATE) != cURL.CURLE_OK) - unreachable; - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_USERPWD, ":") != cURL.CURLE_OK) - unreachable; - } - - // HTTP Method - if (std.mem.eql(u8, config.method, "POST")) { - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_POST), @as(c_long, 1)) != cURL.CURLE_OK) - unreachable; - } else if (!std.mem.eql(u8, config.method, "GET")) { - var method_as_c_string = std.cstr.addNullByte(allocator, config.method) catch unreachable; - defer allocator.free(method_as_c_string); - if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_CUSTOMREQUEST), method_as_c_string.ptr) != cURL.CURLE_OK) - unreachable; - } - - // URL - var url_as_c_string = std.cstr.addNullByte(allocator, config.url) catch unreachable; - defer allocator.free(url_as_c_string); - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_URL, url_as_c_string.ptr) != cURL.CURLE_OK) - unreachable; - - // Headers callback - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK) - unreachable; - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERDATA, &headers_buffer) != cURL.CURLE_OK) - unreachable; - - // Request body - if (!std.mem.eql(u8, config.body, "")) { - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READFUNCTION, readFn) != cURL.CURLE_OK) - unreachable; - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READDATA, &config) != cURL.CURLE_OK) - unreachable; - } - - // Response body callback - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK) - unreachable; - if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK) - unreachable; + fn setCurlOpts(allocator: std.mem.Allocator, handle: *cURL.CURL, config: RequestConfiguration) !void { + if (config.flags.verbose) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_VERBOSE, @as(c_long, 1)) != cURL.CURLE_OK) + unreachable; + } + + if (config.flags.follow_location) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_FOLLOWLOCATION, @as(c_long, 1)) != cURL.CURLE_OK) + unreachable; + } + + if (config.flags.ssl_verifypeer) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)) != cURL.CURLE_OK) + unreachable; + } else { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)) != cURL.CURLE_OK) + unreachable; + } + + if (config.flags.ssl_verifyhost) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYHOST, @as(c_long, 1)) != cURL.CURLE_OK) + unreachable; + } else { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)) != cURL.CURLE_OK) + unreachable; + } + + // Set options to support RFC 4559 for SPNEGO-based Kerberos authentication + // when this flag is enabled + if (config.flags.http_auth_negotiate) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPAUTH, cURL.CURLAUTH_NEGOTIATE) != cURL.CURLE_OK) + unreachable; + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_USERPWD, ":") != cURL.CURLE_OK) + unreachable; + } + + // HTTP Method + if (std.mem.eql(u8, config.method, "POST")) { + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_POST, @as(c_long, 1)) != cURL.CURLE_OK) + unreachable; + } else if (!std.mem.eql(u8, config.method, "GET")) { + const method_as_c_string = allocator.dupeZ(u8, config.method) catch unreachable; + defer allocator.free(method_as_c_string); + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_CUSTOMREQUEST, method_as_c_string.ptr) != cURL.CURLE_OK) + unreachable; + } + + // URL + const url_as_c_string = allocator.dupeZ(u8, config.url) catch unreachable; + defer allocator.free(url_as_c_string); + if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_URL, url_as_c_string.ptr) != cURL.CURLE_OK) + unreachable; } - fn readFn(dest: [*]u8, size: usize, nmemb: usize, config: *RequestConfiguration) usize { - const bufferSize = size * nmemb; - if (config.body.len > 0) { - const n = std.math.min(config.body.len, bufferSize); - std.mem.copy(u8, dest[0..n], config.body[0..n]); + fn readFn(dest: [*c]u8, size: usize, nmemb: usize, config: *RequestConfiguration) callconv(.C) usize { + const bufferSize = size * nmemb; + + if (config.body.len == 0) { + return 0; // nothing to read + } + + const n = @min(config.body.len, bufferSize); + std.mem.copyForwards(u8, dest[0..n], config.body[0..n]); config.body = config.body[n..]; return n; - } - return 0; } fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint { - var buffer = @intToPtr(*std.ArrayList(u8), @ptrToInt(user_data)); - var typed_data = @intToPtr([*]u8, @ptrToInt(data)); - buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0; - return nmemb * size; + var buffer: *std.ArrayList(u8) = @alignCast(@ptrCast(user_data)); + var typed_data: [*]u8 = @ptrCast(data); + buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0; + return nmemb * size; } """ end diff --git a/lib/request_configuration.ex b/lib/request_configuration.ex index ad8ff3b..ae6297d 100644 --- a/lib/request_configuration.ex +++ b/lib/request_configuration.ex @@ -22,7 +22,7 @@ defmodule ExCurl.RequestConfiguration do end defp get_headers(opts) do - add_header_if_not_exists(opts[:headers] || %{}, "user-agent", "ex_curl/0.2.1") + add_header_if_not_exists(opts[:headers] || %{}, "user-agent", "ex_curl/0.3.0") end defp add_header_if_not_exists(headers, key, value) do diff --git a/lib/response.ex b/lib/response.ex index 7333519..5430fde 100644 --- a/lib/response.ex +++ b/lib/response.ex @@ -4,47 +4,34 @@ defmodule ExCurl.Response do ## Metrics - When the `metrics_returned` field is set to `true`, the following metrics will be included: + When the `return_metrics` option is set to `true` for a request, the `metrics` field will be populated with a [`%ExCurl.ResponseMetrics{}`](https://hexdocs.pm/ex_curl/ExCurl.ResponseMetrics.html) value. - * `total_time` - corresponds to the [CURLINFO_TOTAL_TIME_T option](https://curl.se/libcurl/c/CURLINFO_TOTAL_TIME_T.html). - * `namelookup_time` - corresponds to the [CURLINFO_NAMELOOKUP_TIME_T option](https://curl.se/libcurl/c/CURLINFO_NAMELOOKUP_TIME_T.html). - * `connect_time` - corresponds to the [CURLINFO_CONNECT_TIME_T option](https://curl.se/libcurl/c/CURLINFO_CONNECT_TIME_T.html). - * `appconnect_time` - corresponds to the [CURLINFO_APPCONNECT_TIME_T option](https://curl.se/libcurl/c/CURLINFO_APPCONNECT_TIME_T.html). - * `pretransfer_time` - corresponds to the [CURLINFO_PRETRANSFER_TIME_T option](https://curl.se/libcurl/c/CURLINFO_PRETRANSFER_TIME_T.html). - * `starttransfer_time` - corresponds to the [CURLINFO_STARTTRANSFER_TIME_T option](https://curl.se/libcurl/c/CURLINFO_STARTTRANSFER_TIME_T.html). + For example: + + iex> ExCurl.get!("https://httpbin.org/get", return_metrics: true) + %ExCurl.Response{status_code: 200, metrics: %ExCurl.ResponseMetrics{}, ...} """ defstruct body: "", headers: %{}, status_code: 200, - total_time: 0, - namelookup_time: 0, - connect_time: 0, - appconnect_time: 0, - pretransfer_time: 0, - starttransfer_time: 0, - metrics_returned: false + metrics: nil @type t :: %__MODULE__{ body: nil | String.t(), headers: map(), status_code: integer(), - total_time: float(), - namelookup_time: float(), - connect_time: float(), - appconnect_time: float(), - pretransfer_time: float(), - starttransfer_time: float(), - metrics_returned: bool() + metrics: nil | ExCurl.ResponseMetrics.t() } @doc false - def from_keyword_list_response(list) do - response = struct(__MODULE__, list) + def parse(raw) do + response = struct(__MODULE__, raw) %__MODULE__{ response - | headers: header_string_to_map(response.headers) + | headers: header_string_to_map(response.headers), + metrics: parse_metrics(response.metrics) } end @@ -57,4 +44,8 @@ defmodule ExCurl.Response do |> Stream.map(fn [key, value] -> {key, value} end) |> Enum.into(%{}) end + + defp parse_metrics(nil), do: nil + + defp parse_metrics(metrics) when is_map(metrics), do: struct(ExCurl.ResponseMetrics, metrics) end diff --git a/lib/response_metrics.ex b/lib/response_metrics.ex new file mode 100644 index 0000000..8af0427 --- /dev/null +++ b/lib/response_metrics.ex @@ -0,0 +1,32 @@ +defmodule ExCurl.ResponseMetrics do + @moduledoc """ + Request timing metrics. + + ## Available Fields + + The following metrics are returned when the `return_metrics` option is set to `true`: + + * `total_time` - corresponds to the [CURLINFO_TOTAL_TIME_T option](https://curl.se/libcurl/c/CURLINFO_TOTAL_TIME_T.html). + * `namelookup_time` - corresponds to the [CURLINFO_NAMELOOKUP_TIME_T option](https://curl.se/libcurl/c/CURLINFO_NAMELOOKUP_TIME_T.html). + * `connect_time` - corresponds to the [CURLINFO_CONNECT_TIME_T option](https://curl.se/libcurl/c/CURLINFO_CONNECT_TIME_T.html). + * `appconnect_time` - corresponds to the [CURLINFO_APPCONNECT_TIME_T option](https://curl.se/libcurl/c/CURLINFO_APPCONNECT_TIME_T.html). + * `pretransfer_time` - corresponds to the [CURLINFO_PRETRANSFER_TIME_T option](https://curl.se/libcurl/c/CURLINFO_PRETRANSFER_TIME_T.html). + * `starttransfer_time` - corresponds to the [CURLINFO_STARTTRANSFER_TIME_T option](https://curl.se/libcurl/c/CURLINFO_STARTTRANSFER_TIME_T.html). + + """ + defstruct total_time: 0, + namelookup_time: 0, + connect_time: 0, + appconnect_time: 0, + pretransfer_time: 0, + starttransfer_time: 0 + + @type t :: %__MODULE__{ + total_time: float(), + namelookup_time: float(), + connect_time: float(), + appconnect_time: float(), + pretransfer_time: float(), + starttransfer_time: float() + } +end diff --git a/lib/zig_multiplatform_curl.ex b/lib/zig_multiplatform_curl.ex deleted file mode 100644 index 93940ee..0000000 --- a/lib/zig_multiplatform_curl.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule ExCurl.Zig.MultiPlatformCurl do - @moduledoc """ - This module wraps the `use Zig` macro to allow multiplatform - compilation of Zig code that imports libcurl. The current implementation - naively guesses standard libcurl locations based on the `@load_order` - module attribute. - """ - - @load_order [ - %{ - name: :debian, - lib: "/usr/lib/x86_64-linux-gnu/libcurl.so", - include: "/usr/include/x86_64-linux-gnu/curl" - }, - %{name: :arch, lib: "/usr/lib/libcurl.so", include: "/usr/include/curl"}, - %{ - name: :macos, - lib: "/usr/local/opt/curl/lib/libcurl.dylib", - include: "/usr/local/opt/curl/include/curl" - } - ] - - defmacro __using__(_opts) do - %{lib: lib, include: include} = - Enum.find(@load_order, &(File.exists?(&1[:lib]) && File.exists?(&1[:include]))) - - quote do - use Zig, libs: [unquote(lib)], include: [unquote(include)], link_libc: true - end - end -end diff --git a/mix.exs b/mix.exs index 2ad5bf4..cf82e4e 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,13 @@ defmodule ExCurl.MixProject do use Mix.Project - @source_url "https://github.com/open-status/ex_curl" + @source_url "https://github.com/danielrudn/ex_curl" def project do [ app: :ex_curl, - version: "0.2.1", - elixir: "~> 1.11", + version: "0.3.0", + elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), @@ -29,10 +29,9 @@ defmodule ExCurl.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ex_doc, "0.29.0"}, - {:zigler, "~> 0.9.1", runtime: false}, - {:jason, ">= 1.0.0"}, - {:bypass, "~> 2.0", only: :test} + {:ex_doc, "~> 0.34", only: :dev, runtime: false}, + {:bypass, "~> 2.0", only: :test}, + {:zigler, "~> 0.13", runtime: false} ] end diff --git a/mix.lock b/mix.lock index 05423dd..440de24 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,24 @@ %{ "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, - "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "pegasus": {:hex, :pegasus, "0.2.5", "38123461fe41add54f715ce41f89137a31cd217d353005b057f88b9b67c39b6f", [:mix], [{:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ee80708608807f4447f1da1e6e0ebd9604f5bda4fbe2d4bdd9aa6dd67afde020"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "protoss": {:hex, :protoss, "0.2.1", "fcf437ed65178d6cbf9a600886e3da9f7173697223972f062ee593941c2588b1", [:mix], [], "hexpm", "2261dbdc4d5913ce1e88d1410108d97f21140a118f45f6acc3edc4ecdb952052"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, - "zigler": {:hex, :zigler, "0.9.1", "94ef1f7e522973f62593fc765a4a051eafea6971877fc584a3167801367a0d24", [:mix], [{:ex_doc, "~> 0.23", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "116dbbe5c7b55e1596b593b07eb99514647a3d03f8bca3b56ff0c3718a688b0e"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "zig_get": {:hex, :zig_get, "0.13.1", "0c5ba23e8ed9bfabb22ddee3f728fe382b72db057423956549e71c9a33aed090", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "bb05db6ed83a72e3100ab0110aad0f3c81ea005f9e85f22c63c3527a82257b4f"}, + "zig_parser": {:hex, :zig_parser, "0.4.0", "5230576fcea30c061f08f6053448ad3dc5194a45485065564a7f8047bb351ce9", [:mix], [{:pegasus, "~> 0.2.4", [hex: :pegasus, repo: "hexpm", optional: false]}], "hexpm", "ec54cf14e80a1485e29a80b42756d0421426db81eb9e2630721fd46ab5c21bcb"}, + "zigler": {:hex, :zigler, "0.13.2", "cfd0da56822b28d56793f9bfa8f0cf934be3ab6166e6b4bfc7894f463e417c98", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:protoss, "~> 0.2", [hex: :protoss, repo: "hexpm", optional: false]}, {:zig_get, "0.13.1", [hex: :zig_get, repo: "hexpm", optional: false]}, {:zig_parser, "~> 0.4.0", [hex: :zig_parser, repo: "hexpm", optional: false]}], "hexpm", "9f59053c3ddd7bdc4147117acd33c3fc45f02174659bebcee28b57bd4b8615ec"}, } diff --git a/test/ex_curl_test.exs b/test/ex_curl_test.exs index 3cd6423..4350d43 100644 --- a/test/ex_curl_test.exs +++ b/test/ex_curl_test.exs @@ -66,14 +66,14 @@ defmodule ExCurlTest do {:ok, %ExCurl.Response{} = resp} = ExCurl.TestClient.get("http://localhost:#{bypass.port}/test", return_metrics: true) - assert resp.metrics_returned - assert resp.total_time != 0 - assert resp.namelookup_time != 0 - assert resp.connect_time != 0 + assert resp.metrics != nil + assert resp.metrics.total_time != 0 + assert resp.metrics.namelookup_time != 0 + assert resp.metrics.connect_time != 0 # appconnect is 0 when not using SSL - assert resp.appconnect_time == 0 - assert resp.pretransfer_time != 0 - assert resp.starttransfer_time != 0 + assert resp.metrics.appconnect_time == 0 + assert resp.metrics.pretransfer_time != 0 + assert resp.metrics.starttransfer_time != 0 end test "follows redirects by default", %{bypass: bypass} do diff --git a/test/headers_test.exs b/test/headers_test.exs index 3ac2f47..2cef6c9 100644 --- a/test/headers_test.exs +++ b/test/headers_test.exs @@ -26,7 +26,7 @@ defmodule ExCurl.HeadersTest do test "sends default User-Agent header", %{bypass: bypass} do Bypass.expect(bypass, "GET", "/test", fn conn -> case Plug.Conn.get_req_header(conn, "user-agent") do - [val] when is_binary(val) -> Plug.Conn.send_resp(conn, 200, "OK") + ["ex_curl/" <> _rest] -> Plug.Conn.send_resp(conn, 200, "OK") _ -> Plug.Conn.send_resp(conn, 400, "Missing header") end end)