Skip to content

Commit

Permalink
Preserve form keys insertion order
Browse files Browse the repository at this point in the history
  • Loading branch information
sake92 committed Feb 21, 2024
1 parent 9aa3687 commit a8bd62e
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 54 deletions.
2 changes: 1 addition & 1 deletion DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
git diff
git commit -am "msg"

$VERSION="0.1.0"
$VERSION="0.2.0"
git commit --allow-empty -m "Release $VERSION"
git tag -a $VERSION -m "Release $VERSION"
git push --atomic origin main $VERSION
Expand Down
17 changes: 9 additions & 8 deletions formson/src/ba/sake/formson/FormDataRW.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import java.util.UUID
import scala.deriving.*
import scala.quoted.*
import scala.reflect.ClassTag
import scala.collection.mutable.ArrayDeque
import scala.collection.immutable.SeqMap
import scala.collection.mutable
import scala.util.Try

import ba.sake.formson.FormData.*


/** Maps a `T` to/from form data map
*/
trait FormDataRW[T] {
Expand Down Expand Up @@ -204,8 +205,8 @@ object FormDataRW {
private def parseRethrowingErrors[T](path: String, values: Seq[FormData])(using
rw: FormDataRW[T]
): Seq[T] = {
val parsedValues = ArrayDeque.empty[T]
val keyErrors = ArrayDeque.empty[ParseError]
val parsedValues = mutable.ArrayDeque.empty[T]
val keyErrors = mutable.ArrayDeque.empty[ParseError]
values.zipWithIndex.foreach { case (v, i) =>
val subPath = s"$path[$i]"
try {
Expand Down Expand Up @@ -246,22 +247,22 @@ object FormDataRW {
'{
new FormDataRW[T] {
override def write(path: String, value: T): FormData = {
val formDataMap = scala.collection.mutable.Map.empty[String, FormData]
val formDataMap = mutable.LinkedHashMap.empty[String, FormData]
val valueAsProd = ${ 'value.asExprOf[Product] }
$labels.zip(valueAsProd.productIterator).zip($rwInstances).foreach { case ((k, v), rw) =>
val res = rw.asInstanceOf[FormDataRW[Any]].write(k, v)
formDataMap += (k -> res)
}
Obj(formDataMap.toMap)
Obj(SeqMap.from(formDataMap))
}

override def parse(path: String, formData: FormData): T = {
val qParamsMap =
if formData.isInstanceOf[Obj] then formData.asInstanceOf[Obj].values
else typeMismatchError(path, "Object", formData, None)

val arguments = ArrayDeque.empty[Any]
val keyErrors = ArrayDeque.empty[ParseError]
val arguments = mutable.ArrayDeque.empty[Any]
val keyErrors = mutable.ArrayDeque.empty[ParseError]
val defaultValuesMap = $defaultValues.toMap

$labels.zip($rwInstances).foreach { case (label, rw) =>
Expand Down
15 changes: 8 additions & 7 deletions formson/src/ba/sake/formson/parse.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ba.sake.formson

import scala.collection.mutable
import scala.collection.immutable.SeqMap
import scala.collection.immutable.SortedMap
import fastparse.Parsed.Success
import fastparse.Parsed.Failure
Expand All @@ -20,14 +21,14 @@ private[formson] def parseFDMap(formDataMap: FormDataMap): FormData =

private def fromInternal(fdi: FormDataInternal): FormData = fdi match
case FormDataInternal.Simple(value) => FormData.Simple(value)
case FormDataInternal.Obj(values) => FormData.Obj(values.view.mapValues(fromInternal).toMap)
case FormDataInternal.Obj(values) => FormData.Obj(values.map((k, v) => k -> fromInternal(v)))
case FormDataInternal.Sequence(valuesMap) => FormData.Sequence(valuesMap.values.toSeq.flatten.map(fromInternal))

// internal, temporary representation
private[formson] enum FormDataInternal(val tpe: String):
case Simple(value: FormValue) extends FormDataInternal("simple value")
case Sequence(values: SortedMap[Int, Seq[FormDataInternal]]) extends FormDataInternal("sequence")
case Obj(values: Map[String, FormDataInternal]) extends FormDataInternal("object")
case Obj(values: SeqMap[String, FormDataInternal]) extends FormDataInternal("object")

////////////////// INTERNAL parsing..
private[formson] class FormsonParser(formDataMap: FormDataMap) {
Expand All @@ -51,15 +52,15 @@ private[formson] class FormsonParser(formDataMap: FormDataMap) {
Sequence(SortedMap(0 -> Seq(acc, second)))

case (Obj(existingValuesMap), Obj(valuesMap)) =>
val objAcc = existingValuesMap.to(mutable.SortedMap)
val objAcc = mutable.LinkedHashMap.from(existingValuesMap)
valuesMap.foreach { case (key, value) =>
objAcc.get(key) match
case None =>
objAcc(key) = value
case Some(existingValue) =>
objAcc(key) = merge(existingValue, value)
}
Obj(objAcc.toMap)
Obj(SeqMap.from(objAcc))

case (Sequence(existingValuesMap), Sequence(valuesMap)) =>
val seqAcc = existingValuesMap.to(mutable.SortedMap)
Expand All @@ -77,7 +78,7 @@ private[formson] class FormsonParser(formDataMap: FormDataMap) {

private def mergeObjects(flatObjects: Seq[Obj]): Obj =
flatObjects
.foldLeft(Obj(Map.empty)) { case (acc, next) =>
.foldLeft(Obj(SeqMap.empty)) { case (acc, next) =>
merge(acc, next)
}
.asInstanceOf[Obj]
Expand All @@ -94,8 +95,8 @@ private[formson] class FormsonParser(formDataMap: FormDataMap) {
else Sequence(SortedMap(index -> Seq(parseInternal(rest, values))))

case None =>
if rest.isEmpty then Obj(Map(key -> Sequence(SortedMap(0 -> values.map(Simple.apply)))))
else Obj(Map(key -> parseInternal(rest, values)))
if rest.isEmpty then Obj(SeqMap(key -> Sequence(SortedMap(0 -> values.map(Simple.apply)))))
else Obj(SeqMap(key -> parseInternal(rest, values)))

case Seq() => throw FormsonException("Empty key parts")
}
Expand Down
7 changes: 4 additions & 3 deletions formson/src/ba/sake/formson/types.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package ba.sake.formson

import java.nio.file.Path
import scala.collection.immutable.SeqMap

enum FormValue(val tpe: String) {
case Str(value: String) extends FormValue("simple value")
case File(value: Path) extends FormValue("file")
case ByteArray(value: Array[Byte]) extends FormValue("byte array")
}

/** Represents a raw form data map. Values are not encoded.
/** Represents a raw form data map. Keys are ordered by insertion order. Values are not encoded.
*/
type FormDataMap = Map[String, Seq[FormValue]]
type FormDataMap = SeqMap[String, Seq[FormValue]]

enum FormData(val tpe: String):

case Simple(value: FormValue) extends FormData("simple value")

case Sequence(values: Seq[FormData]) extends FormData("sequence")

case Obj(values: Map[String, FormData]) extends FormData("object")
case Obj(values: SeqMap[String, FormData]) extends FormData("object")
12 changes: 7 additions & 5 deletions formson/src/ba/sake/formson/write.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package ba.sake.formson

import FormData.*
import scala.collection.mutable
import scala.collection.immutable.SeqMap

private[formson] def writeToFDMap(path: String, formData: FormData, config: Config): FormDataMap = formData match
case simple: Simple => Map(path -> Seq(simple.value))
case simple: Simple => SeqMap(path -> Seq(simple.value))
case seq: Sequence => writeSeq(path, seq, config)
case obj: Obj => writeObj(path, obj, config)

private def writeObj(path: String, formDataObj: Obj, config: Config): FormDataMap = {
val acc = scala.collection.mutable.Map.empty[String, Seq[FormValue]]
val acc = mutable.LinkedHashMap.empty[String, Seq[FormValue]]

formDataObj.values.foreach { case (key, v) =>
val subPath =
Expand All @@ -21,11 +23,11 @@ private def writeObj(path: String, formDataObj: Obj, config: Config): FormDataMa
acc ++= writeToFDMap(subPath, v, config)
}

acc.toMap
SeqMap.from(acc)
}

private def writeSeq(path: String, formDataSeq: Sequence, config: Config): FormDataMap = {
val acc = scala.collection.mutable.Map.empty[String, Seq[FormValue]].withDefaultValue(Seq.empty)
val acc = mutable.LinkedHashMap.empty[String, Seq[FormValue]].withDefaultValue(Seq.empty)

formDataSeq.values.zipWithIndex.foreach { case (v, i) =>
val subPath = config.seqWriteMode match
Expand All @@ -39,5 +41,5 @@ private def writeSeq(path: String, formDataSeq: Sequence, config: Config): FormD
}
}

acc.toMap
SeqMap.from(acc)
}
53 changes: 27 additions & 26 deletions formson/test/src/ba/sake/querson/FormDataParseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ba.sake.formson
import java.util.UUID
import java.nio.charset.StandardCharsets
import java.nio.file.Paths
import scala.collection.immutable.SeqMap

class FormDataParseSuite extends munit.FunSuite {

Expand All @@ -13,7 +14,7 @@ class FormDataParseSuite extends munit.FunSuite {
test("parseFormDataMap should parse simple key/values") {
Seq[(FormDataMap, FormSimple)](
(
Map(
SeqMap(
"str" -> Seq("text", "this_is_ignored").map(FormValue.Str.apply),
"int" -> Seq("42").map(FormValue.Str.apply),
"uuid" -> Seq(uuid.toString).map(FormValue.Str.apply),
Expand All @@ -30,7 +31,7 @@ class FormDataParseSuite extends munit.FunSuite {

test("parseFormDataMap should parse singleton-cases enum") {
Seq[(FormDataMap, FormEnum)](
(Map("color" -> Seq("Red").map(FormValue.Str.apply)), FormEnum(Color.Red))
(SeqMap("color" -> Seq("Red").map(FormValue.Str.apply)), FormEnum(Color.Red))
).foreach { case (fdMap, expected) =>
val res = fdMap.parseFormDataMap[FormEnum]
assertEquals(res, expected)
Expand All @@ -39,14 +40,14 @@ class FormDataParseSuite extends munit.FunSuite {

test("parseFormDataMap should parse sequence") {
Seq[(FormDataMap, FormSeq)](
(Map(), FormSeq(Seq())),
(Map("a" -> Seq()), FormSeq(Seq())),
(Map("a" -> Seq("").map(FormValue.Str.apply)), FormSeq(Seq(""))),
(Map("a" -> Seq("a1").map(FormValue.Str.apply)), FormSeq(Seq("a1"))),
(Map("a" -> Seq("a1", "a2").map(FormValue.Str.apply)), FormSeq(Seq("a1", "a2"))),
(Map("a[]" -> Seq("a1", "a2").map(FormValue.Str.apply)), FormSeq(Seq("a1", "a2"))),
(SeqMap(), FormSeq(Seq())),
(SeqMap("a" -> Seq()), FormSeq(Seq())),
(SeqMap("a" -> Seq("").map(FormValue.Str.apply)), FormSeq(Seq(""))),
(SeqMap("a" -> Seq("a1").map(FormValue.Str.apply)), FormSeq(Seq("a1"))),
(SeqMap("a" -> Seq("a1", "a2").map(FormValue.Str.apply)), FormSeq(Seq("a1", "a2"))),
(SeqMap("a[]" -> Seq("a1", "a2").map(FormValue.Str.apply)), FormSeq(Seq("a1", "a2"))),
(
Map(
SeqMap(
"a[3]" -> Seq("a3").map(FormValue.Str.apply),
"a" -> Seq("a0", "a00").map(FormValue.Str.apply),
"a[]" -> Seq("a0_1", "a0_11").map(FormValue.Str.apply),
Expand All @@ -63,14 +64,14 @@ class FormDataParseSuite extends munit.FunSuite {
// TODO ???????
test("parseFormDataMap should parse sequence of sequences") {
Seq[(FormDataMap, FormSeqSeq)](
(Map(), FormSeqSeq(Seq())),
(Map("a" -> Seq()), FormSeqSeq(Seq())),
(Map("a[][]" -> Seq("").map(FormValue.Str.apply)), FormSeqSeq(Seq(Seq(""))))
// (Map("a" -> Seq("a1")), FormSeqSeq(Seq("a1"))),
// (Map("a" -> Seq("a1", "a2")), FormSeqSeq(Seq("a1", "a2"))),
// (Map("a[]" -> Seq("a1", "a2")), FormSeqSeq(Seq("a1", "a2"))),
(SeqMap(), FormSeqSeq(Seq())),
(SeqMap("a" -> Seq()), FormSeqSeq(Seq())),
(SeqMap("a[][]" -> Seq("").map(FormValue.Str.apply)), FormSeqSeq(Seq(Seq(""))))
// (SeqMap("a" -> Seq("a1")), FormSeqSeq(Seq("a1"))),
// (SeqMap("a" -> Seq("a1", "a2")), FormSeqSeq(Seq("a1", "a2"))),
// (SeqMap("a[]" -> Seq("a1", "a2")), FormSeqSeq(Seq("a1", "a2"))),
/*(
Map(
SeqMap(
"a[3]" -> Seq("a3"),
"a" -> Seq("a0", "a00"),
"a[]" -> Seq("a0_1", "a0_11"),
Expand All @@ -87,15 +88,15 @@ class FormDataParseSuite extends munit.FunSuite {
test("parseFormDataMap should parse nested fields") {
Seq[(FormDataMap, FormNested)](
(
Map(
SeqMap(
"search" -> Seq("text", "this_is_ignored").map(FormValue.Str.apply),
"p.number" -> Seq("3").map(FormValue.Str.apply),
"p.size" -> Seq("50").map(FormValue.Str.apply)
),
FormNested("text", Page(3, 50))
),
(
Map(
SeqMap(
"search" -> Seq("text", "this_is_ignored").map(FormValue.Str.apply),
"p[number]" -> Seq("3").map(FormValue.Str.apply),
"p[size]" -> Seq("50").map(FormValue.Str.apply)
Expand All @@ -111,11 +112,11 @@ class FormDataParseSuite extends munit.FunSuite {
test("parseFormDataMap should parse falling back to defaults") {
Seq[(FormDataMap, FormDefaults)](
(
Map(),
SeqMap(),
FormDefaults("default", None, Seq())
),
(
Map(
SeqMap(
"q" -> Seq("q1").map(FormValue.Str.apply),
"opt" -> Seq("optValue").map(FormValue.Str.apply),
"seq" -> Seq("seq1", "seq2").map(FormValue.Str.apply)
Expand All @@ -131,7 +132,7 @@ class FormDataParseSuite extends munit.FunSuite {
test("parseFormDataMap should throw nice errors") {

locally {
val ex = intercept[ParsingException] { Map().parseFormDataMap[FormSimple] }
val ex = intercept[ParsingException] { SeqMap().parseFormDataMap[FormSimple] }
assertEquals(
ex.errors,
Seq(
Expand All @@ -146,7 +147,7 @@ class FormDataParseSuite extends munit.FunSuite {

locally {
val ex = intercept[ParsingException] {
Map(
SeqMap(
"str" -> Seq(),
"int" -> Seq("not_an_int").map(FormValue.Str.apply),
"uuid" -> Seq("uuidddd_NOT").map(FormValue.Str.apply),
Expand All @@ -169,7 +170,7 @@ class FormDataParseSuite extends munit.FunSuite {

locally {
val ex = intercept[ParsingException] {
Map("color" -> Seq("Yellow").map(FormValue.Str.apply)).parseFormDataMap[FormEnum]
SeqMap("color" -> Seq("Yellow").map(FormValue.Str.apply)).parseFormDataMap[FormEnum]
}
assertEquals(
ex.errors,
Expand All @@ -180,14 +181,14 @@ class FormDataParseSuite extends munit.FunSuite {
// nested
locally {
val ex = intercept[ParsingException] {
Map().parseFormDataMap[FormNested]
SeqMap().parseFormDataMap[FormNested]
}
assertEquals(ex.errors, Seq(ParseError("search", "is missing", None), ParseError("p", "is missing", None)))
}

locally {
val ex = intercept[ParsingException] {
Map("p" -> Seq()).parseFormDataMap[FormNested]
SeqMap("p" -> Seq()).parseFormDataMap[FormNested]
}
assertEquals(
ex.errors,
Expand All @@ -197,7 +198,7 @@ class FormDataParseSuite extends munit.FunSuite {

locally {
val ex = intercept[ParsingException] {
Map("search" -> Seq("").map(FormValue.Str.apply), "p.number" -> Seq("3a").map(FormValue.Str.apply))
SeqMap("search" -> Seq("").map(FormValue.Str.apply), "p.number" -> Seq("3a").map(FormValue.Str.apply))
.parseFormDataMap[FormNested]
}
assertEquals(
Expand Down
8 changes: 4 additions & 4 deletions sharaf/src/ba/sake/sharaf/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package ba.sake.sharaf

import java.nio.charset.StandardCharsets
import scala.jdk.CollectionConverters.*

import scala.collection.mutable
import scala.collection.immutable.SeqMap
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.form.FormData as UFormData
import io.undertow.server.handlers.form.FormParserFactory
import io.undertow.util.HttpString

import ba.sake.tupson.*
import ba.sake.formson.*
import ba.sake.querson.*
Expand Down Expand Up @@ -89,7 +89,7 @@ object Request {
Request(undertowExchange)

private[sharaf] def undertowFormData2FormsonMap(uFormData: UFormData): FormDataMap = {
val map = scala.collection.mutable.Map.empty[String, Seq[FormValue]]
val map = mutable.LinkedHashMap.empty[String, Seq[FormValue]]
uFormData.forEach { key =>
val values = uFormData.get(key).asScala
val formValues = values.map { value =>
Expand All @@ -104,6 +104,6 @@ object Request {
}
map += (key -> formValues.toSeq)
}
map.toMap
SeqMap.from(map)
}
}
Loading

0 comments on commit a8bd62e

Please sign in to comment.