Skip to content

Commit

Permalink
Initial commit and implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lowmelvin committed Jul 24, 2023
0 parents commit f25eb1e
Show file tree
Hide file tree
Showing 17 changed files with 418 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/project/project/
/**/target/
.bsp/
.idea/
.bloop/
.metals/
.vscode/
32 changes: 32 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
rules = [
DisableSyntax,
# RemoveUnused,
OrganizeImports,
NoValInForComprehension,
]
DisableSyntax.noFinalize = true
DisableSyntax.noIsInstanceOf = true
DisableSyntax.noReturns = true

// `rules` on compilation
triggered.rules = [
DisableSyntax
]

OrganizeImports {
coalesceToWildcardImportThreshold = 6
expandRelative = true
groups = [
"cats",
"fs2",
"io",
"org",
"com",
"java."
"javax."
"scala."
"*"
]
groupedImports = AggressiveMerge
removeUnused = false # added for Scala 3
}
42 changes: 42 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
version=3.7.3

align.preset = more
maxColumn = 100
assumeStandardLibraryStripMargin = true
indent.defnSite = 2
indentOperator.topLevelOnly = false
align.preset = more
align.openParenCallSite = false
newlines.source = keep
newlines.beforeMultiline = keep
newlines.afterCurlyLambdaParams = keep
newlines.alwaysBeforeElseAfterCurlyIf = true

runner.dialect = scala3

rewrite.rules = [
RedundantBraces
RedundantParens
SortModifiers
]

rewrite.redundantBraces {
ifElseExpressions = true
includeUnitMethods = false
stringInterpolation = true
}

rewrite.sortModifiers.order = [
"private", "final", "override", "protected",
"implicit", "sealed", "abstract", "lazy"
]

project.excludeFilters = [
".bloop"
".metals"
".vscode"
".scala-build"
"examples" # Scala 3 scripts and using directives not supported yet
"out"
"scala-version.scala"
]
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM sbtscala/scala-sbt:graalvm-ce-22.3.0-b2-java17_1.9.2_3.3.0 as sbt-graalvm
WORKDIR /app
COPY . .

ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
RUN sbt assembly

RUN gu install native-image
RUN native-image \
--no-fallback \
--enable-http \
--enable-https \
--static \
-jar target/**/formify.jar

FROM scratch
COPY --from=sbt-graalvm /app/formify /app/formify
ENTRYPOINT ["/app/formify"]
53 changes: 53 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
val CatsVersion = "2.9.0"
val CatsEffectVersion = "3.5.1"
val CatsEffectTestKitVersion = "3.5.1"
val Fs2Version = "3.7.0"
val Log4CatsVersion = "2.6.0"
val MunitVersion = "0.7.29"
val MunitCatsEffectVersion = "1.0.7"
val WeaverCatsVersion = "0.8.3"
val LogbackVersion = "1.4.8"

ThisBuild / organization := "com.melvinlow"
ThisBuild / scalaVersion := "3.3.0"
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision

lazy val root = (project in file("."))
.settings(
name := "formify",
version := "0.1.0-SNAPSHOT",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % CatsVersion,
"org.typelevel" %% "cats-effect" % CatsEffectVersion,
"co.fs2" %% "fs2-core" % Fs2Version,
"org.typelevel" %% "log4cats-slf4j" % Log4CatsVersion,
"ch.qos.logback" % "logback-classic" % LogbackVersion % Runtime,
"org.scalameta" %% "munit" % MunitVersion % Test,
"com.disneystreaming" %% "weaver-cats" % WeaverCatsVersion % Test,
"org.typelevel" %% "munit-cats-effect-3" % MunitCatsEffectVersion % Test,
"org.typelevel" %% "cats-effect-testkit" % CatsEffectTestKitVersion % Test
),
scalacOptions ++= Seq(
"-encoding",
"UTF-8",
"-feature",
"-unchecked",
"-deprecation",
"-Wunused:all",
"-Werror",
"-Wvalue-discard",
"-no-indent",
"-explain"
),
testFrameworks ++= List(
new TestFramework("munit.Framework"),
new TestFramework("weaver.framework.CatsEffect")
),
assembly / assemblyJarName := "formify.jar",
assemblyMergeStrategy := {
case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard
case x if x.endsWith("module-info.class") => MergeStrategy.discard
case x => assemblyMergeStrategy.value(x)
},
)
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.9.2
6 changes: 6 additions & 0 deletions project/metals.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// DO NOT EDIT! This file is auto-generated.

// This file enables sbt-bloop to create bloop config files.

addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.8")

5 changes: 5 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0")
addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.4.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4")
14 changes: 14 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %X %n</pattern>
</encoder>
</appender>

<!-- <logger name="org.typelevel.slf4j" level="TRACE"/> -->

<root level="TRACE">
<appender-ref ref="STDOUT" />
</root>
</configuration>
32 changes: 32 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.melvinlow.formify

import cats.data.*

import java.net.URLEncoder

opaque type FormData = Chain[(FormKey, FormValue)]

