Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config #46

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading