Skip to content

Commit

Permalink
Merge pull request #3 from bishabosha/add-CI
Browse files Browse the repository at this point in the history
add CI
  • Loading branch information
bishabosha authored Jun 23, 2024
2 parents 42d82a7 + c451081 commit b693adc
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 53 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI
on:
push:
branches:
- main
tags:
- "v*"
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: coursier/cache-action@v6.4
- uses: VirtusLab/scala-cli-setup@v1.3.2
with:
jvm: "8"
power: true

- name: Check formatting
run: scala-cli fmt src project.scala --check

- name: Run unit tests
run: scala-cli test src project.scala --cross
10 changes: 10 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version = "3.7.14"
runner.dialect = scala3
align.preset = more
maxColumn = 100
indent.fewerBraces = never
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = yes
rewrite.scala3.insertEndMarkerMinLines = 5
verticalMultiline.atDefnSite = true
newlines.usingParamListModifierPrefer = before
5 changes: 3 additions & 2 deletions project.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//> using scala 3.3.3
//> using jvm 8
//> using exclude "${.}/examples/*"
//> using test.dep org.scalameta::munit::1.0.0
//> using publish.organization io.github.bishabosha
//> using publish.name ops-mirror
//> using publish.computeVersion git:tag
//> using exclude "${.}/examples/*"
//> using jvm 8
//> using publish.repository central-s01
//> using publish.license "Apache-2.0"
//> using publish.url "https://github.com/bishabosha/ops-mirror"
Expand Down
141 changes: 90 additions & 51 deletions src/macros/OpsMirror.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import quoted.*
import scala.util.chaining.given
import scala.annotation.implicitNotFound

@implicitNotFound("No OpsMirror could be generated.\nDiagnose any issues by calling OpsMirror.reify[T] directly")
@implicitNotFound(
"No OpsMirror could be generated.\nDiagnose any issues by calling OpsMirror.reify[T] directly"
)
sealed trait OpsMirror:
type Metadata <: Tuple
type MirroredType
type MirroredLabel
type MirroredOperations <: Tuple
type MirroredOperationLabels <: Tuple
end OpsMirror

sealed trait Meta

Expand All @@ -28,6 +31,7 @@ sealed trait Operation:
type InputMetadatas <: Tuple
type ErrorType
type OutputType
end Operation

object OpsMirror:
type Of[T] = OpsMirror { type MirroredType = T }
Expand All @@ -38,7 +42,7 @@ object OpsMirror:

def typesFromTuple[Ts: Type](using Quotes): List[Type[?]] =
Type.of[Ts] match
case '[t *: ts] => Type.of[t] :: typesFromTuple[ts]
case '[t *: ts] => Type.of[t] :: typesFromTuple[ts]
case '[EmptyTuple] => Nil

def stringsFromTuple[Ts: Type](using Quotes): List[String] =
Expand All @@ -49,11 +53,17 @@ object OpsMirror:
import quotes.reflect.*
TypeRepr.of[T] match
case ConstantType(StringConstant(label)) => label
case _ => report.errorAndAbort(s"expected a constant string, got ${TypeRepr.of[T]}")
case _ =>
report.errorAndAbort(s"expected a constant string, got ${TypeRepr.of[T]}")
end match
end stringFromType

def typesToTuple(list: List[Type[?]])(using Quotes): Type[?] =
val empty: Type[? <: Tuple] = Type.of[EmptyTuple]
list.foldRight(empty)({case ('[t], '[acc]) => Type.of[t *: (acc & Tuple)]})
list.foldRight(empty)({ case ('[t], '[acc]) =>
Type.of[t *: (acc & Tuple)]
})
end typesToTuple

def metadata[Op: Type](using Quotes): Metadata =
import quotes.reflect.*
Expand All @@ -64,101 +74,130 @@ object OpsMirror:

def extractMetas[Metadata: Type]: List[Expr[Any]] =
typesFromTuple[Metadata].map:
case '[m] => TypeRepr.of[m] match
case AnnotatedType(_, annot) =>
annot.asExpr
case tpe =>
report.errorAndAbort(s"got the metadata element ${tpe.show}")
case '[m] =>
TypeRepr.of[m] match
case AnnotatedType(_, annot) =>
annot.asExpr
case tpe =>
report.errorAndAbort(s"got the metadata element ${tpe.show}")