object FormData {
inline def Empty: FormData = Chain.empty

inline def one(key: FormKey, value: FormValue): FormData = Chain.one((key, value))

extension (data: FormData) {
inline def underlying: Chain[(FormKey, FormValue)] = data

inline def ++(other: FormData): FormData = data ++ other

def prepend(fragment: FormKeyFragment): FormData =
data.map((key, value) => (key.prepend(fragment), value))

def compile(using FormKeyCompiler): Chain[(String, String)] =
data.collect { case (k, FormValue(v)) => (k.compile, v) }

def serialize(using FormKeyCompiler): String =
data.compile.map { (k, v) =>
val kenc = URLEncoder.encode(k, "UTF-8")
val venc = URLEncoder.encode(v, "UTF-8")
s"$kenc=$venc"
}.toList.mkString("&")
}
}
64 changes: 64 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormDataEncoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.melvinlow.formify

import scala.compiletime.*
import scala.compiletime.ops.any.*
import scala.deriving.*

trait FormDataEncoder[T] {
def encode(data: T): FormData

extension (data: T) {
inline def asFormData: FormData = encode(data)
}
}

object FormDataEncoder {
inline def apply[T](using enc: FormDataEncoder[T]) = enc

inline def encode[T: FormDataEncoder](data: T): FormData = data.asFormData

inline private def summonEncoders[T, Elems <: Tuple]: List[?] =
inline erasedValue[Elems] match {
case _: EmptyTuple => Nil
case _: (head *: tail) => summonElemEncoder[T, head] :: summonEncoders[T, tail]
}

inline private def summonElemEncoder[T, Elem]: FormDataEncoder[Elem] | FormValueEncoder[Elem] =
summonFrom {
case formValueEncoder: FormValueEncoder[Elem] => formValueEncoder // prioritize value encoders
case formEncoder: FormDataEncoder[Elem] => formEncoder
case _: Mirror.ProductOf[Elem] => deriveElemEncoder[T, Elem]
}

inline private def deriveElemEncoder[T, Elem: Mirror.ProductOf]: FormDataEncoder[Elem] =
inline erasedValue[Elem] match {
case _: T => error("infinite recursion derivation")
case _ => derived[Elem]
}

inline private def summonLabels[Labels <: Tuple]: List[String] =
inline erasedValue[Labels] match {
case _: EmptyTuple => Nil
case _: (head *: tail) => constValue[ToString[head]] :: summonLabels[tail]
}

inline def derived[T](using m: Mirror.ProductOf[T]): FormDataEncoder[T] = new FormDataEncoder[T] {
lazy val labels = summonLabels[m.MirroredElemLabels]
lazy val encoders = summonEncoders[T, m.MirroredElemTypes]

override def encode(data: T): FormData = {
val values = data.asInstanceOf[Product].productIterator.toList

labels.lazyZip(values).lazyZip(encoders).map {
case (label, value, formDataEncoder: FormDataEncoder[v]) =>
formDataEncoder.encode(value.asInstanceOf[v]).prepend(FormKeyFragment(label))

case (label, value, formValueEncoder: FormValueEncoder[v]) =>
FormData.one(
FormKey.one(FormKeyFragment(label)),
formValueEncoder.encode(value.asInstanceOf[v])
)
}.foldLeft(FormData.Empty)(_ ++ _)
}
}
}
17 changes: 17 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormKey.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.melvinlow.formify

import cats.data.*

opaque type FormKey = NonEmptyChain[FormKeyFragment]

object FormKey {
inline def one(fragment: FormKeyFragment): FormKey = NonEmptyChain.one(fragment)

extension (key: FormKey) {
inline def underlying: NonEmptyChain[FormKeyFragment] = key

inline def prepend(fragment: FormKeyFragment): FormKey = fragment +: key

inline def compile(using compiler: FormKeyCompiler): String = compiler.compile(key)
}
}
13 changes: 13 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormKeyCompiler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.melvinlow.formify

import cats.data.*

trait FormKeyCompiler {
def compile(key: FormKey): String
}

object FormKeyCompiler {
def make(fn: (NonEmptyChain[String]) => String): FormKeyCompiler = new FormKeyCompiler {
override def compile(key: FormKey): String = fn(key.underlying.map(_.underlying))
}
}
11 changes: 11 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormKeyFragment.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.melvinlow.formify

opaque type FormKeyFragment = String

object FormKeyFragment {
inline def apply(fragment: String): FormKeyFragment = fragment

extension (fragment: FormKeyFragment) {
inline def underlying: String = fragment
}
}
15 changes: 15 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.melvinlow.formify

opaque type FormValue = Option[String]

object FormValue {
inline def Empty: FormValue = None

inline def apply(value: String): FormValue = Some(value)

inline def unapply(value: FormValue): Option[String] = value

extension (value: FormValue) {
inline def underlying: Option[String] = value
}
}
16 changes: 16 additions & 0 deletions src/main/scala/com/melvinlow/formify/FormValueEncoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.melvinlow.formify

import cats.Contravariant

trait FormValueEncoder[T] {
def encode(value: T): FormValue
}

object FormValueEncoder {
inline def apply[T](using enc: FormValueEncoder[T]) = enc

given Contravariant[FormValueEncoder] with {
def contramap[A, B](fa: FormValueEncoder[A])(f: B => A): FormValueEncoder[B] =
(value: B) => fa.encode(f(value))
}
}
Loading

0 comments on commit f25eb1e

Please sign in to comment.