Skip to content

Commit 3426a7a

Browse files
committed
add CI
1 parent 06585d0 commit 3426a7a

File tree

5 files changed

+200
-53
lines changed

5 files changed

+200
-53
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
power: true
21+
22+
- name: Check formatting
23+
run: scala-cli fmt src project.scala --check
24+
25+
- name: Run unit tests
26+
run: scala-cli test src project.scala --cross

.scalafmt.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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

project.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//> using scala 3.3.3
2+
//> using jvm 8
3+
//> using exclude "${.}/examples/*"
4+
//> using test.dep org.scalameta::munit::1.0.0
25
//> using publish.organization io.github.bishabosha
36
//> using publish.name ops-mirror
47
//> using publish.computeVersion git:tag
5-
//> using exclude "${.}/examples/*"
6-
//> using jvm 8
78
//> using publish.repository central-s01
89
//> using publish.license "Apache-2.0"
910
//> using publish.url "https://github.com/bishabosha/ops-mirror"

src/macros/OpsMirror.scala

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import quoted.*
55
import scala.util.chaining.given
66
import scala.annotation.implicitNotFound
77

8-
@implicitNotFound("No OpsMirror could be generated.\nDiagnose any issues by calling OpsMirror.reify[T] directly")
8+
@implicitNotFound(
9+
"No OpsMirror could be generated.\nDiagnose any issues by calling OpsMirror.reify[T] directly"
10+
)
911
sealed trait OpsMirror:
1012
type Metadata <: Tuple
1113
type MirroredType
1214
type MirroredLabel
1315
type MirroredOperations <: Tuple
1416
type MirroredOperationLabels <: Tuple
17+
end OpsMirror
1518

1619
sealed trait Meta
1720

@@ -28,6 +31,7 @@ sealed trait Operation:
2831
type InputMetadatas <: Tuple
2932
type ErrorType
3033
type OutputType
34+
end Operation
3135

3236
object OpsMirror:
3337
type Of[T] = OpsMirror { type MirroredType = T }
@@ -38,7 +42,7 @@ object OpsMirror:
3842

3943
def typesFromTuple[Ts: Type](using Quotes): List[Type[?]] =
4044
Type.of[Ts] match
41-
case '[t *: ts] => Type.of[t] :: typesFromTuple[ts]
45+
case '[t *: ts] => Type.of[t] :: typesFromTuple[ts]
4246
case '[EmptyTuple] => Nil
4347

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

5461
def typesToTuple(list: List[Type[?]])(using Quotes): Type[?] =
5562
val empty: Type[? <: Tuple] = Type.of[EmptyTuple]
56-
list.foldRight(empty)({case ('[t], '[acc]) => Type.of[t *: (acc & Tuple)]})
63+
list.foldRight(empty)({ case ('[t], '[acc]) =>
64+
Type.of[t *: (acc & Tuple)]
65+
})
66+
end typesToTuple
5767

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

6575
def extractMetas[Metadata: Type]: List[Expr[Any]] =
6676
typesFromTuple[Metadata].map:
67-
case '[m] => TypeRepr.of[m] match
68-
case AnnotatedType(_, annot) =>
69-
annot.asExpr
70-
case tpe =>
71-
report.errorAndAbort(s"got the metadata element ${tpe.show}")
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}")
7283

7384
Type.of[Op] match
7485
case '[Operation {
75-
type Metadata = metadata
76-
type InputMetadatas = inputMetadatas
77-
}] => Metadata(extractMetas[metadata], extractMetass[inputMetadatas])
86+
type Metadata = metadata
87+
type InputMetadatas = inputMetadatas
88+
}] =>
89+
Metadata(extractMetas[metadata], extractMetass[inputMetadatas])
7890
case _ => report.errorAndAbort("expected an Operation with Metadata.")
91+
end match
92+
end metadata
7993

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

83-
val tpe = TypeRepr.of[T]
84-
val cls = tpe.classSymbol.get
85-
val decls = cls.declaredMethods
97+
val tpe = TypeRepr.of[T]
98+
val cls = tpe.classSymbol.get
99+
val decls = cls.declaredMethods
86100
val labels = decls.map(m => ConstantType(StringConstant(m.name)))
87101

88102
def isMeta(annot: Term): Boolean =
89103
if annot.tpe <:< TypeRepr.of[MetaAnnotation] then true
90-
else if annot.tpe <:< TypeRepr.of[scala.annotation.internal.SourceFile] then false
104+
else if annot.tpe <:< TypeRepr.of[scala.annotation.internal.SourceFile]
105+
then false
91106
else
92-
report.error(s"annotation ${annot.show} does not extend ${Type.show[MetaAnnotation]}", annot.pos)
107+
report.error(
108+
s"annotation ${annot.show} does not extend ${Type.show[MetaAnnotation]}",
109+
annot.pos
110+
)
93111
false
94112

