Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.functions

import scala.scalajs.js
import scala.scalajs.js.annotation._

import feral.functions.facade.InvocationContext
import feral.functions.facade.JSRequest
import feral.functions.util.AppConfig.buildDefaultConfig
import feral.functions.util.AppConfig
import feral.functions.util.Parser

import org.http4s.HttpApp

import cats.effect.IO
import cats.effect.Resource
import cats.syntax.all._
import cats.effect.unsafe.IORuntime
import cats.effect.std.Dispatcher
import feral.functions.facade.Context

abstract class IOAzureHttpFunction {
protected def handler: Context => Resource[IO, HttpApp[IO]]
protected def appConfig: AppConfig = buildDefaultConfig(handlerFn)
protected def qBound: Int = 100

private[functions] val runtime = IORuntime.global

final def main(args: Array[String]): Unit =
IOAzureHttpFunction.App.http(functionName, appConfig.toJS)

private[functions] val functionName: String =
getClass.getSimpleName.init

private[functions] lazy val handlerFn
: js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = {
val dispatcherHandle = {
Dispatcher
.parallel[IO](await = true)
.product(Resource.pure(handler))
.allocated
.map(_._1) // drop unused finalizer, this resource will live for the duration
.unsafeToPromise()(runtime)
}

(requestJS, context) => {
dispatcherHandle.`then`[js.Any] {
case (dispatcher, handle) => {
val io = for {
request <- Parser.decodeRequest[IO](requestJS)
response <- handle(Context(context)).use(app => app.run(request))
respEncoded <- Parser.encodeResponse[IO](response, dispatcher, qBound)
} yield respEncoded

dispatcher.unsafeToPromise(io)
}
}
}
}
}

