Skip to content

Commit f5188de

Browse files
committed
Rework build and add tests
1 parent 2f156b4 commit f5188de

22 files changed

+953
-626
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ jobs:
2323
- name: Check formatting
2424
run: make code-check || echo "Run `make pre-ci`"
2525

26+
- name: Cache vcpkg
27+
uses: actions/cache@v3
28+
with:
29+
path: |
30+
~/Library/Caches/sbt-vcpkg/vcpkg-install
31+
~/.cache/sbt-vcpkg/vcpkg-install
32+
~/.cache/sbt-vcpkg/vcpkg
33+
key: ${{ runner.os }}-sbt-vcpkg
34+
2635
- name: Test
27-
run: make test
36+
run: make tests
2837

2938
- name: Check documentation compiles and runs
3039
run: make check-docs && make run-example

.scalafix.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
OrganizeImports.groupedImports = Merge
22
OrganizeImports.targetDialect = Scala3
3-
OrganizeImports.removeUnused = false
3+
OrganizeImports.removeUnused = true

Makefile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
check-docs:
2-
scala-cli compile README.md smithy4s-fetch.scala project.scala
2+
# scala-cli compile README.md *.scala
3+
echo "No supported yet"
34

4-
test:
5-
scala-cli test .
5+
tests:
6+
cs launch sn-vcpkg --contrib -- scala-cli curl s2n openssl zlib --rename curl=libcurl -- test .
67

78
publish-snapshot:
89
scala-cli config publish.credentials s01.oss.sonatype.org env:SONATYPE_USERNAME env:SONATYPE_PASSWORD
@@ -17,7 +18,12 @@ code-check:
1718
scala-cli fmt . --check
1819

1920
run-example:
20-
scala-cli run README.md project.scala smithy4s-fetch.scala -M helloWorld
21+
scala-cli run README.md . -M helloWorld
2122

2223
pre-ci:
2324
scala-cli fmt .
25+
26+
smithy4s:
27+
cd test && \
28+
rm -rf httpbin && \
29+
cs launch smithy4s --contrib -- generate httpbin.smithy --skip resource --skip openapi

