Skip to content

Commit

Permalink
Merge pull request #18 from KacperFKorban/publishing
Browse files Browse the repository at this point in the history
Publishing
  • Loading branch information
KacperFKorban authored Mar 6, 2024
2 parents 19b5662 + f2fbab0 commit a158c17
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 245 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Release
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-20.04
steps:
- uses: actions/[email protected]
with:
fetch-depth: 0
- uses: olafurpg/setup-scala@v13
- run: sbt ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
6 changes: 4 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
val scala3 = "3.3.3"

val commonSettings = Seq(
organization := "dev.korban",
organization := "io.github.kacperfkorban",
description := "PoC library to turn Scala 3 functions into UI forms with a single line of code",
homepage := Some(url("https://github.com/KacperFKorban/GUInep")),
licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
Expand All @@ -14,7 +14,6 @@ val commonSettings = Seq(
)
),
scalaVersion := scala3,
// TODO(kπ) enable all macro checks
scalacOptions ++= Seq(
// "-Xcheck-macros",
// "-Ycheck:inlining",
Expand All @@ -30,6 +29,7 @@ val commonSettings = Seq(

lazy val root = project
.in(file("."))
.settings(commonSettings)
.settings(
name := "GUInep-root",
publish / skip := true
Expand All @@ -38,13 +38,15 @@ lazy val root = project

lazy val guinep = projectMatrix
.in(file("guinep"))
.settings(commonSettings)
.settings(
name := "GUInep"
)
.jvmPlatform(scalaVersions = List(scala3))

lazy val web = projectMatrix
.in(file("web"))
.settings(commonSettings)
.settings(
name := "GUInep-web",
libraryDependencies ++= Seq(
Expand Down
219 changes: 111 additions & 108 deletions guinep/src/main/scala/macros.scala
Original file line number Diff line number Diff line change
@@ -1,133 +1,136 @@
package guinep.internal
package guinep

import guinep.model.*
import scala.quoted.*

inline def scriptInfos(inline fs: Any): Seq[Script] =
${ Macros.scriptInfosImpl('fs) }
private[guinep] object macros {
inline def scriptInfos(inline fs: Any): Seq[Script] =
${ Macros.scriptInfosImpl('fs) }

object Macros {
def scriptInfosImpl(fs: Expr[Any])(using Quotes): Expr[Seq[Script]] =
Macros().scriptInfosImpl(fs)
}

class Macros(using Quotes) {
import quotes.reflect.*

private def getMostInnerApply(term: Term): Option[String] = term match {
case Apply(fun, _) => getMostInnerApply(fun)
case TypeApply(fun, _) => getMostInnerApply(fun)
case Ident(name) => Some(name)
case _ => None
object Macros {
def scriptInfosImpl(fs: Expr[Any])(using Quotes): Expr[Seq[Script]] =
Macros().scriptInfosImpl(fs)
}

def wrongParamsListError(f: Expr[Any]): Nothing =
report.errorAndAbort(s"Wrong params list, expected a function reference, got: ${f.show}", f.asTerm.pos)
class Macros(using Quotes) {
import quotes.reflect.*

private def unsupportedFunctionParamType(t: TypeRepr, pos: Position): Nothing =
report.errorAndAbort(s"Unsupported function param type: ${t.show}", pos)
private def getMostInnerApply(term: Term): Option[String] = term match {
case Apply(fun, _) => getMostInnerApply(fun)
case TypeApply(fun, _) => getMostInnerApply(fun)
case Ident(name) => Some(name)
case _ => None
}

extension (t: Term)
private def select(s: Term): Term = Select(t, s.symbol)
private def select(s: String): Term =
t.select(t.tpe.typeSymbol.methodMember(s).head)
def wrongParamsListError(f: Expr[Any]): Nothing =
report.errorAndAbort(s"Wrong params list, expected a function reference, got: ${f.show}", f.asTerm.pos)

private def unsupportedFunctionParamType(t: TypeRepr, pos: Position): Nothing =
report.errorAndAbort(s"Unsupported function param type: ${t.show}", pos)

extension (t: Term)
private def select(s: Term): Term = Select(t, s.symbol)
private def select(s: String): Term =
t.select(t.tpe.typeSymbol.methodMember(s).head)

private def functionNameImpl(f: Expr[Any]): Expr[String] = {
val name = f.asTerm match {
case Inlined(_, _, Lambda(_, body)) =>
getMostInnerApply(body).getOrElse(wrongParamsListError(f))
case Lambda(_, body) =>
getMostInnerApply(body).getOrElse(wrongParamsListError(f))
case _ =>
wrongParamsListError(f)
}
Expr(name)
}

private def functionNameImpl(f: Expr[Any]): Expr[String] = {
val name = f.asTerm match {
case Inlined(_, _, Lambda(_, body)) =>
getMostInnerApply(body).getOrElse(wrongParamsListError(f))
case Lambda(_, body) =>
getMostInnerApply(body).getOrElse(wrongParamsListError(f))
private def functionParams(f: Expr[Any]): Seq[ValDef] = f.asTerm match {
case Lambda(params, body) =>
params.map (param => param)
case _ =>
wrongParamsListError(f)
}
Expr(name)
}

private def functionParams(f: Expr[Any]): Seq[ValDef] = f.asTerm match {
case Lambda(params, body) =>
params.map (param => param)
case _ =>
wrongParamsListError(f)
}
private def functionFormElementFromTree(tree: Tree): FormElement = tree match {
case ValDef(name, tpt, _) =>
val paramType = tpt.tpe
val paramName = name
paramType match {
case ntpe: NamedType if ntpe.name == "String" => FormElement.TextInput(paramName)
case ntpe: NamedType if ntpe.name == "Int" => FormElement.NumberInput(paramName)
case ntpe: NamedType if ntpe.name == "Boolean" => FormElement.CheckboxInput(paramName)
case ntpe: NamedType =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramType, tree.pos))
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree)
FormElement.FieldSet(paramName, fields.map(functionFormElementFromTree))
case _ => unsupportedFunctionParamType(paramType, tree.pos)
}
}

private def functionFormElementFromTree(tree: Tree): FormElement = tree match {
case ValDef(name, tpt, _) =>
val paramType = tpt.tpe
val paramName = name
paramType match {
case ntpe: NamedType if ntpe.name == "String" => FormElement.TextInput(paramName)
case ntpe: NamedType if ntpe.name == "Int" => FormElement.NumberInput(paramName)
case ntpe: NamedType if ntpe.name == "Boolean" => FormElement.CheckboxInput(paramName)
private def functionFormElementsImpl(f: Expr[Any]): Expr[Seq[FormElement]] = {
Expr.ofSeq(
functionParams(f).map(functionFormElementFromTree).map(Expr(_))
)
}

private def constructArg(paramTpe: TypeRepr, param: Term): Term = {
paramTpe match {
case ntpe: NamedType if ntpe.name == "String" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Int" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Boolean" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramType, tree.pos))
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramTpe, param.pos))
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree)
FormElement.FieldSet(paramName, fields.map(functionFormElementFromTree))
case _ => unsupportedFunctionParamType(paramType, tree.pos)
val paramValue = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm
val args = fields.collect { case field: ValDef =>
val fieldName = field.asInstanceOf[ValDef].name
val fieldValue = paramValue.select("apply").appliedTo(Literal(StringConstant(fieldName)))
constructArg(field.tpt.tpe, fieldValue)
}
New(Inferred(ntpe)).select(classSymbol.primaryConstructor).appliedToArgs(args)
case _ => unsupportedFunctionParamType(paramTpe, param.pos)
}
}

private def functionFormElementsImpl(f: Expr[Any]): Expr[Seq[FormElement]] = {
Expr.ofSeq(
functionParams(f).map(functionFormElementFromTree).map(Expr(_))
)
}

private def constructArg(paramTpe: TypeRepr, param: Term): Term = {
paramTpe match {
case ntpe: NamedType if ntpe.name == "String" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Int" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Boolean" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramTpe, param.pos))
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree)
val paramValue = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm
val args = fields.collect { case field: ValDef =>
val fieldName = field.asInstanceOf[ValDef].name
val fieldValue = paramValue.select("apply").appliedTo(Literal(StringConstant(fieldName)))
constructArg(field.tpt.tpe, fieldValue)
}
New(Inferred(ntpe)).select(classSymbol.primaryConstructor).appliedToArgs(args)
case _ => unsupportedFunctionParamType(paramTpe, param.pos)
}
}

private def functionRunImpl(f: Expr[Any]): Expr[List[Any] => String] = {
val fTerm = f.asTerm
f.asTerm match {
case l@Lambda(params, body) =>
/* (params: List[Any]) => l.apply(params(0).asInstanceOf[String], params(1).asInstanceOf[Int], ...) */
Lambda(
Symbol.spliceOwner,
MethodType(List("inputs"))(_ => List(TypeRepr.of[List[Any]]), _ => TypeRepr.of[String]),
{ case (sym, List(params: Term)) =>
l.select("apply").appliedToArgs(
functionParams(f).zipWithIndex.map { case (valdef, i) =>
val paramTpe = valdef.tpt.tpe
val param = params.select("apply").appliedTo(Literal(IntConstant(i)))
constructArg(paramTpe, param)
}.toList
).select("toString").appliedToNone
}
).asExprOf[List[Any] => String]
case _ =>
wrongParamsListError(f)
private def functionRunImpl(f: Expr[Any]): Expr[List[Any] => String] = {
val fTerm = f.asTerm
f.asTerm match {
case l@Lambda(params, body) =>
/* (params: List[Any]) => l.apply(params(0).asInstanceOf[String], params(1).asInstanceOf[Int], ...) */
Lambda(
Symbol.spliceOwner,
MethodType(List("inputs"))(_ => List(TypeRepr.of[List[Any]]), _ => TypeRepr.of[String]),
{ case (sym, List(params: Term)) =>
l.select("apply").appliedToArgs(
functionParams(f).zipWithIndex.map { case (valdef, i) =>
val paramTpe = valdef.tpt.tpe
val param = params.select("apply").appliedTo(Literal(IntConstant(i)))
constructArg(paramTpe, param)
}.toList
).select("toString").appliedToNone
}
).asExprOf[List[Any] => String]
case _ =>
wrongParamsListError(f)
}
}
}

def scriptInfosImpl(fs: Expr[Any]): Expr[Seq[Script]] = {
val functions = fs match {
case Varargs(args) => args
case _ => wrongParamsListError(fs)
def scriptInfosImpl(fs: Expr[Any]): Expr[Seq[Script]] = {
val functions = fs match {
case Varargs(args) => args
case _ => wrongParamsListError(fs)
}
if (functions.isEmpty)
report.errorAndAbort("No functions provided", fs.asTerm.pos)
Expr.ofSeq(functions.map(scriptInfoImpl))
}
if (functions.isEmpty)
report.errorAndAbort("No functions provided", fs.asTerm.pos)
Expr.ofSeq(functions.map(scriptInfoImpl))
}

def scriptInfoImpl(f: Expr[Any]): Expr[Script] = {
val name = functionNameImpl(f)
val params = functionFormElementsImpl(f)
val run = functionRunImpl(f)
'{ Script($name, $params, $run) }
def scriptInfoImpl(f: Expr[Any]): Expr[Script] = {
val name = functionNameImpl(f)
val params = functionFormElementsImpl(f)
val run = functionRunImpl(f)
'{ Script($name, $params, $run) }
}
}
}
Loading

0 comments on commit a158c17

Please sign in to comment.