Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support scala native #2104

Merged
merged 6 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target-platform: [ "JVM", "JS" ]
target-platform: [ "JVM", "JS", "Native" ]
steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -35,6 +35,11 @@ jobs:
unzip -q aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install --update
sam --version
- name: Install libidn11-dev
if: matrix.target-platform == 'Native'
run: |
sudo apt-get update
sudo apt-get install libidn11-dev
- name: Compile
run: sbt -v Test/compile compileDocumentation
- name: Test JVM 2.12
Expand All @@ -55,6 +60,9 @@ jobs:
- name: Test Scala.js
if: matrix.target-platform == 'JS'
run: sbt coreJS/test coreJS2_12/test catsJS/test catsJS2_12/test enumeratumJS/test enumeratumJS2_12/test refinedJS/test refinedJS2_12/test circeJsonJS/test circeJsonJS2_12/test playJsonJS/test playJsonJS2_12/test uPickleJsonJS/test uPickleJsonJS2_12/test jsoniterScalaJS/test jsoniterScalaJS2_12/test sttpClientJS/test sttpClientJS2_12/test
- name: Test Native
if: matrix.target-platform == 'Native'
run: sbt -v testNative
- name: Check MiMa # disable for major releases
if: matrix.target-platform == 'JVM'
run: sbt -v mimaReportBinaryIssues
Expand Down Expand Up @@ -117,6 +125,11 @@ jobs:
~/.ivy2/cache
~/.coursier
key: sbt-cache-${{ runner.os }}-JVM-${{ hashFiles('project/build.properties') }}
- name: Install libidn11-dev
if: matrix.target-platform == 'Native'
run: |
sudo apt-get update
sudo apt-get install libidn11-dev
- name: Compile
run: sbt compile
- name: Publish artifacts
Expand Down
43 changes: 30 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ val scala2And3Versions = scala2Versions ++ List(scala3)
val codegenScalaVersions = List(scala2_12)
val examplesScalaVersions = List(scala2_13)
val documentationScalaVersion = scala2_13
val nativeScalaVersions = List(scala2_13)

