lib/request.ex

defmodule ExCurl.Request do
  @moduledoc false
  use ExCurl.Zig.MultiPlatformCurl

  ~Z"""
  const cURL = @cImport({
    @cInclude("curl.h");
  });

  const Header = struct {
    key: []u8,
    value: []u8
  };

  const RequestFlags = struct {
    follow_location: bool,
    ssl_verifyhost: bool,
    ssl_verifypeer: bool,
    return_metrics: bool,
    verbose: bool,
  };

  const RequestConfiguration = struct {
    headers: []Header,
    url: []u8,
    method: []u8,
    body: []u8,
    flags: RequestFlags,
  };

  /// nif: request_dirty_cpu/1 dirty_cpu
  fn request_dirty_cpu(env: beam.env, json: []u8) !beam.term {
    return request(env, json);
  }

  /// 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);
  }

  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;
  }

  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 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 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;
    }

    // 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 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]);
      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;
  }
  """
end