From 245b5b2d961f5bcc28ca6bca1a97a453ab914ab4 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Wed, 10 Apr 2024 16:25:41 +0200 Subject: [PATCH 1/2] Add basic config for web; still have to pass and andle it in some way --- web/src/main/scala/api.scala | 58 +++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/web/src/main/scala/api.scala b/web/src/main/scala/api.scala index 5165366..9c0917e 100644 --- a/web/src/main/scala/api.scala +++ b/web/src/main/scala/api.scala @@ -1,6 +1,43 @@ package guinep /** + * Creates and instance of GUInep web interface with the given configuration. + * + * To start the web server, call the `apply` method with the functions to be exposed. + * + * To change the configuration, call the `withSetConfig` or `withModifyConfig` methods. + * + * @param config the configuration for the web interface + * @example {{{ + * def add(a: Int, b: Int) = a + b + * def greet(name: String) = s"Hello, $name!" + * @main def run = guinep.web(add, greet) + * }}} + */ +def web: GuinepWeb = + GuinepWeb() + +/** + * Configuration for the GUInep web interface. + * + * @param requireNonNullableInputs when set, all non-nullable types will correspond to required inputs in the frontend + */ +case class GuinepWebConfig( + requireNonNullableInputs: Boolean = false +) + +object GuinepWebConfig: + def default: GuinepWebConfig = + GuinepWebConfig() + +private case class GuinepWeb(config: GuinepWebConfig = GuinepWebConfig.default) { + def withSetConfig(config: GuinepWebConfig): Unit = + this.copy(config = config) + + def withModifyConfig(f: GuinepWebConfig => GuinepWebConfig): Unit = + withSetConfig(f(config)) + + /** * Starts a web server with the endpoints for running the given functions and * an automatically derived frontend for calling them. * @@ -11,13 +48,14 @@ package guinep * @main def run = guinep.web(add, greet) * }}} */ -inline def web(inline functions: Any*): Unit = - val functionsInfos = macros.funInfos(functions) - val functionsInfosMap = functionsInfos.groupBy(_.name) - if functionsInfosMap.exists(_._2.size > 1) then - println( - s"""|Duplicate function names found: ${functionsInfosMap.filter(_._2.size > 1).keys.mkString(", ")} - |Ignoring duplicates""".stripMargin - ) - println("Starting GUInep web server at http://localhost:8090/") - webgen.genWeb(functionsInfos.distinct.map(fun => fun.name -> fun)) + inline def apply(inline functions: Any*): Unit = + val functionsInfos = macros.funInfos(functions) + val functionsInfosMap = functionsInfos.groupBy(_.name) + if functionsInfosMap.exists(_._2.size > 1) then + println( + s"""|Duplicate function names found: ${functionsInfosMap.filter(_._2.size > 1).keys.mkString(", ")} + |Ignoring duplicates""".stripMargin + ) + println("Starting GUInep web server at http://localhost:8090/") + webgen.genWeb(functionsInfos.distinct.map(fun => fun.name -> fun)) +} From 75183b1e1bbff49ec55a4d087e5e39cb26d360f6 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 11 Apr 2024 11:44:07 +0200 Subject: [PATCH 2/2] Pass config to web; extract json serialization from the model --- guinep/src/main/scala/model.scala | 41 +------------------------- web/src/main/scala/api.scala | 5 +++- web/src/main/scala/htmlgen.scala | 2 ++ web/src/main/scala/serialization.scala | 41 ++++++++++++++++++++++++++ web/src/main/scala/webgen.scala | 13 ++++++-- 5 files changed, 58 insertions(+), 44 deletions(-) diff --git a/guinep/src/main/scala/model.scala b/guinep/src/main/scala/model.scala index 81f1d1e..691df08 100644 --- a/guinep/src/main/scala/model.scala +++ b/guinep/src/main/scala/model.scala @@ -5,17 +5,7 @@ import scala.quoted.* private[guinep] object model { case class Fun(name: String, form: Form, run: List[Any] => String) - case class Form(inputs: Seq[FormElement], namedFormElements: Map[String, FormElement]) { - def formElementsJSONRepr = - val elems = this.inputs.map(_.toJSONRepr).mkString(",") - s"[$elems]" - def namedFormElementsJSONRepr: String = - val entries = this.namedFormElements.toList.map { (name, formElement) => - s""""$name": ${formElement.toJSONRepr}""" - } - .mkString(",") - s"{$entries}" - } + case class Form(inputs: Seq[FormElement], namedFormElements: Map[String, FormElement]) object Form: given ToExpr[Form] with def apply(form: Form)(using Quotes): Expr[Form] = form match @@ -100,35 +90,6 @@ private[guinep] object model { case FieldSet(_, _) => 8 case NamedRef(_, _) => 9 - 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.CharInput(name) => - s"""{ "name": '$name', "type": 'char' }""" - case FormElement.NumberInput(name, _) => - s"""{ "name": '$name', "type": 'number' }""" - case FormElement.FloatingNumberInput(name, _) => - s"""{ "name": '$name', "type": 'float' }""" - case FormElement.CheckboxInput(name) => - s"""{ "name": '$name', "type": 'checkbox' }""" - case FormElement.Dropdown(name, options) => - // TODO(kπ) this sortBy isn't 100% sure to be working (the only requirement is for the first constructor to not be recursive; this is a graph problem, sorta) - s"""{ "name": '$name', "type": 'dropdown', "options": [${options.sortBy(_._2).map { case (k, v) => s"""{"name": "$k", "value": ${v.toJSONRepr}}""" }.mkString(",")}] }""" - case FormElement.ListInput(name, element, _) => - s"""{ "name": '$name', "type": 'list', "element": ${element.toJSONRepr} }""" - 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' }""" - case FormElement.NamedRef(name, ref) => - s"""{ "name": '$name', "ref": '$ref', "type": 'namedref' }""" - object FormElement: given ToExpr[FormElement] with def apply(formElement: FormElement)(using Quotes): Expr[FormElement] = formElement match diff --git a/web/src/main/scala/api.scala b/web/src/main/scala/api.scala index 9c0917e..c7ad39e 100644 --- a/web/src/main/scala/api.scala +++ b/web/src/main/scala/api.scala @@ -57,5 +57,8 @@ private case class GuinepWeb(config: GuinepWebConfig = GuinepWebConfig.default) |Ignoring duplicates""".stripMargin ) println("Starting GUInep web server at http://localhost:8090/") - webgen.genWeb(functionsInfos.distinct.map(fun => fun.name -> fun)) + webgen.genWeb( + funs = functionsInfos.distinct.map(fun => fun.name -> fun), + config = config + ) } diff --git a/web/src/main/scala/htmlgen.scala b/web/src/main/scala/htmlgen.scala index 02289a3..84432de 100644 --- a/web/src/main/scala/htmlgen.scala +++ b/web/src/main/scala/htmlgen.scala @@ -2,6 +2,7 @@ package guinep import guinep.* import guinep.model.* +import guinep.serialization.* import zio.* import zio.http.* import zio.http.template.* @@ -9,6 +10,7 @@ import zio.http.codec.* private[guinep] trait HtmlGen { val funs: Seq[(String, Fun)] + val config: GuinepWebConfig def generateHtml = html( head( diff --git a/web/src/main/scala/serialization.scala b/web/src/main/scala/serialization.scala index c601a41..42aa889 100644 --- a/web/src/main/scala/serialization.scala +++ b/web/src/main/scala/serialization.scala @@ -78,3 +78,44 @@ private[guinep] object serialization: case guinep.model.Types.ListType.Seq => res.toSeq case guinep.model.Types.ListType.Vector => res.toVector case _ => Left(s"Unsupported form element: $formElement") + + extension (form: Form) + def formElementsJSONRepr = + val elems = form.inputs.map(_.toJSONRepr).mkString(",") + s"[$elems]" + def namedFormElementsJSONRepr: String = + val entries = form.namedFormElements.toList.map { (name, formElement) => + s""""$name": ${formElement.toJSONRepr}""" + } + .mkString(",") + s"{$entries}" + + extension (formElement: FormElement) + def toJSONRepr: String = formElement 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.CharInput(name) => + s"""{ "name": '$name', "type": 'char' }""" + case FormElement.NumberInput(name, _) => + s"""{ "name": '$name', "type": 'number' }""" + case FormElement.FloatingNumberInput(name, _) => + s"""{ "name": '$name', "type": 'float' }""" + case FormElement.CheckboxInput(name) => + s"""{ "name": '$name', "type": 'checkbox' }""" + case FormElement.Dropdown(name, options) => + // TODO(kπ) this sortBy isn't 100% sure to be working (the only requirement is for the first constructor to not be recursive; this is a graph problem, sorta) + s"""{ "name": '$name', "type": 'dropdown', "options": [${options.sortBy(_._2).map { case (k, v) => s"""{"name": "$k", "value": ${v.toJSONRepr}}""" }.mkString(",")}] }""" + case FormElement.ListInput(name, element, _) => + s"""{ "name": '$name', "type": 'list', "element": ${element.toJSONRepr} }""" + 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' }""" + case FormElement.NamedRef(name, ref) => + s"""{ "name": '$name', "ref": '$ref', "type": 'namedref' }""" diff --git a/web/src/main/scala/webgen.scala b/web/src/main/scala/webgen.scala index 177485d..1a93a7d 100644 --- a/web/src/main/scala/webgen.scala +++ b/web/src/main/scala/webgen.scala @@ -13,8 +13,11 @@ import scala.util.chaining.* private[guinep] object webgen { - def genWeb(funs: Seq[(String, Fun)]): Unit = { - val ws = WebServer(funs) + def genWeb( + funs: Seq[(String, Fun)], + config: GuinepWebConfig + ): Unit = { + val ws = WebServer(funs, config) val runtime = Runtime.default Unsafe.unsafe { implicit unsafe => runtime.unsafe.run( @@ -24,8 +27,12 @@ private[guinep] object webgen { } } - class WebServer(val funs: Seq[(String, Fun)]) extends HtmlGen { + class WebServer( + val funs: Seq[(String, Fun)], + val config: GuinepWebConfig + ) extends HtmlGen { val funsMap = funs.toMap + val app: HttpApp[Any] = Routes( Method.GET / PathCodec.empty -> handler(Response.html(generateHtml)),