From adf85b8c515513a1cef469bcfdd3dd59bc7e5163 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Wed, 6 Mar 2024 15:45:43 +0100 Subject: [PATCH 1/3] Hide implementation details --- build.sbt | 7 +- guinep/src/main/scala/macros.scala | 216 +++++++++--------- web/src/main/scala/api.scala | 4 +- web/src/main/scala/htmlgen.scala | 6 +- web/src/main/scala/serialization.scala | 67 +++--- web/src/main/scala/web.scala | 44 ---- web/src/main/scala/webgen.scala | 48 ++++ .../scala/compiletest.scala} | 2 +- 8 files changed, 198 insertions(+), 196 deletions(-) delete mode 100644 web/src/main/scala/web.scala create mode 100644 web/src/main/scala/webgen.scala rename web/src/{main/scala/Main.scala => test/scala/compiletest.scala} (95%) diff --git a/build.sbt b/build.sbt index b9978b5..5c0a825 100644 --- a/build.sbt +++ b/build.sbt @@ -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")), @@ -14,10 +14,9 @@ val commonSettings = Seq( ) ), scalaVersion := scala3, - // TODO(kπ) enable all macro checks scalacOptions ++= Seq( - // "-Xcheck-macros", - // "-Ycheck:inlining", + "-Xcheck-macros", + "-Ycheck:inlining", "-explain", "-deprecation", "-unchecked", diff --git a/guinep/src/main/scala/macros.scala b/guinep/src/main/scala/macros.scala index 9252186..b852fe9 100644 --- a/guinep/src/main/scala/macros.scala +++ b/guinep/src/main/scala/macros.scala @@ -2,132 +2,134 @@ package guinep.internal 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) } + } } } diff --git a/web/src/main/scala/api.scala b/web/src/main/scala/api.scala index b7f632f..00a9898 100644 --- a/web/src/main/scala/api.scala +++ b/web/src/main/scala/api.scala @@ -1,7 +1,7 @@ package guinep inline def web(inline scripts: Any*): Unit = - val scriptInfos = internal.scriptInfos(scripts) + val scriptInfos = internal.macros.scriptInfos(scripts) val scriptInfosMap = scriptInfos.groupBy(_.name) if scriptInfosMap.exists(_._2.size > 1) then println( @@ -9,4 +9,4 @@ inline def web(inline scripts: Any*): Unit = |Ignoring duplicates""".stripMargin ) println("Starting GUInep web server at http://localhost:8090/scripts") - internal.genWeb(scriptInfosMap.mapValues(_.head).toMap) + internal.webgen.genWeb(scriptInfosMap.mapValues(_.head).toMap) diff --git a/web/src/main/scala/htmlgen.scala b/web/src/main/scala/htmlgen.scala index defd5dd..02346f8 100644 --- a/web/src/main/scala/htmlgen.scala +++ b/web/src/main/scala/htmlgen.scala @@ -5,12 +5,8 @@ import zio.* import zio.http.* import zio.http.template.* import zio.http.codec.* -import zio.json.* -import scala.util.chaining.* -import zio.json.ast.* -import zio.json.ast.Json.* -trait HtmlGen { +private[guinep] trait HtmlGen { val scripts: Map[String, Script] def generateHtml = html( diff --git a/web/src/main/scala/serialization.scala b/web/src/main/scala/serialization.scala index 75f4b28..0b27f47 100644 --- a/web/src/main/scala/serialization.scala +++ b/web/src/main/scala/serialization.scala @@ -7,39 +7,40 @@ import zio.json.ast.* import zio.json.ast.Json.* import scala.util.chaining.* -def sequenceEither[A, B](eithers: List[Either[A, B]]): Either[A, List[B]] = - eithers.foldLeft(Right(List.empty): Either[A, List[B]]) { (acc, e) => - acc.flatMap(a => e.map(b => a :+ b)) - } +private[guinep] object serialization: + def sequenceEither[A, B](eithers: List[Either[A, B]]): Either[A, List[B]] = + eithers.foldLeft(Right(List.empty): Either[A, List[B]]) { (acc, e) => + acc.flatMap(a => e.map(b => a :+ b)) + } -extension (formElements: List[FormElement]) - def parseJSONValue(value: Obj): Either[String, Map[String, Any]] = - formElements - .map { element => - val v = value.get(element.name).toRight(s"Missing value for ${element.name}").flatMap(element.parseJSONValue) - v.map(element.name -> _) - } - .pipe(sequenceEither).map(_.toMap) + extension (formElements: List[FormElement]) + def parseJSONValue(value: Obj): Either[String, Map[String, Any]] = + formElements + .map { element => + val v = value.get(element.name).toRight(s"Missing value for ${element.name}").flatMap(element.parseJSONValue) + v.map(element.name -> _) + } + .pipe(sequenceEither).map(_.toMap) - def sortByArgs(values: Map[String, Any]): List[Any] = - formElements.map { element => - values(element.name) - } + def sortByArgs(values: Map[String, Any]): List[Any] = + formElements.map { element => + values(element.name) + } -extension (formElement: FormElement) - def parseJSONValue(value: Json): Either[String, Any] = formElement match - case FormElement.FieldSet(name, elements) => - for { - m <- value.asObject.toRight(s"Invalid object: $value") - res <- elements.parseJSONValue(m) - } yield res - case FormElement.TextInput(_) => value.asString.toRight(s"Invalid string: $value") - case FormElement.NumberInput(_) => value.asString.flatMap(_.toIntOption).toRight(s"Invalid number: $value") - case FormElement.CheckboxInput(_) => value.asBoolean.toRight(s"Invalid boolean: $value") - case FormElement.Dropdown(_, options) => - for { - v <- value.asString.toRight(s"Invalid string: $value") - res <- options.find(_._1 == v).map(_._2).toRight(s"Invalid option: $value") - } yield res - case FormElement.TextArea(_, _, _) => Right(value) - case _ => Left(s"Unsupported form element: $formElement") \ No newline at end of file + extension (formElement: FormElement) + def parseJSONValue(value: Json): Either[String, Any] = formElement match + case FormElement.FieldSet(name, elements) => + for { + m <- value.asObject.toRight(s"Invalid object: $value") + res <- elements.parseJSONValue(m) + } yield res + case FormElement.TextInput(_) => value.asString.toRight(s"Invalid string: $value") + case FormElement.NumberInput(_) => value.asString.flatMap(_.toIntOption).toRight(s"Invalid number: $value") + case FormElement.CheckboxInput(_) => value.asBoolean.toRight(s"Invalid boolean: $value") + case FormElement.Dropdown(_, options) => + for { + v <- value.asString.toRight(s"Invalid string: $value") + res <- options.find(_._1 == v).map(_._2).toRight(s"Invalid option: $value") + } yield res + case FormElement.TextArea(_, _, _) => Right(value) + case _ => Left(s"Unsupported form element: $formElement") diff --git a/web/src/main/scala/web.scala b/web/src/main/scala/web.scala deleted file mode 100644 index 5a1720b..0000000 --- a/web/src/main/scala/web.scala +++ /dev/null @@ -1,44 +0,0 @@ -package guinep.internal - -import guinep.* -import zio.* -import zio.http.* -import zio.http.template.* -import zio.http.codec.* -import zio.json.* -import zio.json.ast.Json.* -import scala.util.chaining.* - -def genWeb(scripts: Map[String, Script]): Unit = { - val ws = WebServer(scripts) - val runtime = Runtime.default - Unsafe.unsafe { implicit unsafe => - runtime.unsafe.run(ws.run) - } -} - -class WebServer(val scripts: Map[String, Script]) extends HtmlGen { - val app: HttpApp[Any] = Routes( - Method.GET / PathCodec.empty -> - handler(Response.redirect(URL.root / "scripts", isPermanent = true)), - Method.GET / "scripts" -> - handler(Response.html(generateHtml)), - Method.GET / "scripts" / string("name") -> - handler(Response.html(generateHtml)), - Method.POST / "run" -> - handler { (req: Request) => - (for { - str <- req.body.asString - obj <- ZIO.fromEither(str.fromJson[Obj]) - scriptName <- ZIO.fromEither(obj.get("script@name").get.asString.toRight("Missing script name")) - script = scripts(scriptName) - inputsValuesMap <- ZIO.fromEither(script.inputs.toList.parseJSONValue(obj)) - inputsValues = script.inputs.toList.sortByArgs(inputsValuesMap) - result = script.run(inputsValues) - } yield Response.text(result)).onError(e => ZIO.debug(e.toString)) - } - ).sandbox.toHttpApp - - def run = - Server.serve(app).provide(Server.defaultWithPort(8090)) -} diff --git a/web/src/main/scala/webgen.scala b/web/src/main/scala/webgen.scala new file mode 100644 index 0000000..ecc8b7c --- /dev/null +++ b/web/src/main/scala/webgen.scala @@ -0,0 +1,48 @@ +package guinep.internal + +import guinep.* +import guinep.internal.serialization.* +import zio.* +import zio.http.* +import zio.http.template.* +import zio.http.codec.* +import zio.json.* +import zio.json.ast.Json.* +import scala.util.chaining.* + +private[guinep] object webgen { + + def genWeb(scripts: Map[String, Script]): Unit = { + val ws = WebServer(scripts) + val runtime = Runtime.default + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(ws.run) + } + } + + class WebServer(val scripts: Map[String, Script]) extends HtmlGen { + val app: HttpApp[Any] = Routes( + Method.GET / PathCodec.empty -> + handler(Response.redirect(URL.root / "scripts", isPermanent = true)), + Method.GET / "scripts" -> + handler(Response.html(generateHtml)), + Method.GET / "scripts" / string("name") -> + handler(Response.html(generateHtml)), + Method.POST / "run" -> + handler { (req: Request) => + (for { + str <- req.body.asString + obj <- ZIO.fromEither(str.fromJson[Obj]) + scriptName <- ZIO.fromEither(obj.get("script@name").get.asString.toRight("Missing script name")) + script = scripts(scriptName) + inputsValuesMap <- ZIO.fromEither(script.inputs.toList.parseJSONValue(obj)) + inputsValues = script.inputs.toList.sortByArgs(inputsValuesMap) + result = script.run(inputsValues) + } yield Response.text(result)).onError(e => ZIO.debug(e.toString)) + } + ).sandbox.toHttpApp + + def run = + Server.serve(app).provide(Server.defaultWithPort(8090)) + } +} diff --git a/web/src/main/scala/Main.scala b/web/src/test/scala/compiletest.scala similarity index 95% rename from web/src/main/scala/Main.scala rename to web/src/test/scala/compiletest.scala index faee363..8178f8b 100644 --- a/web/src/main/scala/Main.scala +++ b/web/src/test/scala/compiletest.scala @@ -1,4 +1,4 @@ -package guinep.testrun +package guinep.compiletest def personsAge(name: String): Int = name match { case "Bartek" => 20 From 4433ae397ff7a03a58c8b979b43cfa15ab903f82 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Wed, 6 Mar 2024 16:01:56 +0100 Subject: [PATCH 2/3] Cleanup config and structure for publishing --- build.sbt | 7 +- guinep/src/main/scala/macros.scala | 3 +- guinep/src/main/scala/model.scala | 90 +++++++++++++------------- web/src/main/scala/api.scala | 4 +- web/src/main/scala/htmlgen.scala | 3 +- web/src/main/scala/serialization.scala | 3 +- web/src/main/scala/webgen.scala | 5 +- 7 files changed, 62 insertions(+), 53 deletions(-) diff --git a/build.sbt b/build.sbt index 5c0a825..4824444 100644 --- a/build.sbt +++ b/build.sbt @@ -15,8 +15,8 @@ val commonSettings = Seq( ), scalaVersion := scala3, scalacOptions ++= Seq( - "-Xcheck-macros", - "-Ycheck:inlining", + // "-Xcheck-macros", + // "-Ycheck:inlining", "-explain", "-deprecation", "-unchecked", @@ -29,6 +29,7 @@ val commonSettings = Seq( lazy val root = project .in(file(".")) + .settings(commonSettings) .settings( name := "GUInep-root", publish / skip := true @@ -37,6 +38,7 @@ lazy val root = project lazy val guinep = projectMatrix .in(file("guinep")) + .settings(commonSettings) .settings( name := "GUInep" ) @@ -44,6 +46,7 @@ lazy val guinep = projectMatrix lazy val web = projectMatrix .in(file("web")) + .settings(commonSettings) .settings( name := "GUInep-web", libraryDependencies ++= Seq( diff --git a/guinep/src/main/scala/macros.scala b/guinep/src/main/scala/macros.scala index b852fe9..0893033 100644 --- a/guinep/src/main/scala/macros.scala +++ b/guinep/src/main/scala/macros.scala @@ -1,5 +1,6 @@ -package guinep.internal +package guinep +import guinep.model.* import scala.quoted.* private[guinep] object macros { diff --git a/guinep/src/main/scala/model.scala b/guinep/src/main/scala/model.scala index 4569a5e..357cf49 100644 --- a/guinep/src/main/scala/model.scala +++ b/guinep/src/main/scala/model.scala @@ -1,58 +1,60 @@ -package guinep.internal +package guinep import scala.quoted.* -case class Script(name: String, inputs: Seq[FormElement], run: List[Any] => String) +private[guinep] object model { + case class Script(name: String, inputs: Seq[FormElement], run: List[Any] => String) -enum FormElement(val name: String): - case FieldSet(override val name: String, elements: List[FormElement]) extends FormElement(name) - case TextInput(override val name: String) extends FormElement(name) - case NumberInput(override val name: String) extends FormElement(name) - case CheckboxInput(override val name: String) extends FormElement(name) - case Dropdown(override val name: String, options: List[(String, String)]) extends FormElement(name) - case TextArea(override val name: String, rows: Option[Int] = None, cols: Option[Int] = None) extends FormElement(name) - case DateInput(override val name: String) extends FormElement(name) - case EmailInput(override val name: String) extends FormElement(name) - case PasswordInput(override val name: String) extends FormElement(name) + enum FormElement(val name: String): + case FieldSet(override val name: String, elements: List[FormElement]) extends FormElement(name) + case TextInput(override val name: String) extends FormElement(name) + case NumberInput(override val name: String) extends FormElement(name) + case CheckboxInput(override val name: String) extends FormElement(name) + case Dropdown(override val name: String, options: List[(String, String)]) extends FormElement(name) + case TextArea(override val name: String, rows: Option[Int] = None, cols: Option[Int] = None) extends FormElement(name) + case DateInput(override val name: String) extends FormElement(name) + case EmailInput(override val name: String) extends FormElement(name) + case PasswordInput(override val name: String) extends FormElement(name) - def toJSONRepr: String = this match - case FormElement.FieldSet(name, elements) => - s"""{ "name": '$name', "type": 'fieldset', "elements": [${elements.map(_.toJSONRepr).mkString(",")}] }""" - case FormElement.TextInput(name) => - s"""{ "name": '$name', "type": 'text' }""" - case FormElement.NumberInput(name) => - s"""{ "name": '$name', "type": 'number' }""" - case FormElement.CheckboxInput(name) => - s"""{ "name": '$name', "type": 'checkbox' }""" - case FormElement.Dropdown(name, options) => - s"""{ "name": '$name', "type": 'dropdown', "options": [${options.map { case (k, v) => s"""{"key": "$k", "value": "$v"}""" }.mkString(",")}] }""" - case FormElement.TextArea(name, rows, cols) => - s"""{ "name": '$name', "type": 'textarea', "rows": ${rows.getOrElse("")}, "cols": ${cols.getOrElse("")} }""" - case FormElement.DateInput(name) => - s"""{ "name": '$name', "type": 'date' }""" - case FormElement.EmailInput(name) => - s"""{ "name": '$name', "type": 'email' }""" - case FormElement.PasswordInput(name) => - s"""{ "name": '$name', "type": 'password' }""" - -object FormElement: - given ToExpr[FormElement] with - def apply(formElement: FormElement)(using Quotes): Expr[FormElement] = formElement match + def toJSONRepr: String = this match case FormElement.FieldSet(name, elements) => - '{ FormElement.FieldSet(${Expr(name)}, ${Expr(elements)}) } + s"""{ "name": '$name', "type": 'fieldset', "elements": [${elements.map(_.toJSONRepr).mkString(",")}] }""" case FormElement.TextInput(name) => - '{ FormElement.TextInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'text' }""" case FormElement.NumberInput(name) => - '{ FormElement.NumberInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'number' }""" case FormElement.CheckboxInput(name) => - '{ FormElement.CheckboxInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'checkbox' }""" case FormElement.Dropdown(name, options) => - '{ FormElement.Dropdown(${Expr(name)}, ${Expr(options)}) } + s"""{ "name": '$name', "type": 'dropdown', "options": [${options.map { case (k, v) => s"""{"key": "$k", "value": "$v"}""" }.mkString(",")}] }""" case FormElement.TextArea(name, rows, cols) => - '{ FormElement.TextArea(${Expr(name)}, ${Expr(rows)}, ${Expr(cols)}) } + s"""{ "name": '$name', "type": 'textarea', "rows": ${rows.getOrElse("")}, "cols": ${cols.getOrElse("")} }""" case FormElement.DateInput(name) => - '{ FormElement.DateInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'date' }""" case FormElement.EmailInput(name) => - '{ FormElement.EmailInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'email' }""" case FormElement.PasswordInput(name) => - '{ FormElement.PasswordInput(${Expr(name)}) } + s"""{ "name": '$name', "type": 'password' }""" + + object FormElement: + given ToExpr[FormElement] with + def apply(formElement: FormElement)(using Quotes): Expr[FormElement] = formElement match + case FormElement.FieldSet(name, elements) => + '{ FormElement.FieldSet(${Expr(name)}, ${Expr(elements)}) } + case FormElement.TextInput(name) => + '{ FormElement.TextInput(${Expr(name)}) } + case FormElement.NumberInput(name) => + '{ FormElement.NumberInput(${Expr(name)}) } + case FormElement.CheckboxInput(name) => + '{ FormElement.CheckboxInput(${Expr(name)}) } + case FormElement.Dropdown(name, options) => + '{ FormElement.Dropdown(${Expr(name)}, ${Expr(options)}) } + case FormElement.TextArea(name, rows, cols) => + '{ FormElement.TextArea(${Expr(name)}, ${Expr(rows)}, ${Expr(cols)}) } + case FormElement.DateInput(name) => + '{ FormElement.DateInput(${Expr(name)}) } + case FormElement.EmailInput(name) => + '{ FormElement.EmailInput(${Expr(name)}) } + case FormElement.PasswordInput(name) => + '{ FormElement.PasswordInput(${Expr(name)}) } +} diff --git a/web/src/main/scala/api.scala b/web/src/main/scala/api.scala index 00a9898..8102741 100644 --- a/web/src/main/scala/api.scala +++ b/web/src/main/scala/api.scala @@ -1,7 +1,7 @@ package guinep inline def web(inline scripts: Any*): Unit = - val scriptInfos = internal.macros.scriptInfos(scripts) + val scriptInfos = macros.scriptInfos(scripts) val scriptInfosMap = scriptInfos.groupBy(_.name) if scriptInfosMap.exists(_._2.size > 1) then println( @@ -9,4 +9,4 @@ inline def web(inline scripts: Any*): Unit = |Ignoring duplicates""".stripMargin ) println("Starting GUInep web server at http://localhost:8090/scripts") - internal.webgen.genWeb(scriptInfosMap.mapValues(_.head).toMap) + webgen.genWeb(scriptInfosMap.view.mapValues(_.head).toMap) diff --git a/web/src/main/scala/htmlgen.scala b/web/src/main/scala/htmlgen.scala index 02346f8..55983b7 100644 --- a/web/src/main/scala/htmlgen.scala +++ b/web/src/main/scala/htmlgen.scala @@ -1,6 +1,7 @@ -package guinep.internal +package guinep import guinep.* +import guinep.model.* import zio.* import zio.http.* import zio.http.template.* diff --git a/web/src/main/scala/serialization.scala b/web/src/main/scala/serialization.scala index 0b27f47..f79ed20 100644 --- a/web/src/main/scala/serialization.scala +++ b/web/src/main/scala/serialization.scala @@ -1,6 +1,7 @@ -package guinep.internal +package guinep import guinep.* +import guinep.model.* import zio.* import zio.json.* import zio.json.ast.* diff --git a/web/src/main/scala/webgen.scala b/web/src/main/scala/webgen.scala index ecc8b7c..639305a 100644 --- a/web/src/main/scala/webgen.scala +++ b/web/src/main/scala/webgen.scala @@ -1,7 +1,8 @@ -package guinep.internal +package guinep import guinep.* -import guinep.internal.serialization.* +import guinep.model.* +import guinep.serialization.* import zio.* import zio.http.* import zio.http.template.* From f2fbab0909f5ecd05a386354e7e6cc13bce05534 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Wed, 6 Mar 2024 16:26:22 +0100 Subject: [PATCH 3/3] Bump sbt plugins and setup release, hopefully --- .github/workflows/release.yml | 18 ++++++++++++++++++ project/plugins.sbt | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..99cdf17 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Release +on: + push: + tags: ["v*"] +jobs: + publish: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2.3.4 + 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 }} diff --git a/project/plugins.sbt b/project/plugins.sbt index f2af332..ac9af80 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") -addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")