-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
netlify-functions/src/main/scala/feral/netlify/Context.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* | ||
* Copyright 2021 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package feral.netlify | ||
|
||
import cats.effect.Sync | ||
import cats.~> | ||
import io.circe.Json | ||
import io.circe.scalajs._ | ||
|
||
import scala.concurrent.duration.FiniteDuration | ||
import scala.concurrent.duration._ | ||
import scala.annotation.nowarn | ||
|
||
//todo remodel | ||
final class Context[F[_]] private[netlify] ( | ||
val functionName: String, | ||
val functionVersion: String, | ||
val invokedFunctionArn: String, | ||
val memoryLimitInMB: Int, | ||
val awsRequestId: String, | ||
val logGroupName: String, | ||
val logStreamName: String, | ||
val identity: Option[Map[String, Json]], | ||
val clientContext: Option[Map[String, Json]], | ||
val remainingTime: F[FiniteDuration] | ||
) { | ||
def mapK[G[_]](f: F ~> G): Context[G] = new Context( | ||
functionName, | ||
functionVersion, | ||
invokedFunctionArn, | ||
memoryLimitInMB, | ||
awsRequestId, | ||
logGroupName, | ||
logStreamName, | ||
identity, | ||
clientContext, | ||
f(remainingTime)) | ||
} | ||
|
||
object Context { | ||
import scalajs.js.| | ||
|
||
@nowarn("msg=unreachable code") | ||
private def nullableToOption[A](nora: Null | A): Option[A] = nora match { | ||
case null => None | ||
case v => Some(v.asInstanceOf[A]) | ||
} | ||
|
||
private[netlify] def fromJS[F[_]: Sync](context: facade.Context): Context[F] = | ||
new Context( | ||
context.functionName, | ||
context.functionVersion, | ||
context.invokedFunctionArn, | ||
context.memoryLimitInMB, | ||
context.awsRequestId, | ||
context.logGroupName, | ||
context.logStreamName, | ||
context.identity.toOption.flatMap(nullableToOption).map { | ||
decodeJs[Map[String, Json]](_).toOption.get | ||
}, | ||
context.clientContext.toOption.flatMap(nullableToOption).map { | ||
decodeJs[Map[String, Json]](_).toOption.get | ||
}, | ||
Sync[F].delay(context.getRemainingTimeInMillis().millis) | ||
) | ||
} |
88 changes: 88 additions & 0 deletions
88
netlify-functions/src/main/scala/feral/netlify/IOLambda.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* Copyright 2021 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package feral.netlify | ||
|
||
import cats.effect.IO | ||
import cats.effect.IOLocal | ||
import cats.effect.kernel.Resource | ||
import feral.IOSetup | ||
import io.circe.Decoder | ||
import io.circe.Encoder | ||
import io.circe.scalajs._ | ||
|
||
import scala.scalajs.js | ||
import scala.scalajs.js.JSConverters._ | ||
import scala.scalajs.js.| | ||
|
||
abstract class IOLambda[Event, Result]( | ||
implicit private[netlify] val decoder: Decoder[Event], | ||
private[netlify] val encoder: Encoder[Result] | ||
) extends IOSetup { | ||
|
||
final type Setup = (Event, Context[IO]) => IO[Option[Result]] | ||
|
||
final override protected def setup: Resource[IO, Setup] = | ||
handler.map { handler => (event, context) => | ||
for { | ||
event <- IOLocal(event) | ||
context <- IOLocal(context) | ||
env = LambdaEnv.ioLambdaEnv(event, context) | ||
result <- handler(env) | ||
} yield result | ||
} | ||
|
||
def main(args: Array[String]): Unit = { | ||
val handlerName = getClass.getSimpleName.init | ||
js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) | ||
} | ||
|
||
private lazy val handlerFn | ||
: js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = { | ||
(event: js.Any, context: facade.Context) => | ||
(for { | ||
lambda <- setupMemo | ||
event <- IO.fromEither(decodeJs[Event](event)) | ||
result <- lambda(event, Context.fromJS(context)) | ||
} yield result.map(_.asJsAny).orUndefined).unsafeToPromise()(runtime) | ||
} | ||
|
||
def handler: Resource[IO, LambdaEnv[IO, Event] => IO[Option[Result]]] | ||
|
||
} | ||
|
||
object IOLambda { | ||
|
||
abstract class Simple[Event, Result]( | ||
implicit decoder: Decoder[Event], | ||
encoder: Encoder[Result]) | ||
extends IOLambda[Event, Result] { | ||
|
||
type Init | ||
def init: Resource[IO, Init] = Resource.pure(null.asInstanceOf[Init]) | ||
|
||
final def handler = init.map { init => env => | ||
for { | ||
event <- env.event | ||
ctx <- env.context | ||
result <- handle(event, ctx, init) | ||
} yield result | ||
} | ||
|
||
def handle(event: Event, context: Context[IO], init: Init): IO[Option[Result]] | ||
} | ||
|
||
} |
68 changes: 68 additions & 0 deletions
68
netlify-functions/src/main/scala/feral/netlify/LambdaEnv.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright 2021 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package feral.netlify | ||
|
||
import cats.Applicative | ||
import cats.Functor | ||
import cats.data.EitherT | ||
import cats.data.Kleisli | ||
import cats.data.OptionT | ||
import cats.data.WriterT | ||
import cats.effect.IO | ||
import cats.effect.IOLocal | ||
import cats.kernel.Monoid | ||
import cats.syntax.all._ | ||
import cats.~> | ||
|
||
sealed trait LambdaEnv[F[_], Event] { outer => | ||
def event: F[Event] | ||
def context: F[Context[F]] | ||
|
||
final def mapK[G[_]: Functor](f: F ~> G): LambdaEnv[G, Event] = | ||
new LambdaEnv[G, Event] { | ||
def event = f(outer.event) | ||
def context = f(outer.context).map(_.mapK(f)) | ||
} | ||
} | ||
|
||
object LambdaEnv { | ||
def apply[F[_], A](implicit env: LambdaEnv[F, A]): LambdaEnv[F, A] = env | ||
|
||
implicit def kleisliLambdaEnv[F[_]: Functor, A, B]( | ||
implicit env: LambdaEnv[F, A]): LambdaEnv[Kleisli[F, B, *], A] = | ||
env.mapK(Kleisli.liftK) | ||
|
||
implicit def optionTLambdaEnv[F[_]: Functor, A]( | ||
implicit env: LambdaEnv[F, A]): LambdaEnv[OptionT[F, *], A] = | ||
env.mapK(OptionT.liftK) | ||
|
||
implicit def eitherTLambdaEnv[F[_]: Functor, A, B]( | ||
implicit env: LambdaEnv[F, A]): LambdaEnv[EitherT[F, B, *], A] = | ||
env.mapK(EitherT.liftK) | ||
|
||
implicit def writerTLambdaEnv[F[_]: Applicative, A, B: Monoid]( | ||
implicit env: LambdaEnv[F, A]): LambdaEnv[WriterT[F, B, *], A] = | ||
env.mapK(WriterT.liftK[F, B]) | ||
|
||
private[netlify] def ioLambdaEnv[Event]( | ||
localEvent: IOLocal[Event], | ||
localContext: IOLocal[Context[IO]]): LambdaEnv[IO, Event] = | ||
new LambdaEnv[IO, Event] { | ||
def event = localEvent.get | ||
def context = localContext.get | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
netlify-functions/src/main/scala/feral/netlify/NetlifyHttp4sHandler.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/* | ||
* Copyright 2021 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package feral.netlify | ||
|
||
import cats.effect.kernel.Concurrent | ||
import cats.syntax.all._ | ||
import fs2.Stream | ||
import org.http4s.Charset | ||
import org.http4s.Header | ||
import org.http4s.Headers | ||
import org.http4s.HttpRoutes | ||
import org.http4s.Method | ||
import org.http4s.Request | ||
import org.http4s.Response | ||
import org.http4s.Uri | ||
|
||
object NetlifyHttp4sHandler { | ||
|
||
def apply[F[_]: Concurrent]( | ||
routes: HttpRoutes[F] | ||
)(implicit env: LambdaEnv[F, NetlifyHttpEvent]): F[Option[NetlifyHttpResult]] = | ||
for { | ||
event <- env.event | ||
method <- Method.fromString(event.httpMethod).liftTo[F] | ||
uri <- Uri.fromString(event.path).liftTo[F] | ||
headers = Headers(event.headers.toList) | ||
readBody = | ||
if (event.isBase64Encoded) | ||
fs2.text.base64.decode[F] | ||
else | ||
fs2.text.utf8.encode[F] | ||
request = Request( | ||
method, | ||
uri, | ||
headers = headers, | ||
body = Stream.fromOption[F](event.body).through(readBody)) | ||
response <- routes(request).getOrElse(Response.notFound[F]) | ||
isBase64Encoded = !response.charset.contains(Charset.`UTF-8`) | ||
responseBody <- (if (isBase64Encoded) | ||
response.body.through(fs2.text.base64.encode) | ||
else | ||
response.body.through(fs2.text.utf8.decode)).compile.foldMonoid | ||
} yield Some( | ||
NetlifyHttpResult( | ||
response.status.code, | ||
response | ||
.headers | ||
.headers | ||
.map { | ||
case Header.Raw(name, value) => | ||
name.toString -> value | ||
} | ||
.toMap, | ||
responseBody, | ||
isBase64Encoded | ||
) | ||
) | ||
|
||
} |
Oops, something went wrong.