Type.of[Op] match
case '[Operation {
type Metadata = metadata
type InputMetadatas = inputMetadatas
}] => Metadata(extractMetas[metadata], extractMetass[inputMetadatas])
type Metadata = metadata
type InputMetadatas = inputMetadatas
}] =>
Metadata(extractMetas[metadata], extractMetass[inputMetadatas])
case _ => report.errorAndAbort("expected an Operation with Metadata.")
end match
end metadata

private def reifyImpl[T: Type](using Quotes): Expr[Of[T]] =
import quotes.reflect.*

val tpe = TypeRepr.of[T]
val cls = tpe.classSymbol.get
val decls = cls.declaredMethods
val tpe = TypeRepr.of[T]
val cls = tpe.classSymbol.get
val decls = cls.declaredMethods
val labels = decls.map(m => ConstantType(StringConstant(m.name)))

def isMeta(annot: Term): Boolean =
if annot.tpe <:< TypeRepr.of[MetaAnnotation] then true
else if annot.tpe <:< TypeRepr.of[scala.annotation.internal.SourceFile] then false
else if annot.tpe <:< TypeRepr.of[scala.annotation.internal.SourceFile]
then false
else
report.error(s"annotation ${annot.show} does not extend ${Type.show[MetaAnnotation]}", annot.pos)
report.error(
s"annotation ${annot.show} does not extend ${Type.show[MetaAnnotation]}",
annot.pos
)
false

def encodeMeta(annot: Term): Type[?] = AnnotatedType(TypeRepr.of[Meta], annot).asType
def encodeMeta(annot: Term): Type[?] =
AnnotatedType(TypeRepr.of[Meta], annot).asType

val (errorTpe, gmeta) =
val annots = cls.annotations.filter(isMeta)
val (errorAnnots, metaAnnots) = annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
val (errorAnnots, metaAnnots) =
annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
val errorTpe =
if errorAnnots.isEmpty then
Type.of[VoidType]
if errorAnnots.isEmpty then Type.of[VoidType]
else
errorAnnots
.map: annot =>
annot.asExpr match
case '{ $a: ErrorAnnotation[t] } => Type.of[t]
.head
(errorTpe, metaAnnots.map(encodeMeta))
end val

val ops = decls.map(method =>
val metaAnnots =
val annots = method.annotations.filter(isMeta)
val (errorAnnots, metaAnnots) = annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
val (errorAnnots, metaAnnots) =
annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
if errorAnnots.nonEmpty then
errorAnnots.foreach: annot =>
report.error(s"error annotation ${annot.show} has no meaning on a method, annotate the scope itself.", annot.pos)
report.error(
s"error annotation ${annot.show} has no meaning on a method, annotate the scope itself.",
annot.pos
)
end if
metaAnnots.map(encodeMeta)
end metaAnnots
val meta = typesToTuple(metaAnnots)
val (inputTypes, inputLabels, inputMetas, output) =
tpe.memberType(method) match
case ByNameType(res) =>
val output = res.asType
(Nil, Nil, Nil, output)
case MethodType(paramNames, paramTpes, res) =>
val inputTypes = paramTpes.map(_.asType)
val inputTypes = paramTpes.map(_.asType)
val inputLabels = paramNames.map(l => ConstantType(StringConstant(l)).asType)
val inputMetas = method.paramSymss.head.map(s => typesToTuple(s.annotations.filter(isMeta).map(encodeMeta)))
val inputMetas = method.paramSymss.head.map: s =>
typesToTuple(s.annotations.filter(isMeta).map(encodeMeta))
val output = res match
case _: MethodType => report.errorAndAbort(s"curried method ${method.name} is not supported")
case _: PolyType => report.errorAndAbort(s"curried method ${method.name} is not supported")
case _: MethodType =>
report.errorAndAbort(s"curried method ${method.name} is not supported")
case _: PolyType =>
report.errorAndAbort(s"curried method ${method.name} is not supported")
case _ => res.asType
(inputTypes, inputLabels, inputMetas, output)
case _: PolyType => report.errorAndAbort(s"generic method ${method.name} is not supported")
case _: PolyType =>
report.errorAndAbort(s"generic method ${method.name} is not supported")
val inTup = typesToTuple(inputTypes)
val inLab = typesToTuple(inputLabels)
val inMet = typesToTuple(inputMetas)
(meta, inTup, inLab, inMet, errorTpe, output) match
case ('[m], '[i], '[l], '[iM], '[e], '[o]) => Type.of[Operation {
type Metadata = m
type InputTypes = i
type InputLabels = l
type InputMetadatas = iM
type ErrorType = e
type OutputType = o
}]

case ('[m], '[i], '[l], '[iM], '[e], '[o]) =>
Type.of[
Operation {
type Metadata = m
type InputTypes = i
type InputLabels = l
type InputMetadatas = iM
type ErrorType = e
type OutputType = o
}
]
end match
)
val clsMeta = typesToTuple(gmeta)
val opsTup = typesToTuple(ops.toList)
val clsMeta = typesToTuple(gmeta)
val opsTup = typesToTuple(ops.toList)
val labelsTup = typesToTuple(labels.map(_.asType))
val name = ConstantType(StringConstant(cls.name)).asType
val name = ConstantType(StringConstant(cls.name)).asType
(clsMeta, opsTup, labelsTup, name) match
case ('[meta], '[ops], '[labels], '[label]) => '{ (new OpsMirror {
type Metadata = meta & Tuple
type MirroredType = T
type MirroredLabel = label
type MirroredOperations = ops & Tuple
type MirroredOperationLabels = labels & Tuple
}): OpsMirror.Of[T] {
type MirroredLabel = label
type MirroredOperations = ops & Tuple
type MirroredOperationLabels = labels & Tuple
}}
case ('[meta], '[ops], '[labels], '[label]) =>
'{
(new OpsMirror:
type Metadata = meta & Tuple
type MirroredType = T
type MirroredLabel = label
type MirroredOperations = ops & Tuple
type MirroredOperationLabels = labels & Tuple
): OpsMirror.Of[T] {
type MirroredLabel = label
type MirroredOperations = ops & Tuple
type MirroredOperationLabels = labels & Tuple
}
}
end match
end reifyImpl
end OpsMirror
71 changes: 71 additions & 0 deletions src/test/OpsMirrorSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package example

