├── examples ├── project.scala ├── serverlib │ ├── util │ │ └── optional.scala │ ├── HttpService.scala │ ├── ServerMacros.scala │ └── jdkhttp │ │ ├── Client.scala │ │ └── Server.scala └── GreetService.scala ├── .gitignore ├── .scalafmt.conf ├── project.scala ├── .github └── workflows │ └── ci.yml ├── src ├── test │ └── OpsMirrorSuite.scala └── macros │ └── OpsMirror.scala ├── README.md └── LICENSE /examples/project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.3 2 | //> using dep io.github.bishabosha::ops-mirror::0.1.2 3 | //> using jvm 21 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .scala-build 4 | 5 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 6 | hs_err_pid* 7 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.7.14" 2 | runner.dialect = scala3 3 | align.preset = more 4 | maxColumn = 100 5 | indent.fewerBraces = never 6 | rewrite.scala3.convertToNewSyntax = true 7 | rewrite.scala3.removeOptionalBraces = yes 8 | rewrite.scala3.insertEndMarkerMinLines = 5 9 | verticalMultiline.atDefnSite = true 10 | newlines.usingParamListModifierPrefer = before 11 | -------------------------------------------------------------------------------- /examples/serverlib/util/optional.scala: -------------------------------------------------------------------------------- 1 | package serverlib.util 2 | 3 | import scala.util.boundary, boundary.Label, boundary.break 4 | 5 | object optional: 6 | extension [T](t: Option[T]) inline def ? (using Label[Option[T]]): T = 7 | t match 8 | case Some(value) => value 9 | case None => break(None) 10 | 11 | inline def abort[T](using Label[Option[T]]): Nothing = 12 | break(None) 13 | 14 | inline def apply[T](op: Label[Option[T]] ?=> T): Option[T] = 15 | boundary: 16 | Some(op) 17 | -------------------------------------------------------------------------------- /project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.3 2 | //> using jvm 8 3 | //> using exclude "${.}/examples/*" 4 | //> using test.dep org.scalameta::munit::1.0.0 5 | //> using publish.organization io.github.bishabosha 6 | //> using publish.name ops-mirror 7 | //> using publish.computeVersion git:tag 8 | //> using publish.repository central-s01 9 | //> using publish.license "Apache-2.0" 10 | //> using publish.url "https://github.com/bishabosha/ops-mirror" 11 | //> using publish.versionControl "github:bishabosha/ops-mirror" 12 | //> using publish.developer "Jamie Thompson|bishbashboshjt@gmail.com|https://github.com/bishabosha" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "v*" 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - uses: coursier/cache-action@v6.4 18 | - uses: VirtusLab/scala-cli-setup@v1.3.2 19 | with: 20 | jvm: "8" 21 | power: true 22 | 23 | - name: Check formatting 24 | run: scala-cli fmt src project.scala --check 25 | 26 | - name: Run unit tests 27 | run: scala-cli test src project.scala --cross 28 | -------------------------------------------------------------------------------- /examples/serverlib/HttpService.scala: -------------------------------------------------------------------------------- 1 | package serverlib 2 | 3 | import quoted.* 4 | import mirrorops.{OpsMirror, MetaAnnotation, ErrorAnnotation} 5 | 6 | trait HttpService[T]: 7 | val routes: Map[String, HttpService.Route] 8 | 9 | object HttpService: 10 | inline def derived[T](using m: OpsMirror.Of[T]): HttpService[T] = ${ ServerMacros.derivedImpl[T]('m) } 11 | 12 | transparent inline def endpoints[T](using m: HttpService[T], om: OpsMirror.Of[T]): Endpoints[T] = 13 | ${ ServerMacros.decorateImpl[T]('m, 'om) } 14 | 15 | final class Endpoints[T](val model: HttpService[T]) extends Selectable: 16 | def selectDynamic(name: String): HttpService.Route = model.routes(name) 17 | 18 | object Endpoints: 19 | opaque type Endpoint[I, E, O] <: HttpService.Route = HttpService.Route 20 | 21 | sealed trait Empty 22 | 23 | object model: 24 | class failsWith[E] extends ErrorAnnotation[E] 25 | 26 | enum method extends MetaAnnotation: 27 | case get(route: String) 28 | case post(route: String) 29 | case put(route: String) 30 | 31 | enum source extends MetaAnnotation: 32 | case path() 33 | case query() 34 | case body() 35 | 36 | case class Input(label: String, source: model.source) 37 | case class Route(route: model.method, inputs: Seq[Input]) 38 | -------------------------------------------------------------------------------- /examples/GreetService.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import serverlib.* 4 | 5 | import HttpService.model.*, source.*, method.* 6 | 7 | import scala.collection.concurrent.TrieMap 8 | import syntax.* 9 | import mirrorops.OpsMirror 10 | 11 | @failsWith[Int] 12 | trait GreetService derives HttpService: 13 | @get("/greet/{name}") 14 | def greet(@path name: String): String 15 | 16 | @post("/greet/{name}") 17 | def setGreeting(@path name: String, @body greeting: String): Unit 18 | 19 | 20 | @main def server = 21 | import jdkhttp.Server.* 22 | 23 | val e = HttpService.endpoints[GreetService] 24 | 25 | e.model.routes.foreach((k, r) => println(s"$k: $r")) 26 | 27 | val greetings = TrieMap.empty[String, String] 28 | 29 | val server = ServerBuilder() 30 | .addEndpoint: 31 | e.greet.handle(name => Right(s"${greetings.getOrElse(name, "Hello")}, $name")) 32 | .addEndpoint: 33 | e.setGreeting.handle((name, greeting) => Right(greetings(name) = greeting)) 34 | .create(port = 8080) 35 | 36 | sys.addShutdownHook(server.close()) 37 | 38 | @main def client(who: String, newGreeting: String) = 39 | import jdkhttp.PartialRequest 40 | 41 | val e = HttpService.endpoints[GreetService] 42 | val baseURL = "http://localhost:8080" 43 | 44 | val greetRequest = PartialRequest(e.greet, baseURL) 45 | .prepare(who) 46 | 47 | val setGreetingRequest = PartialRequest(e.setGreeting, baseURL) 48 | .prepare(who, newGreeting) 49 | 50 | either: 51 | val init = greetRequest.send().? 52 | setGreetingRequest.send().? 53 | val updated = greetRequest.send().? 54 | println(s"greeting for $who was: $init, now is: $updated") 55 | 56 | 57 | import scala.util.boundary, boundary.{Label, break} 58 | 59 | object syntax: 60 | def either[A, B](op: Label[Left[A, Nothing]] ?=> B): Either[A, B] = 61 | boundary[Either[A, B]]: 62 | Right(op) 63 | 64 | extension [A, B](e: Either[A, B]) def ?(using l: Label[Left[A, Nothing]]): B = e match 65 | case Right(b) => b 66 | case Left(a) => break(Left(a)) 67 | -------------------------------------------------------------------------------- /src/test/OpsMirrorSuite.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import mirrorops.OpsMirror 4 | import mirrorops.Meta 5 | import compiletime.constValue 6 | import compiletime.constValueTuple 7 | 8 | class OpsMirrorSuite extends munit.FunSuite: 9 | import OpsMirrorSuite.* 10 | import OpMeta.* 11 | import ParamMeta.* 12 | 13 | class failsWith[E] extends mirrorops.ErrorAnnotation[E] 14 | 15 | enum BasicError: 16 | case Message(msg: String) 17 | 18 | enum OpMeta extends mirrorops.MetaAnnotation: 19 | case Streaming() 20 | case JSONBody() 21 | 22 | enum ParamMeta extends mirrorops.MetaAnnotation: 23 | case PrimaryKey() 24 | 25 | @failsWith[BasicError] 26 | trait BasicService: 27 | @Streaming 28 | @JSONBody 29 | def lookup(@PrimaryKey id: Long): String 30 | end BasicService 31 | 32 | test("summon mirror basic with annotations") { 33 | val mirror = summon[OpsMirror.Of[BasicService]] 34 | 35 | type FirstOp = Tuple.Head[mirror.MirroredOperations] 36 | 37 | summon[mirror.MirroredLabel =:= "BasicService"] 38 | summon[mirror.MirroredOperationLabels =:= ("lookup" *: EmptyTuple)] 39 | summon[Operation_Metadata[FirstOp] =:= (Meta @JSONBody, Meta @Streaming)] 40 | summon[Operation_InputLabels[FirstOp] =:= ("id" *: EmptyTuple)] 41 | summon[Operation_InputTypes[FirstOp] =:= (Long *: EmptyTuple)] 42 | summon[ 43 | Operation_InputMetadatas[FirstOp] =:= ((Meta @PrimaryKey *: EmptyTuple) *: EmptyTuple) 44 | ] 45 | summon[Operation_ErrorType[FirstOp] =:= BasicError] 46 | summon[Operation_OutputType[FirstOp] =:= String] 47 | } 48 | end OpsMirrorSuite 49 | 50 | object OpsMirrorSuite: 51 | type Operation_Is[Ls <: Tuple] = mirrorops.Operation { type InputTypes = Ls } 52 | type Operation_Im[Ls <: Tuple] = mirrorops.Operation { 53 | type InputMetadatas = Ls 54 | } 55 | type Operation_M[Ls <: Tuple] = mirrorops.Operation { type Metadata = Ls } 56 | type Operation_Et[E] = mirrorops.Operation { type ErrorType = E } 57 | type Operation_Ot[T] = mirrorops.Operation { type OutputType = T } 58 | type Operation_IL[Ls <: Tuple] = mirrorops.Operation { type InputLabels = Ls } 59 | type Operation_InputLabels[Op] = Op match 60 | case Operation_IL[ls] => ls 61 | type Operation_InputTypes[Op] = Op match 62 | case Operation_Is[ls] => ls 63 | type Operation_ErrorType[Op] = Op match 64 | case Operation_Et[ls] => ls 65 | type Operation_OutputType[Op] = Op match 66 | case Operation_Ot[ls] => ls 67 | type Operation_InputMetadatas[Op] = Op match 68 | case Operation_Im[ls] => ls 69 | type Operation_Metadata[Op] = Op match 70 | case Operation_M[ls] => ls 71 | end OpsMirrorSuite 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ops-mirror 2 | 3 | Answering the question of "what if my fancy endpoints were defined as a trait" 4 | 5 | ## Usage 6 | 7 | Use ops-mirror to help define the `derived` method of a Type-class. It provides a view (`mirrorops.OpsMirror`) over the methods of a trait. This is much more convenient to decompose with quotes/splices, or even match types, than the alternative of using the Reflection API (`scala.quoted.Quotes`). 8 | 9 | ```scala 10 | //> using dep io.github.bishabosha::ops-mirror::0.1.2 11 | 12 | import mirrorops.OpsMirror 13 | 14 | // example type-class that defines a Schema of operations. 15 | trait Schema[A] { 16 | def operations: List[Operation] 17 | } 18 | 19 | object Schema { 20 | // necessary method for `... derives Schema` on a class/trait/enum 21 | // `(using mirror: OpsMirror.Of[A])` provides a compile-time view on the methods of `A`. 22 | inline def derived[A](using mirror: OpsMirror.Of[A]): Schema[A] = ??? 23 | } 24 | ``` 25 | 26 | ## Motivating example 27 | 28 | > The following code samples can be found and ran in the [examples](examples) directory. 29 | 30 | As an alternative to endpoint libraries, e.g. Tapir, endpoints4s, zio-http, how about a plain trait + annotations? 31 | 32 | ```scala 33 | @failsWith[Int] 34 | trait GreetService derives HttpService: 35 | @get("/greet/{name}") 36 | def greet(@path name: String): String 37 | 38 | @post("/greet/{name}") 39 | def setGreeting(@path name: String, @body greeting: String): Unit 40 | ``` 41 | 42 | `HttpService` is a type-class that stores a collection of HTTP routes. `HttpService.derived` is an inline method that uses `OpsMirror` to reflect on the structure of `GreetService`, converting each method to a route. 43 | 44 | Using the HttpService, you can then derive servers/clients as such: 45 | 46 | ```scala 47 | val e = HttpService.endpoints[GreetService] 48 | 49 | @main def server = 50 | val greetings = concurrent.TrieMap.empty[String, String] 51 | 52 | val server = ServerBuilder() 53 | .addEndpoint: 54 | e.greet.handle: name => 55 | val greeting = greetings.getOrElse(name, "Hello") 56 | Right(s"$greeting, $name") 57 | .addEndpoint: 58 | e.setGreeting.handle: (name, greeting) => 59 | greetings(name) = greeting 60 | Right(()) 61 | .create(port = 8080) 62 | 63 | sys.addShutdownHook(server.close()) 64 | end server 65 | 66 | @main def client(name: String, newGreeting: String) = 67 | val baseURL = "http://localhost:8080" 68 | 69 | val greetRequest = PartialRequest(e.greet, baseURL) 70 | .prepare(who) 71 | 72 | val setGreetingRequest = PartialRequest(e.setGreeting, baseURL) 73 | .prepare(who, newGreeting) 74 | 75 | either: 76 | val init = greetRequest.send().? 77 | setGreetingRequest.send().? 78 | val updated = greetRequest.send().? 79 | println(s"greeting for $who was: $init, now is: $updated") 80 | end client 81 | ``` 82 | 83 | ## Publishing 84 | 85 | due to the way this project is structured, to publish you need to specify project.scala explicitly and src. This is the only way to ignore examples, but also include the code. 86 | 87 | ```bash 88 | scala-cli --power publish local project.scala src 89 | ``` 90 | -------------------------------------------------------------------------------- /examples/serverlib/ServerMacros.scala: -------------------------------------------------------------------------------- 1 | package serverlib 2 | 3 | import scala.quoted.* 4 | import mirrorops.{OpsMirror, Operation, VoidType} 5 | 6 | import scala.util.chaining.given 7 | 8 | import HttpService.{Route, Input, model, Empty} 9 | import HttpService.Endpoints, Endpoints.Endpoint 10 | 11 | object ServerMacros: 12 | 13 | type EncodeError[T] = T match 14 | case VoidType => Empty 15 | case _ => T 16 | 17 | def decorateImpl[T: Type](modelExpr: Expr[HttpService[T]], mirror: Expr[OpsMirror.Of[T]])(using Quotes): Expr[Endpoints[T]] = 18 | import quotes.reflect.* 19 | 20 | def extractEndpoints[Ts: Type]: List[Type[?]] = 21 | OpsMirror.typesFromTuple[Ts].map: 22 | case '[op] => extractEndpoint[op] 23 | 24 | def extractEndpoint[T: Type]: Type[?] = Type.of[T] match 25 | case '[Operation { type InputTypes = inputTypes; type ErrorType = errorType; type OutputType = outputType }] => 26 | Type.of[Endpoint[inputTypes, EncodeError[errorType], outputType]] 27 | 28 | val refinements = mirror match 29 | case '{ 30 | $m: OpsMirror.Of[T] { 31 | type MirroredOperations = mirroredOps 32 | type MirroredOperationLabels = opLabels 33 | } 34 | } => 35 | val labels = OpsMirror.stringsFromTuple[opLabels] 36 | labels.zip(extractEndpoints[mirroredOps]).foldLeft(Type.of[Endpoints[T]]: Type[?])({ (acc, p) => 37 | ((acc, p): @unchecked) match 38 | case ('[acc], (l, '[e])) => 39 | Refinement(TypeRepr.of[acc], l, TypeRepr.of[e]).asType 40 | }) 41 | 42 | refinements match 43 | case '[resTpe] => '{ Endpoints($modelExpr).asInstanceOf[Endpoints[T] & resTpe] } 44 | end decorateImpl 45 | 46 | 47 | def derivedImpl[T: Type](mirror: Expr[OpsMirror.Of[T]])(using Quotes): Expr[HttpService[T]] = 48 | import quotes.reflect.* 49 | 50 | def extractServices[Ts: Type]: List[Expr[Route]] = OpsMirror.typesFromTuple[Ts].map: 51 | case '[op] => 52 | val metas = OpsMirror.metadata[op] 53 | val route = metas.base.collectFirst { 54 | case '{ $g: model.method } => g 55 | } 56 | val ins = Type.of[op] match 57 | case '[ 58 | Operation { type InputLabels = inputLabels } 59 | ] => 60 | val labels = OpsMirror.stringsFromTuple[inputLabels] 61 | val ins = 62 | labels.lazyZip(metas.inputs).map((l, ms) => 63 | val method: Option[Expr[model.source]] = ms.collectFirst { 64 | case '{ $p: model.source } => p 65 | } 66 | '{Input(${Expr(l)}, ${method.getOrElse(report.errorAndAbort(s"expected a valid source for param ${l}"))})} 67 | ) 68 | .pipe(Expr.ofSeq) 69 | ins 70 | 71 | route match 72 | case Some(r) => '{ Route($r, $ins) } 73 | case None => report.errorAndAbort(s"got the metadata elems ${metas.base.map(_.show)}") 74 | end extractServices 75 | 76 | val serviceExprs = mirror match 77 | case '{ 78 | $m: OpsMirror.Of[T] { 79 | type MirroredOperations = mirroredOps 80 | type MirroredOperationLabels = opLabels 81 | } 82 | } => 83 | val labels = OpsMirror.stringsFromTuple[opLabels] 84 | labels.groupBy(identity).values.find(_.sizeIs > 1).foreach: label => 85 | report.errorAndAbort(s"HttpService does not support overloaded methods, found ${label.head} more than once.") 86 | labels.zip(extractServices[mirroredOps]).map((l, s) => '{(${Expr(l)}, $s)}) 87 | ('{ 88 | new HttpService[T] { 89 | val routes = Map(${Varargs(serviceExprs)}*) 90 | } 91 | }) 92 | end derivedImpl 93 | -------------------------------------------------------------------------------- /examples/serverlib/jdkhttp/Client.scala: -------------------------------------------------------------------------------- 1 | package serverlib.jdkhttp 2 | 3 | import java.net.http.HttpClient 4 | import java.net.http.HttpRequest 5 | import java.net.http.HttpClient.Version 6 | import java.net.http.HttpClient.Redirect 7 | import java.net.http.HttpResponse 8 | import java.net.URI 9 | 10 | import serverlib.HttpService.Endpoints.Endpoint 11 | import serverlib.HttpService.Empty 12 | import serverlib.HttpService.Input 13 | import serverlib.HttpService.model.source 14 | import serverlib.HttpService.model.method 15 | 16 | import PartialRequest.{Request, Bundler, Func} 17 | 18 | class PartialRequest[I <: Tuple, E, O] private ( 19 | e: Endpoint[I, E, O], baseURI: String, builder: HttpRequest.Builder)(using Bundler[I, E, O]): 20 | 21 | private val optBody: Option[IArray[String] => String] = 22 | e.inputs.view.map(_.source).zipWithIndex.collectFirst({ case (source.body(), i) => bundle => bundle(i) }) 23 | 24 | private val (httpMethod, route) = e.route match 25 | case method.get(route) => ("GET", route) 26 | case method.post(route) => ("POST", route) 27 | case method.put(route) => ("PUT", route) 28 | 29 | private val uriParts: Seq[IArray[String] => String] = 30 | val uriParams: Map[String, Int] = 31 | e.inputs.zipWithIndex.collect({ 32 | case (Input(label, source.path()), i) => (label, i) 33 | }).toMap 34 | Server.uriPattern(route).map({ 35 | case Server.UriParts.Exact(str) => Function.const(str) 36 | case Server.UriParts.Wildcard(name) => 37 | val i = uriParams(name) 38 | bundle => bundle(i) 39 | }) 40 | 41 | protected def handle(bundle: IArray[String]): Request[E, O] = 42 | val uri = uriParts.view.map(_(bundle)).mkString(baseURI, "/", "") 43 | val withUri = builder.uri(URI.create(uri)) 44 | val withBody = optBody.fold(withUri)(get => 45 | val body = get(bundle) 46 | assert(body.nonEmpty, "empty body") 47 | withUri 48 | .setHeader("Content-Type", "text/plain") 49 | .method(httpMethod, HttpRequest.BodyPublishers.ofString(body)) 50 | ) 51 | Request(withBody.build()) 52 | 53 | val prepare: Func[I, Request[E, O]] = summon[Bundler[I, E, O]].bundle(this) 54 | 55 | object PartialRequest: 56 | 57 | class Request[E, O] private[PartialRequest] (req: HttpRequest): 58 | protected def baseSend = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()) 59 | 60 | def sendWithError()(using Des[E], Des[O]): Either[E, O] = 61 | val response = baseSend 62 | val body = response.body() 63 | if response.statusCode() < 400 then 64 | Right(summon[Des[O]].deserialize(body)) 65 | else 66 | Left(summon[Des[E]].deserialize(body)) 67 | 68 | def sendNoError()(using Des[O]): O = 69 | val response = baseSend 70 | if response.statusCode() < 400 then 71 | summon[Des[O]].deserialize(response.body()) 72 | else 73 | throw new RuntimeException(s"Request failed with status ${response.statusCode()} and body ${response.body()}") 74 | 75 | inline def send()(using Des[E], Des[O]): Res[E, O] = inline compiletime.erasedValue[E] match 76 | case _: Empty => sendNoError() 77 | case _ => sendWithError() 78 | 79 | type Func[I <: Tuple, O] = I match 80 | case EmptyTuple => () => O 81 | case a *: EmptyTuple => a => O 82 | case (a, b) => (a, b) => O 83 | 84 | type Res[E, O] = E match 85 | case Empty => O 86 | case _ => Either[E, O] 87 | 88 | trait Bundler[I <: Tuple, E, O]: 89 | def bundle(e: PartialRequest[I, E, O]): Func[I, Request[E, O]] 90 | 91 | trait Des[T]: 92 | def deserialize(s: String): T 93 | 94 | object Des: 95 | given Des[String] with 96 | def deserialize(s: String): String = s 97 | given Des[Int] with 98 | def deserialize(s: String): Int = s.toInt 99 | 100 | given Des[Unit] with 101 | def deserialize(s: String): Unit = () 102 | 103 | given Des[Empty] with 104 | def deserialize(s: String): Empty = ??? // should never be called 105 | 106 | trait Ser[I]: 107 | def serialize(i: I): String 108 | 109 | object Ser: 110 | given Ser[String] with 111 | def serialize(i: String): String = i 112 | given Ser[Int] with 113 | def serialize(i: Int): String = i.toString 114 | 115 | object Bundler: 116 | inline given [I <: Tuple, E, O]: Bundler[I, E, O] = new Bundler[I, E, O]: 117 | def bundle(req: PartialRequest[I, E, O]): Func[I, Request[E, O]] = 118 | inline compiletime.erasedValue[I] match 119 | case _: EmptyTuple => () => 120 | req.handle(IArray.empty) 121 | case _: (a *: EmptyTuple) => (a0: a) => 122 | req.handle(IArray(compiletime.summonInline[Ser[a]].serialize(a0))) 123 | case _: (a, b) => (a0: a, a1: b) => 124 | val bundle = IArray( 125 | compiletime.summonInline[Ser[a]].serialize(a0), 126 | compiletime.summonInline[Ser[b]].serialize(a1), 127 | ) 128 | req.handle(bundle) 129 | 130 | def apply[I <: Tuple, E, O](e: Endpoint[I, E, O], baseURI: String)(using Bundler[I, E, O]): PartialRequest[I, E, O] = 131 | new PartialRequest(e, s"$baseURI/", HttpRequest.newBuilder()) 132 | -------------------------------------------------------------------------------- /src/macros/OpsMirror.scala: -------------------------------------------------------------------------------- 1 | package mirrorops 2 | 3 | import quoted.* 4 | 5 | import scala.util.chaining.given 6 | import scala.annotation.implicitNotFound 7 | 8 | @implicitNotFound( 9 | "No OpsMirror could be generated.\nDiagnose any issues by calling OpsMirror.reify[T] directly" 10 | ) 11 | sealed trait OpsMirror: 12 | type Metadata <: Tuple 13 | type MirroredType 14 | type MirroredLabel 15 | type MirroredOperations <: Tuple 16 | type MirroredOperationLabels <: Tuple 17 | end OpsMirror 18 | 19 | sealed trait Meta 20 | 21 | sealed trait VoidType 22 | 23 | open class MetaAnnotation extends scala.annotation.RefiningAnnotation 24 | 25 | open class ErrorAnnotation[E] extends MetaAnnotation 26 | 27 | sealed trait Operation: 28 | type Metadata <: Tuple 29 | type InputTypes <: Tuple 30 | type InputLabels <: Tuple 31 | type InputMetadatas <: Tuple 32 | type ErrorType 33 | type OutputType 34 | end Operation 35 | 36 | object OpsMirror: 37 | type Of[T] = OpsMirror { type MirroredType = T } 38 | 39 | transparent inline given reify[T]: Of[T] = ${ reifyImpl[T] } 40 | 41 | case class Metadata(base: List[Expr[Any]], inputs: List[List[Expr[Any]]]) 42 | 43 | def typesFromTuple[Ts: Type](using Quotes): List[Type[?]] = 44 | Type.of[Ts] match 45 | case '[t *: ts] => Type.of[t] :: typesFromTuple[ts] 46 | case '[EmptyTuple] => Nil 47 | 48 | def stringsFromTuple[Ts: Type](using Quotes): List[String] = 49 | typesFromTuple[Ts].map: 50 | case '[t] => stringFromType[t] 51 | 52 | def stringFromType[T: Type](using Quotes): String = 53 | import quotes.reflect.* 54 | TypeRepr.of[T] match 55 | case ConstantType(StringConstant(label)) => label 56 | case _ => 57 | report.errorAndAbort(s"expected a constant string, got ${TypeRepr.of[T]}") 58 | end match 59 | end stringFromType 60 | 61 | def typesToTuple(list: List[Type[?]])(using Quotes): Type[?] = 62 | val empty: Type[? <: Tuple] = Type.of[EmptyTuple] 63 | list.foldRight(empty)({ case ('[t], '[acc]) => 64 | Type.of[t *: (acc & Tuple)] 65 | }) 66 | end typesToTuple 67 | 68 | def metadata[Op: Type](using Quotes): Metadata = 69 | import quotes.reflect.* 70 | 71 | def extractMetass[Metadatas: Type]: List[List[Expr[Any]]] = 72 | typesFromTuple[Metadatas].map: 73 | case '[m] => extractMetas[m] 74 | 75 | def extractMetas[Metadata: Type]: List[Expr[Any]] = 76 | typesFromTuple[Metadata].map: 77 | case '[m] => 78 | TypeRepr.of[m] match 79 | case AnnotatedType(_, annot) => 80 | annot.asExpr 81 | case tpe => 82 | report.errorAndAbort(s"got the metadata element ${tpe.show}") 83 | 84 | Type.of[Op] match 85 | case '[Operation { 86 | type Metadata = metadata 87 | type InputMetadatas = inputMetadatas 88 | }] => 89 | Metadata(extractMetas[metadata], extractMetass[inputMetadatas]) 90 | case _ => report.errorAndAbort("expected an Operation with Metadata.") 91 | end match 92 | end metadata 93 | 94 | private def reifyImpl[T: Type](using Quotes): Expr[Of[T]] = 95 | import quotes.reflect.* 96 | 97 | val tpe = TypeRepr.of[T] 98 | val cls = tpe.classSymbol.get 99 | val decls = cls.declaredMethods 100 | val labels = decls.map(m => ConstantType(StringConstant(m.name))) 101 | 102 | def isMeta(annot: Term): Boolean = 103 | if annot.tpe <:< TypeRepr.of[MetaAnnotation] then true 104 | else if annot.tpe <:< TypeRepr.of[scala.annotation.internal.SourceFile] 105 | then false 106 | else 107 | report.error( 108 | s"annotation ${annot.show} does not extend ${Type.show[MetaAnnotation]}", 109 | annot.pos 110 | ) 111 | false 112 | 113 | def encodeMeta(annot: Term): Type[?] = 114 | AnnotatedType(TypeRepr.of[Meta], annot).asType 115 | 116 | val (errorTpe, gmeta) = 117 | val annots = cls.annotations.filter(isMeta) 118 | val (errorAnnots, metaAnnots) = 119 | annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]]) 120 | val errorTpe = 121 | if errorAnnots.isEmpty then Type.of[VoidType] 122 | else 123 | errorAnnots 124 | .map: annot => 125 | annot.asExpr match 126 | case '{ $a: ErrorAnnotation[t] } => Type.of[t] 127 | .head 128 | (errorTpe, metaAnnots.map(encodeMeta)) 129 | end val 130 | 131 | val ops = decls.map(method => 132 | val metaAnnots = 133 | val annots = method.annotations.filter(isMeta) 134 | val (errorAnnots, metaAnnots) = 135 | annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]]) 136 | if errorAnnots.nonEmpty then 137 | errorAnnots.foreach: annot => 138 | report.error( 139 | s"error annotation ${annot.show} has no meaning on a method, annotate the scope itself.", 140 | annot.pos 141 | ) 142 | end if 143 | metaAnnots.map(encodeMeta) 144 | end metaAnnots 145 | val meta = typesToTuple(metaAnnots) 146 | val (inputTypes, inputLabels, inputMetas, output) = 147 | tpe.memberType(method) match 148 | case ByNameType(res) => 149 | val output = res.asType 150 | (Nil, Nil, Nil, output) 151 | case MethodType(paramNames, paramTpes, res) => 152 | val inputTypes = paramTpes.map(_.asType) 153 | val inputLabels = paramNames.map(l => ConstantType(StringConstant(l)).asType) 154 | val inputMetas = method.paramSymss.head.map: s => 155 | typesToTuple(s.annotations.filter(isMeta).map(encodeMeta)) 156 | val output = res match 157 | case _: MethodType => 158 | report.errorAndAbort(s"curried method ${method.name} is not supported") 159 | case _: PolyType => 160 | report.errorAndAbort(s"curried method ${method.name} is not supported") 161 | case _ => res.asType 162 | (inputTypes, inputLabels, inputMetas, output) 163 | case _: PolyType => 164 | report.errorAndAbort(s"generic method ${method.name} is not supported") 165 | val inTup = typesToTuple(inputTypes) 166 | val inLab = typesToTuple(inputLabels) 167 | val inMet = typesToTuple(inputMetas) 168 | (meta, inTup, inLab, inMet, errorTpe, output) match 169 | case ('[m], '[i], '[l], '[iM], '[e], '[o]) => 170 | Type.of[ 171 | Operation { 172 | type Metadata = m 173 | type InputTypes = i 174 | type InputLabels = l 175 | type InputMetadatas = iM 176 | type ErrorType = e 177 | type OutputType = o 178 | } 179 | ] 180 | end match 181 | ) 182 | val clsMeta = typesToTuple(gmeta) 183 | val opsTup = typesToTuple(ops.toList) 184 | val labelsTup = typesToTuple(labels.map(_.asType)) 185 | val name = ConstantType(StringConstant(cls.name)).asType 186 | (clsMeta, opsTup, labelsTup, name) match 187 | case ('[meta], '[ops], '[labels], '[label]) => 188 | '{ 189 | (new OpsMirror: 190 | type Metadata = meta & Tuple 191 | type MirroredType = T 192 | type MirroredLabel = label 193 | type MirroredOperations = ops & Tuple 194 | type MirroredOperationLabels = labels & Tuple 195 | ): OpsMirror.Of[T] { 196 | type MirroredLabel = label 197 | type MirroredOperations = ops & Tuple 198 | type MirroredOperationLabels = labels & Tuple 199 | } 200 | } 201 | end match 202 | end reifyImpl 203 | end OpsMirror 204 | -------------------------------------------------------------------------------- /examples/serverlib/jdkhttp/Server.scala: -------------------------------------------------------------------------------- 1 | package serverlib.jdkhttp 2 | 3 | import com.sun.net.httpserver.HttpHandler 4 | import com.sun.net.httpserver.HttpExchange 5 | import com.sun.net.httpserver.HttpServer 6 | 7 | import java.util.concurrent.Executors 8 | import scala.collection.mutable.ListBuffer 9 | 10 | import serverlib.HttpService.Empty 11 | import serverlib.HttpService.Endpoints.Endpoint 12 | import scala.util.TupledFunction 13 | 14 | import serverlib.util.optional 15 | import scala.collection.View.Empty 16 | 17 | class Server private (private val internal: HttpServer) extends AutoCloseable: 18 | def close(): Unit = internal.stop(0) 19 | 20 | object Server: 21 | 22 | enum UriParts: 23 | case Exact(str: String) 24 | case Wildcard(name: String) 25 | 26 | def uriPattern(route: String): IndexedSeq[UriParts] = 27 | assert(route.startsWith("/")) 28 | val parts = route.split("/").view.drop(1) 29 | assert(parts.forall(_.nonEmpty)) 30 | parts.toIndexedSeq.map { 31 | case s if s.startsWith("{") && s.endsWith("}") => UriParts.Wildcard(s.slice(1, s.length - 1)) 32 | case s => UriParts.Exact(s) 33 | } 34 | 35 | enum HttpMethod: 36 | case Get, Post, Put 37 | 38 | type UriHandler = String => Option[Map[String, String]] 39 | 40 | type Func[I <: Tuple, E, O] = I match 41 | case EmptyTuple => () => Res[E, O] 42 | case a *: EmptyTuple => a => Res[E, O] 43 | case (a, b) => (a, b) => Res[E, O] 44 | 45 | type Res[E, O] = E match 46 | case Empty => O 47 | case _ => Either[E, O] 48 | 49 | trait Exchanger[I <: Tuple, E, O](using Ser[Res[E, O]]): 50 | def apply(bundle: Bundle, func: Func[I, E, O]): Ser.Result 51 | 52 | trait Ser[O]: 53 | def serialize(o: O): Ser.Result 54 | 55 | object Ser: 56 | type Result = Either[Option[Array[Byte]], Option[Array[Byte]]] 57 | 58 | given [E: Ser, O: Ser]: Ser[Either[E, O]] with 59 | def serialize(o: Either[E, O]): Result = o.fold( 60 | e => Left(summon[Ser[E]].serialize(e).merge), 61 | o => Right(summon[Ser[O]].serialize(o).merge) 62 | ) 63 | 64 | given Ser[Unit] with 65 | def serialize(o: Unit): Result = Right(None) 66 | 67 | given Ser[String] with 68 | def serialize(o: String): Result = Right(Some(o.getBytes(java.nio.charset.StandardCharsets.UTF_8))) 69 | 70 | given Ser[Int] with 71 | def serialize(o: Int): Result = summon[Ser[String]].serialize(o.toString) 72 | 73 | trait Des[I]: 74 | def deserialize(str: String): I 75 | 76 | object Des: 77 | given Des[Int] with 78 | def deserialize(str: String): Int = str.toInt 79 | 80 | given Des[String] with 81 | def deserialize(str: String): String = str 82 | 83 | object Exchanger: 84 | inline given [I <: Tuple, E, O](using Ser[Res[E, O]]): Exchanger[I, E, O] = new Exchanger[I, E, O]: 85 | def apply(bundle: Bundle, func: Func[I, E, O]): Ser.Result = 86 | val res = 87 | inline compiletime.erasedValue[I] match 88 | case _: EmptyTuple => func.asInstanceOf[() => Res[E, O]]() 89 | case _: (a *: EmptyTuple) => 90 | val dA = compiletime.summonInline[Des[a]] 91 | func.asInstanceOf[a => Res[E, O]](dA.deserialize(bundle.arg(0))) 92 | case _: (a, b) => 93 | val dA = compiletime.summonInline[Des[a]] 94 | val dB = compiletime.summonInline[Des[b]] 95 | func.asInstanceOf[(a, b) => Res[E, O]](dA.deserialize(bundle.arg(0)), dB.deserialize(bundle.arg(1))) 96 | summon[Ser[Res[E, O]]].serialize(res) 97 | 98 | trait Bundle: 99 | def arg(index: Int): String 100 | 101 | extension [I <: Tuple, E, O](e: Endpoint[I, E, O]) 102 | def handle(op: Func[I, E, O])(using Exchanger[I, E, O]): Handler[I, E, O] = 103 | Handler[I, E, O](e, op, summon[Exchanger[I, E, O]]) 104 | 105 | private def rootHandler(handlers: List[Handler[?, ?, ?]]): HttpHandler = 106 | val lazyHandlers = handlers.to(LazyList) 107 | .map: h => 108 | val (method, uriHandler) = h.route 109 | method -> (uriHandler, h) 110 | .groupMap: 111 | (method, _) => method 112 | .apply: 113 | (_, pair) => pair 114 | 115 | (exchange: HttpExchange) => 116 | // get method 117 | val method = exchange.getRequestMethod match 118 | case "GET" => HttpMethod.Get 119 | case "POST" => HttpMethod.Post 120 | case "PUT" => HttpMethod.Put 121 | case _ => throw new IllegalArgumentException("Unsupported method") 122 | 123 | // get uri 124 | val uri = exchange.getRequestURI.getPath() 125 | // match the uri to a handler 126 | val handlerOpt = lazyHandlers.get(method).flatMap: ls => 127 | ls 128 | .flatMap: (uriHandler, h) => 129 | uriHandler(uri).map: params => 130 | h -> params 131 | .headOption 132 | 133 | def readBody(length: Int): Array[Byte] = 134 | // consume the full input stream 135 | val is = exchange.getRequestBody 136 | try 137 | is.readAllBytes().ensuring(_.length == length, "read less bytes than expected") 138 | finally 139 | is.close() 140 | 141 | try handlerOpt match 142 | case None => 143 | exchange.sendResponseHeaders(404, -1) 144 | case Some((handler, params)) => 145 | val length = Option(exchange.getRequestHeaders.getFirst("Content-Length")).map(_.toInt).getOrElse(0) 146 | val body = readBody(length) 147 | val bodyStr = new String(body, java.nio.charset.StandardCharsets.UTF_8) 148 | println(s"matched ${uri} to handler ${handler.debug} with params ${params}\nbody: ${bodyStr}") 149 | 150 | handler.exchange(params, bodyStr) match 151 | case Left(errExchange) => 152 | // TODO: in real world you would encode the data type to the error format 153 | errExchange match 154 | case None => 155 | exchange.sendResponseHeaders(500, -1) 156 | case Some(response) => 157 | exchange.sendResponseHeaders(500, response.length) 158 | exchange.getResponseBody.write(response) 159 | case Right(response) => 160 | response match 161 | case None => 162 | exchange.sendResponseHeaders(200, -1) 163 | case Some(response) => 164 | exchange.sendResponseHeaders(200, response.length) 165 | exchange.getResponseBody.write(response) 166 | finally 167 | exchange.close() 168 | 169 | class Handler[I <: Tuple, E, O](e: Endpoint[I, E, O], op: Func[I, E, O], exchange: Exchanger[I, E, O]): 170 | import serverlib.HttpService.model.* 171 | 172 | type Bundler = (params: Map[String, String], body: String) => Bundle 173 | type BundleArg = (params: Map[String, String], body: String) => String 174 | 175 | val template: Bundler = 176 | val readers: Array[BundleArg] = e.inputs 177 | .map[BundleArg]: i => 178 | (i.source: @unchecked) match 179 | case source.path() => 180 | val name = i.label 181 | (params, _) => params(name) 182 | case source.body() => 183 | (_, body) => body 184 | .toArray 185 | (params, body) => 186 | new: 187 | def arg(index: Int): String = readers(index)(params, body) 188 | 189 | def exchange(params: Map[String, String], body: String): Ser.Result = 190 | val bundle = template(params, body) 191 | exchange(bundle, op) 192 | 193 | def uriHandle(route: String): UriHandler = 194 | val elems = uriPattern(route) 195 | uri => optional: 196 | val uriElems = uri.split("/") 197 | val elemsIt = elems.iterator 198 | val uriIt = uriElems.iterator.filter(_.nonEmpty) 199 | var result = Map.empty[String, String] 200 | while elemsIt.hasNext && uriIt.hasNext do 201 | elemsIt.next() match 202 | case UriParts.Exact(str) => 203 | if uriIt.next() != str then optional.abort 204 | case UriParts.Wildcard(name) => 205 | result += (name -> uriIt.next()) 206 | if elemsIt.hasNext || uriIt.hasNext then optional.abort 207 | result 208 | 209 | def debug: String = e.route match 210 | case method.get(route) => s"GET ${route}" 211 | case method.post(route) => s"POST ${route}" 212 | case method.put(route) => s"PUT ${route}" 213 | 214 | def route: (HttpMethod, UriHandler) = e.route match 215 | case method.get(route) => (HttpMethod.Get, uriHandle(route)) 216 | case method.post(route) => (HttpMethod.Post, uriHandle(route)) 217 | case method.put(route) => (HttpMethod.Put, uriHandle(route)) 218 | 219 | class ServerBuilder(): 220 | private val handlers: ListBuffer[Handler[?, ?, ?]] = ListBuffer() 221 | 222 | def addEndpoint[I <: Tuple, E, O](handler: Handler[I, E, O]): this.type = 223 | handlers += handler 224 | this 225 | 226 | def create(port: Int): Server = 227 | val server = HttpServer.create() 228 | val handlers0 = handlers.toList 229 | server.bind(new java.net.InetSocketAddress(port), 0) 230 | val _ = server.createContext("/", rootHandler(handlers0)) 231 | server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()) 232 | server.start() 233 | Server(server) 234 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------