95-
def encodeMeta(annot: Term): Type[?] = AnnotatedType(TypeRepr.of[Meta], annot).asType
113+
def encodeMeta(annot: Term): Type[?] =
114+
AnnotatedType(TypeRepr.of[Meta], annot).asType
96115

97116
val (errorTpe, gmeta) =
98117
val annots = cls.annotations.filter(isMeta)
99-
val (errorAnnots, metaAnnots) = annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
118+
val (errorAnnots, metaAnnots) =
119+
annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
100120
val errorTpe =
101-
if errorAnnots.isEmpty then
102-
Type.of[VoidType]
121+
if errorAnnots.isEmpty then Type.of[VoidType]
103122
else
104123
errorAnnots
105124
.map: annot =>
106125
annot.asExpr match
107126
case '{ $a: ErrorAnnotation[t] } => Type.of[t]
108127
.head
109128
(errorTpe, metaAnnots.map(encodeMeta))
129+
end val
110130

111131
val ops = decls.map(method =>
112132
val metaAnnots =
113133
val annots = method.annotations.filter(isMeta)
114-
val (errorAnnots, metaAnnots) = annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
134+
val (errorAnnots, metaAnnots) =
135+
annots.partition(annot => annot.tpe <:< TypeRepr.of[ErrorAnnotation[?]])
115136
if errorAnnots.nonEmpty then
116137
errorAnnots.foreach: annot =>
117-
report.error(s"error annotation ${annot.show} has no meaning on a method, annotate the scope itself.", annot.pos)
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
118143
metaAnnots.map(encodeMeta)
144+
end metaAnnots
119145
val meta = typesToTuple(metaAnnots)
120146
val (inputTypes, inputLabels, inputMetas, output) =
121147
tpe.memberType(method) match
122148
case ByNameType(res) =>
123149
val output = res.asType
124150
(Nil, Nil, Nil, output)
125151
case MethodType(paramNames, paramTpes, res) =>
126-
val inputTypes = paramTpes.map(_.asType)
152+
val inputTypes = paramTpes.map(_.asType)
127153
val inputLabels = paramNames.map(l => ConstantType(StringConstant(l)).asType)
128-
val inputMetas = method.paramSymss.head.map(s => typesToTuple(s.annotations.filter(isMeta).map(encodeMeta)))
154+
val inputMetas = method.paramSymss.head.map: s =>
155+
typesToTuple(s.annotations.filter(isMeta).map(encodeMeta))
129156
val output = res match
130-
case _: MethodType => report.errorAndAbort(s"curried method ${method.name} is not supported")
131-
case _: PolyType => report.errorAndAbort(s"curried method ${method.name} is not supported")
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")
132161
case _ => res.asType
133162
(inputTypes, inputLabels, inputMetas, output)
134-
case _: PolyType => report.errorAndAbort(s"generic method ${method.name} is not supported")
163+
case _: PolyType =>
164+
report.errorAndAbort(s"generic method ${method.name} is not supported")
135165
val inTup = typesToTuple(inputTypes)
136166
val inLab = typesToTuple(inputLabels)
137167
val inMet = typesToTuple(inputMetas)
138168
(meta, inTup, inLab, inMet, errorTpe, output) match
139-
case ('[m], '[i], '[l], '[iM], '[e], '[o]) => Type.of[Operation {
140-
type Metadata = m
141-
type InputTypes = i
142-
type InputLabels = l
143-
type InputMetadatas = iM
144-
type ErrorType = e
145-
type OutputType = o
146-
}]
147-
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
148181
)
149-
val clsMeta = typesToTuple(gmeta)
150-
val opsTup = typesToTuple(ops.toList)
182+
val clsMeta = typesToTuple(gmeta)
183+
val opsTup = typesToTuple(ops.toList)
151184
val labelsTup = typesToTuple(labels.map(_.asType))
152-
val name = ConstantType(StringConstant(cls.name)).asType
185+
val name = ConstantType(StringConstant(cls.name)).asType
153186
(clsMeta, opsTup, labelsTup, name) match
154-
case ('[meta], '[ops], '[labels], '[label]) => '{ (new OpsMirror {
155-
type Metadata = meta & Tuple
156-
type MirroredType = T
157-
type MirroredLabel = label
158-
type MirroredOperations = ops & Tuple
159-
type MirroredOperationLabels = labels & Tuple
160-
}): OpsMirror.Of[T] {
161-
type MirroredLabel = label
162-
type MirroredOperations = ops & Tuple
163-
type MirroredOperationLabels = labels & Tuple
164-
}}
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

src/test/OpsMirrorSuite.scala

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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

0 commit comments

Comments
 (0)