lazy val clientTestServerPort = settingKey[Int]("Port to run the client interpreter test server on")
lazy val startClientTestServer = taskKey[Unit]("Start a http server used by client interpreter tests")
Expand Down Expand Up @@ -96,6 +97,8 @@ val commonJvmSettings: Seq[Def.Setting[_]] = commonSettings ++ Seq(
// run JS tests inside Gecko, due to jsdom not supporting fetch and to avoid having to install node
val commonJsSettings = commonSettings ++ browserGeckoTestSettings

val commonNativeSettings = commonSettings

def dependenciesFor(version: String)(deps: (Option[(Long, Long)] => ModuleID)*): Seq[ModuleID] =
deps.map(_.apply(CrossVersion.partialVersion(version)))

Expand Down Expand Up @@ -186,6 +189,7 @@ val testJVM_2_12 = taskKey[Unit]("Test JVM Scala 2.12 projects, without Finatra"
val testJVM_2_13 = taskKey[Unit]("Test JVM Scala 2.13 projects, without Finatra")
val testJVM_3 = taskKey[Unit]("Test JVM Scala 3 projects, without Finatra")
val testJS = taskKey[Unit]("Test JS projects")
val testNative = taskKey[Unit]("Test native projects")
val testDocs = taskKey[Unit]("Test docs projects")
val testServers = taskKey[Unit]("Test server projects")
val testClients = taskKey[Unit]("Test client projects")
Expand Down Expand Up @@ -224,12 +228,19 @@ lazy val rootProject = (project in file("."))
.settings(
publishArtifact := false,
name := "tapir",
testJVM_2_12 := (Test / test).all(filterProject(p => !p.contains("JS") && !p.contains("finatra") && p.contains("2_12"))).value,
testJVM_2_12 := (Test / test)
.all(filterProject(p => !p.contains("JS") && !p.contains("Native") && !p.contains("finatra") && p.contains("2_12")))
.value,
testJVM_2_13 := (Test / test)
.all(filterProject(p => !p.contains("JS") && !p.contains("finatra") && !p.contains("2_12") && !p.contains("3")))
.all(
filterProject(p => !p.contains("JS") && !p.contains("Native") && !p.contains("finatra") && !p.contains("2_12") && !p.contains("3"))
)
.value,
testJVM_3 := (Test / test)
.all(filterProject(p => !p.contains("JS") && !p.contains("Native") && !p.contains("finatra") && p.contains("3")))
.value,
testJVM_3 := (Test / test).all(filterProject(p => !p.contains("JS") && !p.contains("finatra") && p.contains("3"))).value,
testJS := (Test / test).all(filterProject(_.contains("JS"))).value,
testNative := (Test / test).all(filterProject(_.contains("Native"))).value,
testDocs := (Test / test).all(filterProject(p => p.contains("Docs") || p.contains("openapi") || p.contains("asyncapi"))).value,
testServers := (Test / test).all(filterProject(p => p.contains("Server"))).value,
testClients := (Test / test).all(filterProject(p => p.contains("Client"))).value,
Expand Down Expand Up @@ -295,8 +306,7 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core"))
"com.softwaremill.sttp.shared" %%% "ws" % Versions.sttpShared,
scalaTest.value % Test,
scalaCheck.value % Test,
scalaTestPlusScalaCheck.value % Test,
"com.47deg" %%% "scalacheck-toolbox-datetime" % "0.6.0" % Test
scalaTestPlusScalaCheck.value % Test
),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down Expand Up @@ -331,6 +341,17 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core"))
)
)
)
.nativePlatform(
scalaVersions = nativeScalaVersions,
settings = {
commonNativeSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.nativeScalaJavaTime,
"io.github.cquiroz" %%% "scala-java-time-tzdb" % Versions.nativeScalaJavaTime % Test
)
)
}
)
//.enablePlugins(spray.boilerplate.BoilerplatePlugin)

lazy val testing: ProjectMatrix = (projectMatrix in file("testing"))
Expand All @@ -341,6 +362,7 @@ lazy val testing: ProjectMatrix = (projectMatrix in file("testing"))
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(scalaVersions = scala2And3Versions, settings = commonJsSettings)
.nativePlatform(scalaVersions = nativeScalaVersions, settings = commonNativeSettings)
.dependsOn(core)

lazy val tests: ProjectMatrix = (projectMatrix in file("tests"))
Expand Down Expand Up @@ -929,14 +951,9 @@ lazy val serverCore: ProjectMatrix = (projectMatrix in file("server/core"))
libraryDependencies ++= Seq(scalaTest.value % Test)
)
.dependsOn(core % CompileAndTest)
.jvmPlatform(
scalaVersions = scala2And3Versions,
settings = commonJvmSettings
)
.jsPlatform(
scalaVersions = scala2And3Versions,
settings = commonJsSettings
)
.jvmPlatform(scalaVersions = scala2And3Versions, settings = commonJvmSettings)
.jsPlatform(scalaVersions = scala2And3Versions, settings = commonJsSettings)
.nativePlatform(scalaVersions = nativeScalaVersions, settings = commonNativeSettings)

lazy val serverTests: ProjectMatrix = (projectMatrix in file("server/tests"))
.settings(commonJvmSettings)
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/scalanative/sttp/tapir/CodecExtensions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sttp.tapir

import sttp.tapir.Codec.fileRange
import sttp.tapir.CodecFormat.OctetStream

import java.nio.file.Path

trait CodecExtensions {
implicit lazy val path: Codec[FileRange, Path, OctetStream] = fileRange.map(d => d.file.toPath)(e => FileRange(e.toFile))
}
8 changes: 8 additions & 0 deletions core/src/main/scalanative/sttp/tapir/Defaults.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package sttp.tapir