MonadThrowLikeTry.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package smithy4s_curl
2+
3+
import smithy4s.capability.MonadThrowLike
4+
import scala.util.*
5+
6+
given MonadThrowLike[Try] with
7+
def flatMap[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = fa.flatMap(f)
8+
def handleErrorWith[A](fa: Try[A])(f: Throwable => Try[A]): Try[A] =
9+
fa match
10+
case Failure(exception) => f(exception)
11+
case _ => fa
12+
13+
def pure[A](a: A): Try[A] = Success(a)
14+
def raiseError[A](e: Throwable): Try[A] = Failure(e)
15+
def zipMapAll[A](seq: IndexedSeq[Try[Any]])(f: IndexedSeq[Any] => A): Try[A] =
16+
val b = IndexedSeq.newBuilder[Any]
17+
b.sizeHint(seq.size)
18+
var failure: Throwable = null
19+
20+
var i = 0
21+
22+
while failure == null && i < seq.length do
23+
seq(i) match
24+
case Failure(exception) => failure = exception
25+
case Success(value) => if failure == null then b += value
26+
27+
i += 1
28+
end while
29+
30+
if failure != null then Failure(failure) else Try(f(b.result()))
31+
end zipMapAll
32+
end given

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ Latest version: [![smithy4s-curl Scala version support](https://index.scala-lang
2929
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):
3030

3131
```scala
32-
//> using dep "tech.neander::smithy4s-deriving::0.0.2"
32+
//> using dep "tech.neander::smithy4s-deriving::0.0.0-SNAPSHOT"
3333
//> using platform scala-native
3434
//> using scala 3.4.2
3535
//> using option -Wunused:all
3636

3737
import scala.annotation.experimental
3838
import smithy4s.*, deriving.{given, *}, aliases.*
39+
import scala.util.Try
3940

4041
case class Response(headers: Map[String, String], origin: String, url: String)
4142
derives Schema
@@ -44,7 +45,7 @@ case class Response(headers: Map[String, String], origin: String, url: String)
4445
trait HttpbinService derives API:
4546
@readonly
4647
@httpGet("/get")
47-
def get(): Response
48+
def get(): Try[Response]
4849
```
4950

5051
***Note** that we only need to use `@experimental` annotation because we are using smithy4s-deriving.*
@@ -58,10 +59,11 @@ import smithy4s_curl.*
5859

5960
@main @experimental
6061
def helloWorld =
61-
val service: HttpbinService =
62+
val service =
6263
SimpleRestJsonCurlClient(
6364
API.service[HttpbinService],
64-
"https://httpbin.org"
65+
"https://httpbin.org",
66+
SyncCurlClient()
6567
).make.unliftService
6668

6769
println(service.get())

SimpleRestJsonCodecs.scala

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package smithy4s_curl
2+
3+
import curl.all as C
4+
import smithy4s.Blob
5+
import smithy4s.client.*
6+
import smithy4s.codecs.BlobEncoder
7+
import smithy4s.http.HttpUriScheme.{Http, Https}
8+
import smithy4s.http.{
9+
HttpDiscriminator,
10+
HttpMethod,
11+
HttpRequest,
12+
HttpResponse,
13+
HttpUnaryClientCodecs,
14+
HttpUri,
15+
HttpUriScheme,
16+
Metadata
17+
}
18+
import smithy4s.json.Json
19+
import smithy4s_curl.*
20+
21+
import scala.scalanative.unsigned.*
22+
import scala.util.{Failure, Success, Try}
23+
24+
import scalanative.unsafe.*
25+
import util.chaining.*
26+
27+
private[smithy4s_curl] object SimpleRestJsonCodecs
28+
extends SimpleRestJsonCodecs(1024, false, false)
29+
30+
private[smithy4s_curl] case class SimpleRestJsonCodecs(
31+
maxArity: Int,
32+
explicitDefaultsEncoding: Boolean,
33+
hostPrefixInjection: Boolean
34+
):
35+
private val hintMask =
36+
alloy.SimpleRestJson.protocol.hintMask
37+
38+
def fromSmithy4sHttpUri(uri: smithy4s.http.HttpUri): String =
39+
val qp = uri.queryParams
40+
val newValue =
41+
uri.scheme match
42+
case Http => "http"
43+
case Https => "https"
44+
val hostName = uri.host
45+
val port =
46+
uri.port
47+
.filterNot(p => uri.host.endsWith(s":$p"))
48+
.map(":" + _.toString)
49+
.getOrElse("")
50+
51+
val path = "/" + uri.path.mkString("/")
52+
val query =
53+
if qp.isEmpty then ""
54+
else
55+
var b = "?"
56+
qp.zipWithIndex.map:
57+
case ((key, values), idx) =>
58+
if idx != 0 then b += "&"
59+
b += key
60+
for
61+
i <- 0 until values.length
62+
value = values(i)
63+
do
64+
if i == 0 then b += "=" + value
65+
else b += s"&$key=$value"
66+
end for
67+
68+
b
69+
70+
s"$newValue://$hostName$port$path$query"
71+
end fromSmithy4sHttpUri
72+
73+
def toSmithy4sHttpUri(
74+
uri: String,
75+
pathParams: Option[smithy4s.http.PathParams] = None
76+
): smithy4s.http.HttpUri =
77+
78+
import C.CURLUPart.*
79+
import C.CURLUcode.*
80+
81+
Zone:
82+
implicit z =>
83+
84+
val url = C.curl_url()
85+
86+
checkU(
87+
C.curl_url_set(
88+
url,
89+
CURLUPART_URL,
90+
toCString(uri),
91+
0.toUInt
92+
)
93+
)
94+
95+
def getPart(part: C.CURLUPart): String =
96+
val scheme = stackalloc[Ptr[Byte]](1)
97+
98+
checkU(C.curl_url_get(url, part, scheme, 0.toUInt))
99+
100+
val str = fromCString(!scheme)
101+
102+
C.curl_free(!scheme)
103+
104+
str
105+
end getPart
106+
107+
val httpScheme = getPart(CURLUPART_SCHEME) match
108+
case "https" => HttpUriScheme.Https
109+
case "http" => HttpUriScheme.Http
110+
case other =>
111+
throw UnsupportedOperationException(
112+
s"Protocol `${other}` is not supported"
113+
)
114+
115+
val port = Try(getPart(CURLUPART_PORT)) match
116+
case Failure(CurlUrlParseException(CURLUE_NO_PORT, _)) =>
117+
None
118+
case Success(value) => Some(value.toInt)
119+
120+
case Failure(other) => throw other
121+
122+
val host = getPart(CURLUPART_HOST)
123+
val path = getPart(CURLUPART_PATH)
124+
125+
val cleanedPath: IndexedSeq[String] =
126+
path.tail
127+
// drop the guaranteed leading slash, so that we don't produce an empty segment for it
128+
.tail
129+
// splitting an empty path would produce a single element, so we special-case to empty
130+
.match
131+
case "" => IndexedSeq.empty[String]
132+
case other => other.split("/")
133+
134+
HttpUri(
135+
httpScheme,
136+
host,
137+
port,
138+
cleanedPath,
139+
Map.empty,
140+
pathParams
141+
)
142+
end toSmithy4sHttpUri
143+
144+
val jsonCodecs = Json.payloadCodecs
145+
.withJsoniterCodecCompiler(
146+
Json.jsoniter
147+
.withHintMask(hintMask)
148+
.withMaxArity(maxArity)
149+
.withExplicitDefaultsEncoding(explicitNulls = true)
150+
)
151+
152+
val payloadEncoders: BlobEncoder.Compiler =
153+
jsonCodecs.encoders
154+
155+
val payloadDecoders =
156+
jsonCodecs.decoders
157+
158+
val errorHeaders = List(
159+
smithy4s.http.errorTypeHeader
160+
)
161+
162+
def makeClientCodecs(
163+
uri: String
164+
): UnaryClientCodecs.Make[Try, HttpRequest[Blob], HttpResponse[Blob]] =
165+
val baseRequest = HttpRequest(
166+
HttpMethod.POST,
167+
toSmithy4sHttpUri(uri, None),
168+
Map.empty,
169+
Blob.empty
170+
)
171+
172+
HttpUnaryClientCodecs.builder
173+
.withBodyEncoders(payloadEncoders)
174+
.withSuccessBodyDecoders(payloadDecoders)
175+
.withErrorBodyDecoders(payloadDecoders)
176+
.withErrorDiscriminator(resp =>
177+
Success(HttpDiscriminator.fromResponse(errorHeaders, resp))
178+
)
179+
.withMetadataDecoders(Metadata.Decoder)
180+
.withMetadataEncoders(
181+
Metadata.Encoder.withExplicitDefaultsEncoding(
182+
explicitDefaultsEncoding
183+
)
184+
)
185+
.withBaseRequest(_ => Success(baseRequest))
186+
.withRequestMediaType("application/json")
187+
.withRequestTransformation[HttpRequest[Blob]](Success(_))
188+
.withResponseTransformation[HttpResponse[Blob]](Success(_))
189+
.withHostPrefixInjection(hostPrefixInjection)
190+
.build()
191+
end makeClientCodecs
192+
end SimpleRestJsonCodecs

0 commit comments

Comments
 (0)