Skip to content

Commit

Permalink
Merge pull request #46 from KacperFKorban/config
Browse files Browse the repository at this point in the history
Config
  • Loading branch information
KacperFKorban authored Apr 11, 2024
2 parents b779fc8 + 75183b1 commit 1aed401
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 52 deletions.
41 changes: 1 addition & 40 deletions guinep/src/main/scala/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
59 changes: 50 additions & 9 deletions web/src/main/scala/api.scala
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -11,13 +48,17 @@ 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
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(
funs = functionsInfos.distinct.map(fun => fun.name -> fun),
config = config
)
println("Starting GUInep web server at http://localhost:8090/")
webgen.genWeb(functionsInfos.distinct.map(fun => fun.name -> fun))
}
2 changes: 2 additions & 0 deletions web/src/main/scala/htmlgen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package guinep

import guinep.*
import guinep.model.*
import guinep.serialization.*
import zio.*
import zio.http.*
import zio.http.template.*
import zio.http.codec.*

private[guinep] trait HtmlGen {
val funs: Seq[(String, Fun)]
val config: GuinepWebConfig
def generateHtml =
html(
head(
Expand Down
41 changes: 41 additions & 0 deletions web/src/main/scala/serialization.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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' }"""
13 changes: 10 additions & 3 deletions web/src/main/scala/webgen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)),
Expand Down

0 comments on commit 1aed401

Please sign in to comment.