diff --git a/.scalafmt.conf b/.scalafmt.conf index 3b9e943..3cb83a2 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,6 @@ version = "3.8.2" runner.dialect = scala3 +rewrite.scala3.insertEndMarkerMinLines = 5 +rewrite.scala3.removeOptionalBraces = true +rewrite.scala3.convertToNewSyntax = true + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b74beb1 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# smithy4s-curl + + + +- [smithy4s-curl](#smithy4s-curl) + - [Installation](#installation) + - [Getting started](#getting-started) + - [Contributing](#contributing) + + +An _experimental_ [Smithy4s](https://disneystreaming.github.io/smithy4s/) client backend for [Scala Native](https://www.scala-native.org/), using Curl directly, without introducing a http4s/cats dependency. + +The purpose of the library is to provide a synchronous client implementation for smithy4s services, suitable for usage in CLIs where introducing http4s/cats dependency just to interact with a smithy4s service is undesirable. + +The library is currently only available for Scala 3, but we will welcome contributions cross-compiling it to 2.13 – it should be very easy. API surface is very minimal and designed for binary compatible evolution, so after initial round of testing and gathering community feedback, we plan to release 1.0.0 and start checking binary/Tasty compatibility for each release. + +Additionally, the library is currently published for Scala Native 0.4, but only for as long as smithy4s core stays on SN 0.4 – once it's published for SN 0.5, this library will jump straight to that. + +## Installation + +Latest version: [![smithy4s-curl Scala version support](https://index.scala-lang.org/neandertech/smithy4s-curl/smithy4s-curl/latest.svg)](https://index.scala-lang.org/neandertech/smithy4s-curl/smithy4s-curl) + +- **SBT**: `libraryDependencies += "tech.neander" %%% "smithy4s-curl" % ""` +- **Scala CLI**: `//> using dep tech.neander::smithy4s-curl::` +- **Mill**: `ivy"tech.neander::smithy4s-curl::"` + +## Getting started + +For example's sake, let's say we have a smithy4s service that models one of the endpoints from https://httpbin.org, defined using [smithy4s-deriving](https://github.com/neandertech/smithy4s-deriving) (note we're using [Scala CLI](https://scala-cli.virtuslab.org) for this demo): + +```scala +//> using dep "tech.neander::smithy4s-deriving::0.0.2" +//> using platform scala-native +//> using scala 3.4.2 +//> using option -Wunused:all + +import scala.annotation.experimental +import smithy4s.*, deriving.{given, *}, aliases.* + +case class Response(headers: Map[String, String], origin: String, url: String) + derives Schema + +@experimental +trait HttpbinService derives API: + @readonly + @httpGet("/get") + def get(): Response +``` + +***Note** that we only need to use `@experimental` annotation because we are using smithy4s-deriving.* +*If you're creating clients for services generated by standard Smithy4s codegen, just remove all `@experimental` annotations* +*you see.* + +To create a Curl client for this service all we need to do is this: + +```scala +import smithy4s_curl.* + +@main @experimental +def helloWorld = + val service: HttpbinService = + SimpleRestJsonCurlClient( + API.service[HttpbinService], + "https://httpbin.org" + ).make.unliftService + + println(service.get()) +``` + +Note that you need to have Curl library enabled, and a `-lcurl` flag added to the Scala Native linking flags + +## Contributing + +If you see something that can be improved in this library – please contribute! + +This is a relatively standard Scala CLI project, even though the tests run a +Scala version newer than the library itself (library is published for 3.3, tests are +in 3.4, to make use of smithy4s-deriving). + +Here are some useful commands: + +- `make test` – run tests +- `make check-docs` – verify that snippets in `README.md` (this file) compile +- `make pre-ci` – format the code so that it passes CI check +- `make run-example` – run example from README against real https://httpbin.org diff --git a/curl.scala b/curl.scala index aa453b9..bc56b53 100644 --- a/curl.scala +++ b/curl.scala @@ -37,7 +37,7 @@ object enumerations: val CURL_FORMADD_DISABLED = define(7) val CURL_FORMADD_LAST = define(8) inline def getName(inline value: CURLFORMcode): Option[String] = - inline value match + value match case CURL_FORMADD_OK => Some("CURL_FORMADD_OK") case CURL_FORMADD_MEMORY => Some("CURL_FORMADD_MEMORY") case CURL_FORMADD_OPTION_TWICE => Some("CURL_FORMADD_OPTION_TWICE") @@ -68,7 +68,7 @@ object enumerations: val CURLHE_BAD_ARGUMENT = define(6) val CURLHE_NOT_BUILT_IN = define(7) inline def getName(inline value: CURLHcode): Option[String] = - inline value match + value match case CURLHE_OK => Some("CURLHE_OK") case CURLHE_BADINDEX => Some("CURLHE_BADINDEX") case CURLHE_MISSING => Some("CURLHE_MISSING") @@ -163,7 +163,7 @@ object enumerations: val CURLINFO_CONN_ID = define(6291520) val CURLINFO_LASTONE = define(64) inline def getName(inline value: CURLINFO): Option[String] = - inline value match + value match case CURLINFO_NONE => Some("CURLINFO_NONE") case CURLINFO_EFFECTIVE_URL => Some("CURLINFO_EFFECTIVE_URL") case CURLINFO_RESPONSE_CODE => Some("CURLINFO_RESPONSE_CODE") @@ -253,7 +253,7 @@ object enumerations: val CURLMSG_DONE = define(1) val CURLMSG_LAST = define(2) inline def getName(inline value: CURLMSG): Option[String] = - inline value match + value match case CURLMSG_NONE => Some("CURLMSG_NONE") case CURLMSG_DONE => Some("CURLMSG_DONE") case CURLMSG_LAST => Some("CURLMSG_LAST") @@ -285,7 +285,7 @@ object enumerations: val CURLM_UNRECOVERABLE_POLL = define(12) val CURLM_LAST = define(13) inline def getName(inline value: CURLMcode): Option[String] = - inline value match + value match case CURLM_CALL_MULTI_PERFORM => Some("CURLM_CALL_MULTI_PERFORM") case CURLM_OK => Some("CURLM_OK") case CURLM_BAD_HANDLE => Some("CURLM_BAD_HANDLE") @@ -331,7 +331,7 @@ object enumerations: val CURLMOPT_MAX_CONCURRENT_STREAMS = define(16) val CURLMOPT_LASTENTRY = define(17) inline def getName(inline value: CURLMoption): Option[String] = - inline value match + value match case CURLMOPT_SOCKETFUNCTION => Some("CURLMOPT_SOCKETFUNCTION") case CURLMOPT_SOCKETDATA => Some("CURLMOPT_SOCKETDATA") case CURLMOPT_PIPELINING => Some("CURLMOPT_PIPELINING") @@ -369,7 +369,7 @@ object enumerations: val CURLSHE_NOT_BUILT_IN = define(5) val CURLSHE_LAST = define(6) inline def getName(inline value: CURLSHcode): Option[String] = - inline value match + value match case CURLSHE_OK => Some("CURLSHE_OK") case CURLSHE_BAD_OPTION => Some("CURLSHE_BAD_OPTION") case CURLSHE_IN_USE => Some("CURLSHE_IN_USE") @@ -397,7 +397,7 @@ object enumerations: val CURLSHOPT_USERDATA = define(5) val CURLSHOPT_LAST = define(6) inline def getName(inline value: CURLSHoption): Option[String] = - inline value match + value match case CURLSHOPT_NONE => Some("CURLSHOPT_NONE") case CURLSHOPT_SHARE => Some("CURLSHOPT_SHARE") case CURLSHOPT_UNSHARE => Some("CURLSHOPT_UNSHARE") @@ -421,7 +421,7 @@ object enumerations: val CURLSTS_DONE = define(1) val CURLSTS_FAIL = define(2) inline def getName(inline value: CURLSTScode): Option[String] = - inline value match + value match case CURLSTS_OK => Some("CURLSTS_OK") case CURLSTS_DONE => Some("CURLSTS_DONE") case CURLSTS_FAIL => Some("CURLSTS_FAIL") @@ -449,7 +449,7 @@ object enumerations: val CURLUPART_FRAGMENT = define(9) val CURLUPART_ZONEID = define(10) inline def getName(inline value: CURLUPart): Option[String] = - inline value match + value match case CURLUPART_URL => Some("CURLUPART_URL") case CURLUPART_SCHEME => Some("CURLUPART_SCHEME") case CURLUPART_USER => Some("CURLUPART_USER") @@ -506,7 +506,7 @@ object enumerations: val CURLUE_LACKS_IDN = define(30) val CURLUE_LAST = define(31) inline def getName(inline value: CURLUcode): Option[String] = - inline value match + value match case CURLUE_OK => Some("CURLUE_OK") case CURLUE_BAD_HANDLE => Some("CURLUE_BAD_HANDLE") case CURLUE_BAD_PARTPOINTER => Some("CURLUE_BAD_PARTPOINTER") @@ -556,7 +556,7 @@ object enumerations: val CURL_NETRC_REQUIRED = define(2) val CURL_NETRC_LAST = define(3) inline def getName(inline value: CURL_NETRC_OPTION): Option[String] = - inline value match + value match case CURL_NETRC_IGNORED => Some("CURL_NETRC_IGNORED") case CURL_NETRC_OPTIONAL => Some("CURL_NETRC_OPTIONAL") case CURL_NETRC_REQUIRED => Some("CURL_NETRC_REQUIRED") @@ -577,7 +577,7 @@ object enumerations: val CURL_TLSAUTH_SRP = define(1) val CURL_TLSAUTH_LAST = define(2) inline def getName(inline value: CURL_TLSAUTH): Option[String] = - inline value match + value match case CURL_TLSAUTH_NONE => Some("CURL_TLSAUTH_NONE") case CURL_TLSAUTH_SRP => Some("CURL_TLSAUTH_SRP") case CURL_TLSAUTH_LAST => Some("CURL_TLSAUTH_LAST") @@ -694,8 +694,8 @@ object enumerations: val CURLE_SSL_CLIENTCERT = define(98) val CURLE_UNRECOVERABLE_POLL = define(99) val CURL_LAST = define(100) - inline def getName(inline value: CURLcode): Option[String] = - inline value match + def getName(value: CURLcode): Option[String] = + value match case CURLE_OK => Some("CURLE_OK") case CURLE_UNSUPPORTED_PROTOCOL => Some("CURLE_UNSUPPORTED_PROTOCOL") case CURLE_FAILED_INIT => Some("CURLE_FAILED_INIT") @@ -832,7 +832,7 @@ object enumerations: val CURLFORM_CONTENTLEN = define(20) val CURLFORM_LASTENTRY = define(21) inline def getName(inline value: CURLformoption): Option[String] = - inline value match + value match case CURLFORM_NOTHING => Some("CURLFORM_NOTHING") case CURLFORM_COPYNAME => Some("CURLFORM_COPYNAME") case CURLFORM_PTRNAME => Some("CURLFORM_PTRNAME") @@ -1174,7 +1174,7 @@ object enumerations: val CURLOPT_HAPROXY_CLIENT_IP = define(10323) val CURLOPT_LASTENTRY = define(10324) inline def getName(inline value: CURLoption): Option[String] = - inline value match + value match case CURLOPT_WRITEDATA => Some("CURLOPT_WRITEDATA") case CURLOPT_URL => Some("CURLOPT_URL") case CURLOPT_PORT => Some("CURLOPT_PORT") @@ -1529,7 +1529,7 @@ object enumerations: val CURLPX_USER_REJECTED = define(33) val CURLPX_LAST = define(34) inline def getName(inline value: CURLproxycode): Option[String] = - inline value match + value match case CURLPX_OK => Some("CURLPX_OK") case CURLPX_BAD_ADDRESS_TYPE => Some("CURLPX_BAD_ADDRESS_TYPE") case CURLPX_BAD_VERSION => Some("CURLPX_BAD_VERSION") @@ -1582,7 +1582,7 @@ object enumerations: val CURLSSLSET_TOO_LATE = define(2) val CURLSSLSET_NO_BACKENDS = define(3) inline def getName(inline value: CURLsslset): Option[String] = - inline value match + value match case CURLSSLSET_OK => Some("CURLSSLSET_OK") case CURLSSLSET_UNKNOWN_BACKEND => Some("CURLSSLSET_UNKNOWN_BACKEND") case CURLSSLSET_TOO_LATE => Some("CURLSSLSET_TOO_LATE") @@ -1613,7 +1613,7 @@ object enumerations: val CURLVERSION_ELEVENTH = define(10) val CURLVERSION_LAST = define(11) inline def getName(inline value: CURLversion): Option[String] = - inline value match + value match case CURLVERSION_FIRST => Some("CURLVERSION_FIRST") case CURLVERSION_SECOND => Some("CURLVERSION_SECOND") case CURLVERSION_THIRD => Some("CURLVERSION_THIRD") @@ -1644,7 +1644,7 @@ object enumerations: val CURL_TIMECOND_LASTMOD = define(3) val CURL_TIMECOND_LAST = define(4) inline def getName(inline value: curl_TimeCond): Option[String] = - inline value match + value match case CURL_TIMECOND_NONE => Some("CURL_TIMECOND_NONE") case CURL_TIMECOND_IFMODSINCE => Some("CURL_TIMECOND_IFMODSINCE") case CURL_TIMECOND_IFUNMODSINCE => Some("CURL_TIMECOND_IFUNMODSINCE") @@ -1670,7 +1670,7 @@ object enumerations: val CURLCLOSEPOLICY_CALLBACK = define(5) val CURLCLOSEPOLICY_LAST = define(6) inline def getName(inline value: curl_closepolicy): Option[String] = - inline value match + value match case CURLCLOSEPOLICY_NONE => Some("CURLCLOSEPOLICY_NONE") case CURLCLOSEPOLICY_OLDEST => Some("CURLCLOSEPOLICY_OLDEST") case CURLCLOSEPOLICY_LEAST_RECENTLY_USED => Some("CURLCLOSEPOLICY_LEAST_RECENTLY_USED") @@ -1700,7 +1700,7 @@ object enumerations: val CURLOT_BLOB = define(7) val CURLOT_FUNCTION = define(8) inline def getName(inline value: curl_easytype): Option[String] = - inline value match + value match case CURLOT_LONG => Some("CURLOT_LONG") case CURLOT_VALUES => Some("CURLOT_VALUES") case CURLOT_OFF_T => Some("CURLOT_OFF_T") @@ -1727,7 +1727,7 @@ object enumerations: val CURLFTPAUTH_TLS = define(2) val CURLFTPAUTH_LAST = define(3) inline def getName(inline value: curl_ftpauth): Option[String] = - inline value match + value match case CURLFTPAUTH_DEFAULT => Some("CURLFTPAUTH_DEFAULT") case CURLFTPAUTH_SSL => Some("CURLFTPAUTH_SSL") case CURLFTPAUTH_TLS => Some("CURLFTPAUTH_TLS") @@ -1749,7 +1749,7 @@ object enumerations: val CURLFTPSSL_CCC_ACTIVE = define(2) val CURLFTPSSL_CCC_LAST = define(3) inline def getName(inline value: curl_ftpccc): Option[String] = - inline value match + value match case CURLFTPSSL_CCC_NONE => Some("CURLFTPSSL_CCC_NONE") case CURLFTPSSL_CCC_PASSIVE => Some("CURLFTPSSL_CCC_PASSIVE") case CURLFTPSSL_CCC_ACTIVE => Some("CURLFTPSSL_CCC_ACTIVE") @@ -1771,7 +1771,7 @@ object enumerations: val CURLFTP_CREATE_DIR_RETRY = define(2) val CURLFTP_CREATE_DIR_LAST = define(3) inline def getName(inline value: curl_ftpcreatedir): Option[String] = - inline value match + value match case CURLFTP_CREATE_DIR_NONE => Some("CURLFTP_CREATE_DIR_NONE") case CURLFTP_CREATE_DIR => Some("CURLFTP_CREATE_DIR") case CURLFTP_CREATE_DIR_RETRY => Some("CURLFTP_CREATE_DIR_RETRY") @@ -1794,7 +1794,7 @@ object enumerations: val CURLFTPMETHOD_SINGLECWD = define(3) val CURLFTPMETHOD_LAST = define(4) inline def getName(inline value: curl_ftpmethod): Option[String] = - inline value match + value match case CURLFTPMETHOD_DEFAULT => Some("CURLFTPMETHOD_DEFAULT") case CURLFTPMETHOD_MULTICWD => Some("CURLFTPMETHOD_MULTICWD") case CURLFTPMETHOD_NOCWD => Some("CURLFTPMETHOD_NOCWD") @@ -1821,7 +1821,7 @@ object enumerations: val CURLINFO_SSL_DATA_OUT = define(6) val CURLINFO_END = define(7) inline def getName(inline value: curl_infotype): Option[String] = - inline value match + value match case CURLINFO_TEXT => Some("CURLINFO_TEXT") case CURLINFO_HEADER_IN => Some("CURLINFO_HEADER_IN") case CURLINFO_HEADER_OUT => Some("CURLINFO_HEADER_OUT") @@ -1847,7 +1847,7 @@ object enumerations: val CURLKHMATCH_MISSING = define(2) val CURLKHMATCH_LAST = define(3) inline def getName(inline value: curl_khmatch): Option[String] = - inline value match + value match case CURLKHMATCH_OK => Some("CURLKHMATCH_OK") case CURLKHMATCH_MISMATCH => Some("CURLKHMATCH_MISMATCH") case CURLKHMATCH_MISSING => Some("CURLKHMATCH_MISSING") @@ -1871,7 +1871,7 @@ object enumerations: val CURLKHSTAT_FINE_REPLACE = define(4) val CURLKHSTAT_LAST = define(5) inline def getName(inline value: curl_khstat): Option[String] = - inline value match + value match case CURLKHSTAT_FINE_ADD_TO_FILE => Some("CURLKHSTAT_FINE_ADD_TO_FILE") case CURLKHSTAT_FINE => Some("CURLKHSTAT_FINE") case CURLKHSTAT_REJECT => Some("CURLKHSTAT_REJECT") @@ -1897,7 +1897,7 @@ object enumerations: val CURLKHTYPE_ECDSA = define(4) val CURLKHTYPE_ED25519 = define(5) inline def getName(inline value: curl_khtype): Option[String] = - inline value match + value match case CURLKHTYPE_UNKNOWN => Some("CURLKHTYPE_UNKNOWN") case CURLKHTYPE_RSA1 => Some("CURLKHTYPE_RSA1") case CURLKHTYPE_RSA => Some("CURLKHTYPE_RSA") @@ -1921,7 +1921,7 @@ object enumerations: val CURL_LOCK_ACCESS_SINGLE = define(2) val CURL_LOCK_ACCESS_LAST = define(3) inline def getName(inline value: curl_lock_access): Option[String] = - inline value match + value match case CURL_LOCK_ACCESS_NONE => Some("CURL_LOCK_ACCESS_NONE") case CURL_LOCK_ACCESS_SHARED => Some("CURL_LOCK_ACCESS_SHARED") case CURL_LOCK_ACCESS_SINGLE => Some("CURL_LOCK_ACCESS_SINGLE") @@ -1949,7 +1949,7 @@ object enumerations: val CURL_LOCK_DATA_HSTS = define(7) val CURL_LOCK_DATA_LAST = define(8) inline def getName(inline value: curl_lock_data): Option[String] = - inline value match + value match case CURL_LOCK_DATA_NONE => Some("CURL_LOCK_DATA_NONE") case CURL_LOCK_DATA_SHARE => Some("CURL_LOCK_DATA_SHARE") case CURL_LOCK_DATA_COOKIE => Some("CURL_LOCK_DATA_COOKIE") @@ -1980,7 +1980,7 @@ object enumerations: val CURLPROXY_SOCKS4A = define(6) val CURLPROXY_SOCKS5_HOSTNAME = define(7) inline def getName(inline value: curl_proxytype): Option[String] = - inline value match + value match case CURLPROXY_HTTP => Some("CURLPROXY_HTTP") case CURLPROXY_HTTP_1_0 => Some("CURLPROXY_HTTP_1_0") case CURLPROXY_HTTPS => Some("CURLPROXY_HTTPS") @@ -2017,7 +2017,7 @@ object enumerations: val CURLSSLBACKEND_BEARSSL = define(13) val CURLSSLBACKEND_RUSTLS = define(14) inline def getName(inline value: curl_sslbackend): Option[String] = - inline value match + value match case CURLSSLBACKEND_NONE => Some("CURLSSLBACKEND_NONE") case CURLSSLBACKEND_OPENSSL => Some("CURLSSLBACKEND_OPENSSL") case CURLSSLBACKEND_GNUTLS => Some("CURLSSLBACKEND_GNUTLS") @@ -2051,7 +2051,7 @@ object enumerations: val CURLUSESSL_ALL = define(3) val CURLUSESSL_LAST = define(4) inline def getName(inline value: curl_usessl): Option[String] = - inline value match + value match case CURLUSESSL_NONE => Some("CURLUSESSL_NONE") case CURLUSESSL_TRY => Some("CURLUSESSL_TRY") case CURLUSESSL_CONTROL => Some("CURLUSESSL_CONTROL") @@ -2079,7 +2079,7 @@ object enumerations: val CURLFILETYPE_DOOR = define(7) val CURLFILETYPE_UNKNOWN = define(8) inline def getName(inline value: curlfiletype): Option[String] = - inline value match + value match case CURLFILETYPE_FILE => Some("CURLFILETYPE_FILE") case CURLFILETYPE_DIRECTORY => Some("CURLFILETYPE_DIRECTORY") case CURLFILETYPE_SYMLINK => Some("CURLFILETYPE_SYMLINK") @@ -2105,7 +2105,7 @@ object enumerations: val CURLIOCMD_RESTARTREAD = define(1) val CURLIOCMD_LAST = define(2) inline def getName(inline value: curliocmd): Option[String] = - inline value match + value match case CURLIOCMD_NOP => Some("CURLIOCMD_NOP") case CURLIOCMD_RESTARTREAD => Some("CURLIOCMD_RESTARTREAD") case CURLIOCMD_LAST => Some("CURLIOCMD_LAST") @@ -2126,7 +2126,7 @@ object enumerations: val CURLIOE_FAILRESTART = define(2) val CURLIOE_LAST = define(3) inline def getName(inline value: curlioerr): Option[String] = - inline value match + value match case CURLIOE_OK => Some("CURLIOE_OK") case CURLIOE_UNKNOWNCMD => Some("CURLIOE_UNKNOWNCMD") case CURLIOE_FAILRESTART => Some("CURLIOE_FAILRESTART") @@ -2147,7 +2147,7 @@ object enumerations: val CURLSOCKTYPE_ACCEPT = define(1) val CURLSOCKTYPE_LAST = define(2) inline def getName(inline value: curlsocktype): Option[String] = - inline value match + value match case CURLSOCKTYPE_IPCXN => Some("CURLSOCKTYPE_IPCXN") case CURLSOCKTYPE_ACCEPT => Some("CURLSOCKTYPE_ACCEPT") case CURLSOCKTYPE_LAST => Some("CURLSOCKTYPE_LAST") @@ -3790,4 +3790,4 @@ object all: export _root_.curl.functions.curl_version_info export _root_.curl.functions.curl_ws_meta export _root_.curl.functions.curl_ws_recv - export _root_.curl.functions.curl_ws_send \ No newline at end of file + export _root_.curl.functions.curl_ws_send diff --git a/curl_macros.scala b/curl_macros.scala index 1d9c400..39d9410 100644 --- a/curl_macros.scala +++ b/curl_macros.scala @@ -4,19 +4,53 @@ import scala.quoted.* import curl.enumerations.CURLcode import curl.all.curl_easy_strerror import scalanative.unsafe.fromCString +import curl.enumerations.CURLUcode +import curl.all.curl_url_strerror + +case class CurlException(code: CURLcode, msg: String) + extends Exception( + s"Curl error: ${CURLcode.getName(code).getOrElse("")}($code): $msg" + ) inline def check(inline expr: => CURLcode): CURLcode = ${ checkImpl('expr) } private def checkImpl(expr: Expr[CURLcode])(using Quotes): Expr[CURLcode] = import quotes.* - val e = Expr(s"${expr.show} failed: ") '{ val code = $expr - assert( - code == CURLcode.CURLE_OK, - $e + "[" + fromCString(curl_easy_strerror(code)) + "]" - ) + + if code != CURLcode.CURLE_OK then + throw new CurlException( + code, + fromCString(curl_easy_strerror(code)) + ) + end if + code } end checkImpl + +case class CurlUrlParseException(code: CURLUcode, msg: String) + extends Exception( + s"Curl URL parsing error: ${CURLUcode.getName(code).getOrElse("")}($code): $msg" + ) + +inline def checkU(inline expr: => CURLUcode): CURLUcode = ${ checkUImpl('expr) } + +private def checkUImpl(expr: Expr[CURLUcode])(using Quotes): Expr[CURLUcode] = + import quotes.* + val e = Expr(s"${expr.show} failed: ") + + '{ + val code = $expr + + if code != CURLUcode.CURLUE_OK then + throw new CurlUrlParseException( + code, + fromCString(curl_url_strerror(code)) + ) + end if + code + } +end checkUImpl diff --git a/smithy4s-curl.scala b/smithy4s-curl.scala index 6d61cc2..cbc7521 100644 --- a/smithy4s-curl.scala +++ b/smithy4s-curl.scala @@ -29,18 +29,27 @@ import util.chaining.* import curl.all as C import scala.collection.mutable.ArrayBuilder import scala.scalanative.libc.string +import scala.scalanative.unsigned.* + +class SyncCurlClient private ( + private var valid: Boolean, + CURL: Ptr[curl.all.CURL] +) extends AutoCloseable: + override def close(): Unit = + C.curl_easy_cleanup(CURL); valid = false -class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { import curl.all.CURLoption.* def send(request: smithy4s.http.HttpRequest[Blob]): Try[HttpResponse[Blob]] = + assert(valid, "This client has already been shut down and cannot be used!") + val finalizers = Seq.newBuilder[() => Unit] finalizers += (() => C.curl_easy_reset(CURL)) Zone: implicit z => - try { - for { + try + for _ <- setMethod(request) _ <- setURL(request) @@ -84,16 +93,16 @@ class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { headers = headerLines.flatMap(parseHeaders).groupMap(_._1)(_._2) code <- getCode() - } yield HttpResponse( + yield HttpResponse( code, headers, - Blob.apply(bodyBuilder.result()) + Blob.apply(bodyBuilder.result().tap(arr => new String(arr))) ) - } finally { + finally finalizers.result().foreach { fin => fin() } - } + end send private def parseHeaders(str: String): Seq[(CaseInsensitive, String)] = val array = str @@ -172,6 +181,7 @@ class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { } } slist + end makeHeaders private def setHeaders( handle: Ptr[C.CURL], @@ -188,6 +198,7 @@ class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { ), () => C.curl_slist_free_all(slist) ) + end setHeaders private def setMethod(request: HttpRequest[Blob])(using Zone): Try[Unit] = Try: @@ -209,16 +220,12 @@ class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { private inline def OPT[T](opt: curl.all.CURLoption, value: T) = check(curl.all.curl_easy_setopt(CURL, opt, value)) - // private inline def OPTry[T](opt: curl.all.CURLoption, value: T) = - // Try(check(curl.all.curl_easy_setopt(CURL, opt, value))) - - def fromSmithy4sHttpUri(uri: smithy4s.http.HttpUri): String = { + def fromSmithy4sHttpUri(uri: smithy4s.http.HttpUri): String = val qp = uri.queryParams - val newValue = { + val newValue = uri.scheme match case Http => "http" case Https => "https" - } val hostName = uri.host val port = uri.port @@ -241,17 +248,16 @@ class SyncCurlClient private (CURL: Ptr[curl.all.CURL]) { do if i == 0 then b += "=" + value else b += s"&$key=$value" + end for b s"$newValue://$hostName$port$path$query" - } + end fromSmithy4sHttpUri +end SyncCurlClient -} - -object SyncCurlClient { - def apply() = new SyncCurlClient(curl.all.curl_easy_init()) -} +object SyncCurlClient: + def apply() = new SyncCurlClient(valid = true, curl.all.curl_easy_init()) class SimpleRestJsonCurlClient[ Alg[_[_, _, _, _, _]] @@ -261,7 +267,7 @@ class SimpleRestJsonCurlClient[ client: SyncCurlClient, middleware: Endpoint.Middleware[SyncCurlClient], codecs: SimpleRestJsonCodecs -) { +): def withMaxArity(maxArity: Int): SimpleRestJsonCurlClient[Alg] = changeCodecs(_.copy(maxArity = maxArity)) @@ -304,10 +310,9 @@ class SimpleRestJsonCurlClient[ middleware, f(codecs) ) +end SimpleRestJsonCurlClient -} - -object SimpleRestJsonCurlClient { +object SimpleRestJsonCurlClient: def apply[Alg[_[_, _, _, _, _]]]( service: smithy4s.Service[Alg], @@ -323,13 +328,12 @@ object SimpleRestJsonCurlClient { ) private def lowLevelClient(fetch: SyncCurlClient) = - new UnaryLowLevelClient[Try, HttpRequest[Blob], HttpResponse[Blob]] { + new UnaryLowLevelClient[Try, HttpRequest[Blob], HttpResponse[Blob]]: override def run[Output](request: HttpRequest[Blob])( responseCB: HttpResponse[Blob] => Try[Output] ): Try[Output] = fetch.send(request).flatMap(responseCB) - } -} +end SimpleRestJsonCurlClient private[smithy4s_curl] object SimpleRestJsonCodecs extends SimpleRestJsonCodecs(1024, false, false) @@ -338,104 +342,80 @@ private[smithy4s_curl] case class SimpleRestJsonCodecs( maxArity: Int, explicitDefaultsEncoding: Boolean, hostPrefixInjection: Boolean -) { +): private val hintMask = alloy.SimpleRestJson.protocol.hintMask - // def unsafeFromSmithy4sHttpMethod( - // method: smithy4s.http.HttpMethod - // ): org.scalajs.dom.HttpMethod = - // import smithy4s.http.HttpMethod.* - // import org.scalajs.dom.HttpMethod as FetchMethod - // method match - // case GET => FetchMethod.GET - // case PUT => FetchMethod.PUT - // case POST => FetchMethod.POST - // case DELETE => FetchMethod.DELETE - // case PATCH => FetchMethod.PATCH - // case OTHER(nm) => nm.asInstanceOf[FetchMethod] - - // def toHeaders(smithyHeaders: Map[CaseInsensitive, Seq[String]]): Headers = { - - // val h = new Headers() - - // smithyHeaders.foreach { case (name, values) => - // values.foreach { value => - // h.append(name.toString, value) - // } - // } - - // h - // } - - // def toSmithy4sHttpResponse( - // resp: Response - // ): Promise[smithy4s.http.HttpResponse[Blob]] = { - // resp - // .arrayBuffer() - // .`then`: body => - // val headers = Map.newBuilder[CaseInsensitive, Seq[String]] - - // resp.headers.foreach: - // case arr if arr.size >= 2 => - // val header = arr(0) - // val values = arr.tail.toSeq - // headers += CaseInsensitive(header) -> values - // case _ => - - // smithy4s.http.HttpResponse( - // resp.status, - // headers.result(), - // Blob(new Int8Array(body).toArray) - // ) - - // } - - // def fromSmithy4sHttpRequest( - // req: smithy4s.http.HttpRequest[Blob] - // ): Request = { - // val m = unsafeFromSmithy4sHttpMethod(req.method) - // val h = toHeaders(req.headers) - // val ri = new RequestInit {} - // if (req.body.size != 0) { - // val arr = new Int8Array(req.body.size) - // arr.set( - // req.body.toArray.toJSArray, - // 0 - // ) - // ri.body = arr - // h.append("Content-Length", req.body.size.toString) - // } - - // ri.method = m - // ri.headers = h - - // new Request(fromSmithy4sHttpUri(req.uri), ri) - // } + object StackZone extends Zone: + + override inline def alloc(size: CSize): Ptr[Byte] = stackalloc[Byte](size) + + override def close(): Unit = () + + override def isClosed: Boolean = false + end StackZone def toSmithy4sHttpUri( uri: String, pathParams: Option[smithy4s.http.PathParams] = None - ): smithy4s.http.HttpUri = { - // import smithy4s.http.* - // val uriScheme = uri.protocol match { - // case "https:" => HttpUriScheme.Https - // case "http:" => HttpUriScheme.Http - // case _ => - // throw UnsupportedOperationException( - // s"Protocol `${uri.protocol}` is not supported" - // ) - // } - - HttpUri( - HttpUriScheme.Https, - "httpbin.org", - None, - IndexedSeq.empty, - Map.empty, - pathParams - ) - } + ): smithy4s.http.HttpUri = + + import C.CURLUPart.* + import C.CURLUcode.* + + Zone: + implicit z => + + val url = C.curl_url() + + checkU( + C.curl_url_set( + url, + CURLUPART_URL, + toCString(uri), + 0.toUInt + ) + ) + + def getPart(part: C.CURLUPart): String = + val scheme = stackalloc[Ptr[Byte]](1) + + checkU(C.curl_url_get(url, part, scheme, 0.toUInt)) + + val str = fromCString(!scheme) + + C.curl_free(!scheme) + + str + end getPart + + val httpScheme = getPart(CURLUPART_SCHEME) match + case "https" => HttpUriScheme.Https + case "http" => HttpUriScheme.Http + case other => + throw UnsupportedOperationException( + s"Protocol `${other}` is not supported" + ) + + val port = Try(getPart(CURLUPART_PORT)) match + case Failure(CurlUrlParseException(CURLUE_NO_PORT, _)) => + None + case Success(value) => Some(value.toInt) + + case Failure(other) => throw other + + val host = getPart(CURLUPART_HOST) + val path = getPart(CURLUPART_PATH).split("/").dropWhile(_.isEmpty()) + + HttpUri( + httpScheme, + host, + port, + path, + Map.empty, + pathParams + ) + end toSmithy4sHttpUri val jsonCodecs = Json.payloadCodecs .withJsoniterCodecCompiler( @@ -457,7 +437,7 @@ private[smithy4s_curl] case class SimpleRestJsonCodecs( def makeClientCodecs( uri: String - ): UnaryClientCodecs.Make[Try, HttpRequest[Blob], HttpResponse[Blob]] = { + ): UnaryClientCodecs.Make[Try, HttpRequest[Blob], HttpResponse[Blob]] = val baseRequest = HttpRequest( HttpMethod.POST, toSmithy4sHttpUri(uri, None), @@ -484,9 +464,8 @@ private[smithy4s_curl] case class SimpleRestJsonCodecs( .withResponseTransformation[HttpResponse[Blob]](Success(_)) .withHostPrefixInjection(hostPrefixInjection) .build() - - } -} + end makeClientCodecs +end SimpleRestJsonCodecs given MonadThrowLike[Try] with def flatMap[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = fa.flatMap(f) @@ -495,7 +474,6 @@ given MonadThrowLike[Try] with case Failure(exception) => f(exception) case _ => fa - // fa.transform(identity, f) def pure[A](a: A): Try[A] = Success(a) def raiseError[A](e: Throwable): Try[A] = Failure(e) def zipMapAll[A](seq: IndexedSeq[Try[Any]])(f: IndexedSeq[Any] => A): Try[A] = @@ -503,12 +481,19 @@ given MonadThrowLike[Try] with b.sizeHint(seq.size) var failure: Throwable = null - seq.foreach { - case Failure(exception) => failure = exception - case Success(value) => if failure == null then b += value - } + var i = 0 + + while failure == null && i < seq.length do + seq(i) match + case Failure(exception) => failure = exception + case Success(value) => if failure == null then b += value + + i += 1 + end while if failure != null then Failure(failure) else Try(f(b.result())) + end zipMapAll +end given import smithy4s_curl.* import httpbin.* @@ -521,3 +506,4 @@ import httpbin.* ).make println(smithyClient.anything(25, Some("This is a test!"))) +end hello