object IOAzureHttpFunction {
@js.native
@JSImport("@azure/functions", "app")
object App extends js.Object {
def http(
name: String,
appConfig: js.Object
): Unit = js.native
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.functions.facade

final case class Context(context: InvocationContext) {
def log(m: String): Unit = context.log(m)
def trace(m: String): Unit = context.trace(m)
def debug(m: String): Unit = context.debug(m)
def info(m: String): Unit = context.info(m)
def warn(m: String): Unit = context.warn(m)
def error(m: String): Unit = context.error(m)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.functions.facade

import scala.scalajs.js

@js.native
private[functions] trait InvocationContext extends js.Object {
def log(args: js.Any*): Unit = js.native
def trace(args: js.Any*): Unit = js.native
def debug(args: js.Any*): Unit = js.native
def info(args: js.Any*): Unit = js.native
def warn(args: js.Any*): Unit = js.native
def error(args: js.Any*): Unit = js.native

def functionId: String = js.native
def functionName: String = js.native
def extraInputs: ExtraInputs = js.native
def extraOutputs: ExtraOutputs = js.native
def retryContext: js.UndefOr[RetryContext] = js.native
def traceContext: js.UndefOr[TraceContext] = js.native
def options: Options = js.native
}

@js.native
private[functions] trait ExtraInputs extends js.Object {
def get(binding: js.Any): js.Any = js.native
}

@js.native
private[functions] trait ExtraOutputs extends js.Object {
def set(binding: js.Any, value: js.Any): Unit = js.native
}

@js.native
private[functions] trait RetryContext extends js.Object {
def retryCount: Int = js.native
def maxRetryCount: Int = js.native
def exception: js.UndefOr[js.Any] = js.native
}

@js.native
private[functions] trait TraceContext extends js.Object {
def traceParent: js.UndefOr[String] = js.native
def traceState: js.UndefOr[String] = js.native
def attributes: js.UndefOr[js.Dictionary[js.Any]] = js.native
}

@js.native
private[functions] trait Options extends js.Object {
def trigger: js.UndefOr[js.Any] = js.native
def extraInputs: js.UndefOr[js.Array[js.Any]] = js.native
def extraOutputs: js.UndefOr[js.Array[js.Any]] = js.native
def `return`: js.UndefOr[js.Any] = js.native
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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.functions.facade

import scala.scalajs.js
import scala.scalajs.js.typedarray.Uint8Array

import fs2.Stream
import fs2.Chunk

import cats.effect.kernel.Async
import cats.syntax.all._

@js.native
private[functions] trait JSRequest extends js.Object {
def method: String = js.native
def url: String = js.native
def headers: JSHeaders = js.native
def body: js.UndefOr[JSReadableStream] = js.native
}

@js.native
private[functions] trait JSHeaders extends js.Object {
def get(name: String): js.UndefOr[String] = js.native
def keys(): js.Iterator[String] = js.native
}

@js.native
private[functions] trait JSReadableStream extends js.Object {
def getReader(): JSReadableStreamDefaultReader = js.native
}

@js.native
private[functions] trait JSReadableStreamDefaultReader extends js.Object {
def read(): js.Promise[JSReadObject] = js.native
def releaseLock(): Unit = js.native
def cancel(reason: js.UndefOr[js.Any]): js.Promise[Unit]
}

@js.native
private[functions] trait JSReadObject extends js.Object {
def value: js.UndefOr[Uint8Array] = js.native
def done: Boolean = js.native
}

object JSHeaders {
private[functions] def keyList(h: JSHeaders): List[String] = {
val builder = List.newBuilder[String]
val itr = h.keys()
var entity = itr.next()

while (!entity.done) {
builder.addOne(entity.value)
entity = itr.next()
}

builder.result()
}

object Syntax {
// syntax for method like calls???
}
}

object JSReadableStream {
private[functions] def toFs2[F[_]: Async](
streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = {
streamOption.toOption match {
case None => Stream.empty
case Some(null) => Stream.empty
case Some(stream) => {
Stream.eval(Async[F].delay(stream.getReader())).flatMap { reader =>
def nextChunk = {
Async[F].fromPromise(Async[F].delay(reader.read())).map { read =>
if (read.done) {
None
} else {
val chunk = read
.value
.toOption
.map(arr => Chunk.array[Byte](toByteArray(arr)))
.getOrElse(Chunk.empty[Byte])

Some(chunk)
}
}
}

Stream
.repeatEval(nextChunk)
.unNoneTerminate
.flatMap(Stream.chunk)
.onFinalize(Async[F].delay(reader.releaseLock()))
}
}
}
}

private def toByteArray(array: Uint8Array): Array[Byte] = {
val builder = Array.newBuilder[Byte]
val length = array.length
var index = 0

while (index != length) {
builder.addOne(array(index).toByte)
index = index + 1
}

builder.result()
}

object Syntax {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.functions.util

import AppConfig._

import scala.scalajs.js

import feral.functions.facade.JSRequest
import feral.functions.facade.InvocationContext

final case class AppConfig(
methods: List[HttpMethod],
authLevel: AuthLevel,
route: AzureRoute,
handlerFn: HandlerFnT) {
private[functions] def toJS: js.Object = {
js.Dynamic
.literal(
methods = methodsToJS(methods),
authLevel = authLevel.value,
route = route.value,
handler = handlerFn
)
}
}

object AppConfig {
sealed trait HttpMethod {
private[functions] def value: String = {
this match {
case Get => "GET"
case Post => "POST"
case Put => "PUT"
case Patch => "PATCH"
case Delete => "DELETE"
}
}
}
case object Get extends HttpMethod
case object Post extends HttpMethod
case object Put extends HttpMethod
case object Patch extends HttpMethod
case object Delete extends HttpMethod

private def methodsToJS(list: List[HttpMethod]): js.Array[String] = {
val dList = list.distinct.map(_.value)
js.Array(dList: _*)
}

case class AuthLevel(value: String)
val anonymous: AuthLevel = AuthLevel("anonymous")
// add more?

case class AzureRoute(value: String)
val catchAll: AzureRoute = AzureRoute("{*path}")
// add more?

type HandlerFnT = js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]]

private[functions] def buildDefaultConfig(handlerFn: HandlerFnT) =
AppConfig(List(Get, Post, Put, Patch, Delete), anonymous, catchAll, handlerFn)
}
Loading
Loading