import mirrorops.OpsMirror
import mirrorops.Meta
import compiletime.constValue
import compiletime.constValueTuple

class OpsMirrorSuite extends munit.FunSuite:
import OpsMirrorSuite.*
import OpMeta.*
import ParamMeta.*

class failsWith[E] extends mirrorops.ErrorAnnotation[E]

enum BasicError:
case Message(msg: String)

enum OpMeta extends mirrorops.MetaAnnotation:
case Streaming()
case JSONBody()

enum ParamMeta extends mirrorops.MetaAnnotation:
case PrimaryKey()

@failsWith[BasicError]
trait BasicService:
@Streaming
@JSONBody
def lookup(@PrimaryKey id: Long): String
end BasicService

test("summon mirror basic with annotations") {
val mirror = summon[OpsMirror.Of[BasicService]]

type FirstOp = Tuple.Head[mirror.MirroredOperations]

summon[mirror.MirroredLabel =:= "BasicService"]
summon[mirror.MirroredOperationLabels =:= ("lookup" *: EmptyTuple)]
summon[Operation_Metadata[FirstOp] =:= (Meta @JSONBody, Meta @Streaming)]
summon[Operation_InputLabels[FirstOp] =:= ("id" *: EmptyTuple)]
summon[Operation_InputTypes[FirstOp] =:= (Long *: EmptyTuple)]
summon[
Operation_InputMetadatas[FirstOp] =:= ((Meta @PrimaryKey *: EmptyTuple) *: EmptyTuple)
]
summon[Operation_ErrorType[FirstOp] =:= BasicError]
summon[Operation_OutputType[FirstOp] =:= String]
}
end OpsMirrorSuite

object OpsMirrorSuite:
type Operation_Is[Ls <: Tuple] = mirrorops.Operation { type InputTypes = Ls }
type Operation_Im[Ls <: Tuple] = mirrorops.Operation {
type InputMetadatas = Ls
}
type Operation_M[Ls <: Tuple] = mirrorops.Operation { type Metadata = Ls }
type Operation_Et[E] = mirrorops.Operation { type ErrorType = E }
type Operation_Ot[T] = mirrorops.Operation { type OutputType = T }
type Operation_IL[Ls <: Tuple] = mirrorops.Operation { type InputLabels = Ls }
type Operation_InputLabels[Op] = Op match
case Operation_IL[ls] => ls
type Operation_InputTypes[Op] = Op match
case Operation_Is[ls] => ls
type Operation_ErrorType[Op] = Op match
case Operation_Et[ls] => ls
type Operation_OutputType[Op] = Op match
case Operation_Ot[ls] => ls
type Operation_InputMetadatas[Op] = Op match
case Operation_Im[ls] => ls
type Operation_Metadata[Op] = Op match
case Operation_M[ls] => ls
end OpsMirrorSuite

0 comments on commit b693adc

Please sign in to comment.