///|
priv enum CurlDataKind {
  FormData
  RawData
  BinaryData
  UrlEncodedData
}

///|
priv struct CurlDataSpec {
  kind : CurlDataKind
  value : String
}

///|
struct CurlOptions {
  urls : Array[String]
  output_specs : Array[String]
  mut remote_name : Bool
  mut remote_header_name : Bool
  mut silent : Bool
  mut show_error : Bool
  mut fail : Bool
  mut location : Bool
  mut max_redirects : Int
  mut head : Bool
  mut request_method : @netops.HttpMethod?
  headers : Array[@netops.HttpHeader]
  data_specs : Array[CurlDataSpec]
  mut upload_file : String?
  mut retry_count : Int
  mut retry_delay_ms : Int
  mut retry_max_time_ms : Int?
  mut retry_connection_refused : Bool
  mut retry_all_errors : Bool
  mut connect_timeout_ms : Int
  mut max_time_ms : Int?
  mut idle_timeout_ms : Int
  mut insecure : Bool
  mut proxy_url : String?
  mut no_proxy : String?
  mut remove_on_error : Bool
  mut help : Bool
}

///|
suberror CurlUsageError {
  CurlUsageError(String)
}

///|
fn curl_usage() -> String {
  let text =
    #|Usage: curl [options...] 
    #|  -s, --silent                 silent mode
    #|  -S, --show-error             show errors with --silent
    #|  -f, --fail                   fail on HTTP response codes >= 400
    #|  -o, --output           write to file instead of stdout
    #|  -O, --remote-name            use the remote file name
    #|  -J, --remote-header-name     use Content-Disposition with -O
    #|  -L, --location               follow redirects
    #|      --max-redirs        maximum redirects (default 50)
    #|  -I, --head                   show response headers only
    #|  -H, --header 
pass a custom header to the server #| -X, --request specify a supported HTTP method #| -d, --data HTTP POST data #| --data-raw HTTP POST data without @ expansion #| --data-binary HTTP POST data without conversion #| --data-urlencode URL-encode HTTP POST data #| -T, --upload-file stream file as an upload #| --retry retry transient failures #| --retry-delay wait between retries #| --retry-max-time maximum time spent retrying #| --retry-connrefused retry connection refused failures #| --retry-all-errors retry all transfer errors #| --connect-timeout maximum connection setup time #| -m, --max-time maximum time for one transfer #| --idle-timeout extension: inactivity timeout #| -k, --insecure allow insecure TLS connections #| -x, --proxy use this proxy #| --noproxy bypass proxy for matching hosts #| --remove-on-error remove output file after transfer failure #| #|Compatibility limits: HTTP/HTTPS only; -X accepts GET, HEAD, POST, PUT, #|DELETE, CONNECT, OPTIONS, TRACE, and PATCH. Timer values are bounded by #|the runtime millisecond timer range. text } ///| fn curl_nonnegative( value : String, option : String, ) -> Int raise CurlUsageError { let result = @string.parse_int(value) catch { _ => raise CurlUsageError("\{option}: expected a non-negative integer") } if result < 0 { raise CurlUsageError("\{option}: expected a non-negative integer") } result } ///| fn curl_redirect_limit(value : String) -> Int raise CurlUsageError { let result = @string.parse_int(value) catch { _ => raise CurlUsageError( "--max-redirs: expected -1 or a non-negative integer", ) } if result < -1 { raise CurlUsageError("--max-redirs: expected -1 or a non-negative integer") } result } ///| fn curl_seconds(value : String, option : String) -> Int raise CurlUsageError { @cli.parse_idle_timeout(value) catch { _ => raise CurlUsageError("\{option}: expected a positive number of seconds") } } ///| fn curl_seconds_or_zero( value : String, option : String, ) -> Int raise CurlUsageError { let seconds = @string.parse_double(value) catch { _ => raise CurlUsageError( "\{option}: expected a non-negative number of seconds", ) } if seconds == 0.0 { return 0 } if seconds < 0.0 { raise CurlUsageError("\{option}: expected a non-negative number of seconds") } curl_seconds(value, option) } ///| fn curl_retry_seconds( value : String, option : String, ) -> Int raise CurlUsageError { let seconds = @string.parse_int(value) catch { _ => raise CurlUsageError( "\{option}: expected a non-negative integer number of seconds", ) } if seconds < 0 || seconds > 2_147_483 { raise CurlUsageError("\{option}: seconds exceed the supported timer range") } seconds * 1000 } ///| fn curl_header(value : String) -> @netops.HttpHeader raise CurlUsageError { guard value.find(":") is Some(index) && index > 0 else { raise CurlUsageError("--header: invalid header '\{value}'") } @netops.http_header( value[:index].to_owned().trim().to_owned(), value[index + 1:].to_owned().trim().to_owned(), ) } ///| fn default_curl_options() -> CurlOptions { { urls: [], output_specs: [], remote_name: false, remote_header_name: false, silent: false, show_error: false, fail: false, location: false, max_redirects: 50, head: false, request_method: None, headers: [], data_specs: [], upload_file: None, retry_count: 0, retry_delay_ms: 0, retry_max_time_ms: None, retry_connection_refused: false, retry_all_errors: false, connect_timeout_ms: 300_000, max_time_ms: None, idle_timeout_ms: 300_000, insecure: false, proxy_url: None, no_proxy: None, remove_on_error: false, help: false, } } ///| fn parse_curl_options( args : ArrayView[String], ) -> CurlOptions raise CurlUsageError { let parsed = @cli.parse(args, [ @cli.flag("silent", short='s'), @cli.flag("show-error", short='S'), @cli.flag("fail", short='f'), @cli.option("output", short='o'), @cli.flag("remote-name", short='O'), @cli.flag("remote-header-name", short='J'), @cli.flag("location", short='L'), @cli.option("max-redirs"), @cli.flag("head", short='I'), @cli.option("header", short='H'), @cli.option("request", short='X'), @cli.option("data", short='d'), @cli.option("data-raw"), @cli.option("data-binary"), @cli.option("data-urlencode"), @cli.option("upload-file", short='T'), @cli.option("retry"), @cli.option("retry-delay"), @cli.option("retry-max-time"), @cli.flag("retry-connrefused"), @cli.flag("retry-all-errors"), @cli.option("connect-timeout"), @cli.option("max-time", short='m'), @cli.option("idle-timeout"), @cli.flag("insecure", short='k'), @cli.option("proxy", short='x'), @cli.option("noproxy"), @cli.flag("remove-on-error"), @cli.flag("help"), ]) catch { @cli.CliError(option~, message~, ..) => raise CurlUsageError("\{message}: '\{option}'") } let options = default_curl_options() options.urls.append(parsed.operands) options.output_specs.append(parsed.values("output")) options.remote_name = parsed.contains("remote-name") options.remote_header_name = parsed.contains("remote-header-name") options.silent = parsed.contains("silent") options.show_error = parsed.contains("show-error") options.fail = parsed.contains("fail") options.location = parsed.contains("location") options.head = parsed.contains("head") options.retry_connection_refused = parsed.contains("retry-connrefused") options.retry_all_errors = parsed.contains("retry-all-errors") options.insecure = parsed.contains("insecure") options.remove_on_error = parsed.contains("remove-on-error") options.help = parsed.contains("help") if parsed.last_value("max-redirs") is Some(value) { options.max_redirects = curl_redirect_limit(value) } for value in parsed.values("header") { options.headers.push(curl_header(value)) } if parsed.last_value("request") is Some(value) { options.request_method = Some( @netops.parse_http_method(value) catch { _ => raise CurlUsageError("--request: unsupported HTTP method '\{value}'") }, ) } let data_positions : Map[String, Int] = Map([]) for name in parsed.order { if name == "data" || name == "data-raw" || name == "data-binary" || name == "data-urlencode" { let position = data_positions.get(name).unwrap_or(0) let value = parsed.values(name)[position] data_positions[name] = position + 1 options.data_specs.push({ kind: match name { "data" => FormData "data-raw" => RawData "data-binary" => BinaryData _ => UrlEncodedData }, value, }) } } options.upload_file = parsed.last_value("upload-file") if parsed.last_value("retry") is Some(value) { options.retry_count = curl_nonnegative(value, "--retry") } if parsed.last_value("retry-delay") is Some(value) { options.retry_delay_ms = curl_retry_seconds(value, "--retry-delay") } if parsed.last_value("retry-max-time") is Some(value) { let timeout = curl_retry_seconds(value, "--retry-max-time") options.retry_max_time_ms = if timeout == 0 { None } else { Some(timeout) } } if parsed.last_value("connect-timeout") is Some(value) { let timeout = curl_seconds_or_zero(value, "--connect-timeout") options.connect_timeout_ms = if timeout == 0 { 300_000 } else { timeout } } if parsed.last_value("max-time") is Some(value) { let timeout = curl_seconds_or_zero(value, "--max-time") options.max_time_ms = if timeout == 0 { None } else { Some(timeout) } } if parsed.last_value("idle-timeout") is Some(value) { options.idle_timeout_ms = curl_seconds(value, "--idle-timeout") } options.proxy_url = parsed.last_value("proxy") options.no_proxy = parsed.last_value("noproxy") if options.data_specs.length() > 0 && options.upload_file is Some(_) { raise CurlUsageError("--data and --upload-file are mutually exclusive") } if options.remote_header_name && !options.remote_name { raise CurlUsageError("--remote-header-name requires --remote-name") } options }