import java.io.File

object Defaults {
def createTempFile: () => TapirFile = () => File.createTempFile("tapir", "tmp")
def deleteFile(): TapirFile => Unit = file => file.delete()
}
12 changes: 12 additions & 0 deletions core/src/main/scalanative/sttp/tapir/TapirExtensions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sttp.tapir

import java.nio.file.Path

trait TapirExtensions {
type TapirFile = java.io.File
def pathBody: EndpointIO.Body[FileRange, Path] = binaryBody(RawBodyType.FileBody)[Path]
}

object TapirFile {
def name(f: TapirFile): String = f.getName
}
34 changes: 34 additions & 0 deletions core/src/main/scalanative/sttp/tapir/internal/UrlencodedData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sttp.tapir.internal

import sttp.model.internal.Rfc3986

import java.net.{URLDecoder, URLEncoder}
import java.nio.charset.Charset

private[tapir] object UrlencodedData {
def decode(s: String, charset: Charset): Seq[(String, String)] = {
s.split("&")
.toList
.flatMap(kv =>
kv.split("=", 2) match {
case Array(k, v) =>
Some((URLDecoder.decode(k, charset.toString), URLDecoder.decode(v, charset.toString)))
case _ => None
}
)
}

def encode(s: Seq[(String, String)], charset: Charset): String = {
s.map { case (k, v) =>
s"${URLEncoder.encode(k, charset.toString)}=${URLEncoder.encode(v, charset.toString)}"
}.mkString("&")
}

def encode(s: String): String = {
URLEncoder.encode(s, "UTF-8")
}

def encodePathSegment(s: String): String = {
Rfc3986.encode(Rfc3986.PathSegment)(s)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package sttp.tapir.static

trait TapirStaticContentEndpoints
136 changes: 3 additions & 133 deletions core/src/test/scala/sttp/tapir/CodecTest.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package sttp.tapir

import com.fortysevendeg.scalacheck.datetime.jdk8.ArbitraryJdk8.{arbLocalDateJdk8, arbLocalDateTimeJdk8, genZonedDateTimeWithZone}
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.Assertion
import org.scalatest.flatspec.AnyFlatSpec
Expand All @@ -14,54 +13,24 @@ import sttp.tapir.DecodeResult.Value
import java.math.{BigDecimal => JBigDecimal}
import java.nio.charset.StandardCharsets
import java.time._
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.{Date, UUID}
import scala.concurrent.duration.{Duration => SDuration}
import scala.reflect.ClassTag

// see also CodecTestDateTime
class CodecTest extends AnyFlatSpec with Matchers with Checkers {

implicit val arbitraryUri: Arbitrary[Uri] = Arbitrary(for {
private implicit val arbitraryUri: Arbitrary[Uri] = Arbitrary(for {
scheme <- Gen.alphaLowerStr if scheme.nonEmpty
host <- Gen.identifier.map(_.take(5)) // schemes may not be too long
port <- Gen.option[Int](Gen.chooseNum(1, Short.MaxValue))
path <- Gen.listOfN(5, Gen.identifier)
query <- Gen.mapOf(Gen.zip(Gen.identifier, Gen.identifier))
} yield uri"$scheme://$host:$port/${path.mkString("/")}?$query")

implicit val arbitraryJBigDecimal: Arbitrary[JBigDecimal] = Arbitrary(
private implicit val arbitraryJBigDecimal: Arbitrary[JBigDecimal] = Arbitrary(
implicitly[Arbitrary[BigDecimal]].arbitrary.map(bd => new JBigDecimal(bd.toString))
)

implicit val arbitraryZonedDateTime: Arbitrary[ZonedDateTime] = Arbitrary(
genZonedDateTimeWithZone(Some(ZoneOffset.ofHoursMinutes(3, 30)))
)

implicit val arbitraryOffsetDateTime: Arbitrary[OffsetDateTime] =
Arbitrary(arbitraryZonedDateTime.arbitrary.map(_.toOffsetDateTime))

implicit val arbitraryInstant: Arbitrary[Instant] = Arbitrary(Gen.posNum[Long].map(Instant.ofEpochMilli))

implicit val arbitraryDuration: Arbitrary[Duration] =
Arbitrary(for {
instant1 <- arbitraryInstant.arbitrary
instant2 <- arbitraryInstant.arbitrary.suchThat(_.isAfter(instant1))
} yield Duration.between(instant1, instant2))

implicit val arbitraryScalaDuration: Arbitrary[SDuration] =
Arbitrary(arbitraryDuration.arbitrary.map(d => SDuration.fromNanos(d.toNanos): SDuration))

implicit val arbitraryZoneOffset: Arbitrary[ZoneOffset] = Arbitrary(
arbitraryOffsetDateTime.arbitrary.map(_.getOffset)
)

implicit val arbitraryLocalTime: Arbitrary[LocalTime] = Arbitrary(Gen.chooseNum[Long](0, 86399999999999L).map(LocalTime.ofNanoOfDay))

implicit val arbitraryOffsetTime: Arbitrary[OffsetTime] = Arbitrary(arbitraryOffsetDateTime.arbitrary.map(_.toOffsetTime))

val localDateTimeCodec: Codec[String, LocalDateTime, TextPlain] = implicitly[Codec[String, LocalDateTime, TextPlain]]

it should "encode simple types using .toString" in {
checkEncodeDecodeToString[String]
checkEncodeDecodeToString[Byte]
Expand All @@ -74,105 +43,6 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers {
checkEncodeDecodeToString[Uri]
checkEncodeDecodeToString[BigDecimal]
checkEncodeDecodeToString[JBigDecimal]
checkEncodeDecodeToString[Duration]
checkEncodeDecodeToString[SDuration]
}

// Because there is no separate standard for local date time, we encode it WITH timezone set to "Z"
// https://swagger.io/docs/specification/data-models/data-types/#string
it should "encode LocalDateTime with timezone" in {
check((ldt: LocalDateTime) => localDateTimeCodec.encode(ldt) == OffsetDateTime.of(ldt, ZoneOffset.UTC).toString)
}

it should "decode LocalDateTime from string with timezone" in {
check((zdt: ZonedDateTime) =>
localDateTimeCodec.decode(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zdt)) == Value(zdt.toLocalDateTime)
)
}

it should "correctly encode and decode ZonedDateTime" in {
val codec = implicitly[Codec[String, ZonedDateTime, TextPlain]]
codec.encode(ZonedDateTime.of(LocalDateTime.of(2010, 9, 22, 14, 32, 1), ZoneOffset.ofHours(5))) shouldBe "2010-09-22T14:32:01+05:00"
codec.encode(ZonedDateTime.of(LocalDateTime.of(2010, 9, 22, 14, 32, 1), ZoneOffset.UTC)) shouldBe "2010-09-22T14:32:01Z"
check { (zdt: ZonedDateTime) =>
val encoded = codec.encode(zdt)
codec.decode(encoded) == Value(zdt) && ZonedDateTime.parse(encoded) == zdt
}
}

it should "correctly encode and decode OffsetDateTime" in {
val codec = implicitly[Codec[String, OffsetDateTime, TextPlain]]
codec.encode(OffsetDateTime.of(LocalDateTime.of(2019, 12, 31, 23, 59, 14), ZoneOffset.ofHours(5))) shouldBe "2019-12-31T23:59:14+05:00"
codec.encode(OffsetDateTime.of(LocalDateTime.of(2020, 9, 22, 14, 32, 1), ZoneOffset.ofHours(0))) shouldBe "2020-09-22T14:32:01Z"
check { (odt: OffsetDateTime) =>
val encoded = codec.encode(odt)
codec.decode(encoded) == Value(odt) && OffsetDateTime.parse(encoded) == odt
}
}

it should "correctly encode and decode Instant" in {
val codec = implicitly[Codec[String, Instant, TextPlain]]
codec.encode(Instant.ofEpochMilli(1583760958000L)) shouldBe "2020-03-09T13:35:58Z"
codec.encode(Instant.EPOCH) shouldBe "1970-01-01T00:00:00Z"
codec.decode("2020-02-19T12:35:58Z") shouldBe Value(Instant.ofEpochMilli(1582115758000L))
check { (i: Instant) =>
val encoded = codec.encode(i)
codec.decode(encoded) == Value(i) && Instant.parse(encoded) == i
}
}

it should "correctly encode and decode example Durations" in {
val codec = implicitly[Codec[String, Duration, TextPlain]]
val start = OffsetDateTime.parse("2020-02-19T12:35:58Z")
codec.encode(Duration.between(start, start.plusDays(791).plusDays(12).plusSeconds(3))) shouldBe "PT19272H3S"
codec.decode("PT3H15S") shouldBe Value(Duration.of(10815000, ChronoUnit.MILLIS))
check { (d: Duration) =>
val encoded = codec.encode(d)
codec.decode(encoded) == Value(d) && Duration.parse(encoded) == d
}
}

it should "correctly encode and decode example ZoneOffsets" in {
val codec = implicitly[Codec[String, ZoneOffset, TextPlain]]
codec.encode(ZoneOffset.UTC) shouldBe "Z"
codec.encode(ZoneId.of("Europe/Moscow").getRules.getOffset(Instant.ofEpochMilli(1582115758000L))) shouldBe "+03:00"
codec.decode("-04:30") shouldBe Value(ZoneOffset.ofHoursMinutes(-4, -30))
}

it should "correctly encode and decode example OffsetTime" in {
val codec = implicitly[Codec[String, OffsetTime, TextPlain]]
codec.encode(OffsetTime.parse("13:45:30.123456789+02:00")) shouldBe "13:45:30.123456789+02:00"
codec.decode("12:00-11:30") shouldBe Value(OffsetTime.of(12, 0, 0, 0, ZoneOffset.ofHoursMinutes(-11, -30)))
codec.decode("06:15Z") shouldBe Value(OffsetTime.of(6, 15, 0, 0, ZoneOffset.UTC))
check { (ot: OffsetTime) =>
val encoded = codec.encode(ot)
codec.decode(encoded) == Value(ot) && OffsetTime.parse(encoded) == ot
}
}

it should "decode LocalDateTime from string without timezone" in {
check((ldt: LocalDateTime) => localDateTimeCodec.decode(ldt.toString) == Value(ldt))
}

it should "correctly encode and decode LocalTime" in {
val codec = implicitly[Codec[String, LocalTime, TextPlain]]
codec.encode(LocalTime.of(22, 59, 31, 3)) shouldBe "22:59:31.000000003"
codec.encode(LocalTime.of(13, 30)) shouldBe "13:30:00"
codec.decode("22:59:31.000000003") shouldBe Value(LocalTime.of(22, 59, 31, 3))
check { (lt: LocalTime) =>
val encoded = codec.encode(lt)
codec.decode(encoded) == Value(lt) && LocalTime.parse(encoded) == lt
}
}

it should "correctly encode and decode LocalDate" in {
val codec = implicitly[Codec[String, LocalDate, TextPlain]]
codec.encode(LocalDate.of(2019, 12, 31)) shouldBe "2019-12-31"
codec.encode(LocalDate.of(2020, 9, 22)) shouldBe "2020-09-22"
check { (ld: LocalDate) =>
val encoded = codec.encode(ld)
codec.decode(encoded) == Value(ld) && LocalDate.parse(encoded) == ld
}
}

it should "use default, when available" in {
Expand Down
Loading