Skip to content

Commit

Permalink
use URLSearchParams to decode and encode query params
Browse files Browse the repository at this point in the history
  • Loading branch information
tsnobip committed Jan 16, 2025
1 parent c9f333b commit 1ac1e08
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 81 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-scissors-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rescript-relay-router": minor
---

use URLSearchParams to decode and encode query params
36 changes: 31 additions & 5 deletions examples/client-rendering/test/UrlEncodingDecoding.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe("makeLink", () => {
let link = Routes.Root.Todos.Route.makeLink(
~statuses=[TodoStatus.Completed, TodoStatus.NotCompleted],
)
expect(link)->Expect.toBe("/todos?statuses=completed,not-completed")
expect(link)->Expect.toBe("/todos?statuses=completed&statuses=not-completed")
})

test("should generate link without statuses", _t => {
Expand All @@ -17,7 +17,7 @@ describe("makeLink", () => {

test("should generate link correctly URI encoded", _t => {
let link = Routes.Root.Todos.Route.makeLink(~byValue="/incorrect value, for url")
expect(link)->Expect.toBe("/todos?byValue=%2Fincorrect%20value%2C%20for%20url")
expect(link)->Expect.toBe("/todos?byValue=%2Fincorrect+value%2C+for+url")
})

test("should omit query param when value is default value", _t => {
Expand All @@ -30,11 +30,12 @@ describe("parsing", () => {
test("parseRoute correctly decode query params", _t => {
let queryParams =
Routes.Root.Todos.Route.parseRoute(
"/todos?byValue=%2Fincorrect%20value%2C%20for%20url",
"/todos?byValue=%2Fincorrect+value%2C+for+url",
)->Option.getExn

expect(queryParams.byValue->Option.getUnsafe)->Expect.toBe("/incorrect value, for url")
})

test("parseRoute correctly decode path params", _t => {
let pathParams =
Routes.Root.PathParamsOnly.Route.parseRoute(
Expand All @@ -43,6 +44,31 @@ describe("parsing", () => {

expect(pathParams.pageSlug)->Expect.toBe("/incorrect value, for url")
})

test("query params can decode standard multiple query params", _t => {
let queryParams =
Routes.Root.Todos.Route.parseRoute(
"/todos?statuses=completed&statuses=not-completed&byValue=beware%2C%20a%20comma!",
)->Option.getExn
expect(queryParams.statuses->Option.getUnsafe)->Expect.toStrictEqual([
TodoStatus.Completed,
TodoStatus.NotCompleted,
])
expect(queryParams.byValue->Option.getUnsafe)->Expect.toBe("beware, a comma!")
})

test("query params still allow comma separated values decoded", _t => {
let queryParams =
Routes.Root.Todos.Route.parseRoute(
"/todos?statuses=completed,not-completed&byValue=beware%2C%20a%20comma!",
)->Option.getExn
expect(queryParams.statuses->Option.getUnsafe)->Expect.toStrictEqual([
TodoStatus.Completed,
TodoStatus.NotCompleted,
])
expect(queryParams.byValue->Option.getUnsafe)->Expect.toBe("beware, a comma!")
})

test("parseRoute correctly decode path and query params", _t => {
let (pathParams, queryParams) =
Routes.Root.Todos.Single.Route.parseRoute("/todos/123?showMore=false")->Option.getExn
Expand Down Expand Up @@ -96,7 +122,7 @@ describe("parsing", () => {
act(
() => {
result.current["push"](
"?statuses=completed,not-completed&byValue=%2Fincorrect%20value%2C%20for%20url",
"?statuses=completed&statuses=not-completed&byValue=%2Fincorrect+value%2C+for+url",
)
},
)
Expand All @@ -117,7 +143,7 @@ describe("parsing", () => {
)
act(
() => {
result.current["push"]("?statuses=completed,not-completed")
result.current["push"]("?statuses=completed&statuses=not-completed")
},
)
Array.push(followingStatuses, result.current["useQueryParams"].queryParams.statuses)
Expand Down
136 changes: 60 additions & 76 deletions packages/rescript-relay-router/src/RelayRouter__Bindings.res
Original file line number Diff line number Diff line change
@@ -1,109 +1,93 @@
module QueryParams = {
type t = dict<array<string>>
type t

@new external make: unit => t = "URLSearchParams"
@new external fromString: string => t = "URLSearchParams"

@send external deleteParam: (t, string) => unit = "delete"

let make = () => Dict.make()
@send external setParam: (t, ~key: string, ~value: 'value) => unit = "set"

let deleteParam = (dict, key) => Dict.delete(Obj.magic(dict), key)
@send external append: (t, ~key: string, ~value: string) => unit = "append"

let setParam = (dict, ~key, ~value) => dict->Dict.set(key, [value])
let setParamOpt = (dict, ~key, ~value) =>
let setParamOpt = (t, ~key, ~value) =>
switch value {
| Some(value) => dict->Dict.set(key, [value])
| None => dict->deleteParam(key)
| Some(value) => t->setParam(~key, ~value)
| None => t->deleteParam(key)
}

let setParamInt = (dict, ~key, ~value) =>
dict->setParamOpt(~key, ~value=value->Option.map(v => Int.toString(v)))

let setParamBool = (dict, ~key, ~value) =>
dict->setParamOpt(
~key,
~value=switch value {
| Some(false) => Some("false")
| Some(true) => Some("true")
| _ => None
},
)

let setParamArray = (dict, ~key, ~value) => dict->Dict.set(key, value)
let setParamArrayOpt = (dict, ~key, ~value) =>
let setParamInt = setParamOpt

let setParamBool = setParamOpt

let setParamArray = (t, ~key, ~value) => {
t->deleteParam(key)
value->Array.forEach(value => t->append(~key, ~value))
}
let setParamArrayOpt = (t, ~key, ~value) =>
switch value {
| Some(value) => dict->Dict.set(key, value)
| None => dict->deleteParam(key)
| Some(value) => t->setParamArray(~key, ~value)
| None => t->deleteParam(key)
}

let printValue = value =>
value
->Array.map(v => encodeURIComponent(v))
->Array.join(",")
@send external toString: t => string = "toString"

let printKeyValue = (key, value) => key ++ "=" ++ printValue(value)
@send external sort: t => unit = "sort"

let toString = raw => {
let parts =
raw
->Dict.toArray
->Array.map(((key, value)) => {
printKeyValue(key, value)
})
let toString = t => {
let search = t->toString

switch parts->Array.length {
| 0 => ""
| _ => "?" ++ parts->Array.join("&")
switch search {
| "" => ""
| _ => "?" ++ search
}
}

let toStringStable = raw => {
let parts =
raw
->Dict.toArray
->Array.toSorted(((a, _), (b, _)) =>
if a->String.localeCompare(b) > 0. {
1.0
} else {
-1.0
}
)
->Array.map(((key, value)) => {
printKeyValue(key, value)
})

switch parts->Array.length {
| 0 => ""
| _ => "?" ++ parts->Array.join("&")
}
let toStringStable = t => {
t->sort
t->toString
}

let decodeValue = value => {
value->String.split(",")->Array.map(v => decodeURIComponent(v))
}

let parse = search => {
let dict = Dict.make()
if search->String.includes(",") {
let t = make()

let search = if search->String.startsWith("?") {
search->String.sliceToEnd(~start=1)
} else {
search
}
let search = if search->String.startsWith("?") {
search->String.sliceToEnd(~start=1)
} else {
search
}

let parts = search->String.split("&")
let parts = search->String.split("&")

parts->Array.forEach(part => {
let keyValue = part->String.split("=")
switch (keyValue->Array.get(0), keyValue->Array.get(1)) {
| (Some(key), Some(value)) => dict->Dict.set(key, decodeValue(value))
| _ => ()
}
})
parts->Array.forEach(part => {
let keyValue = part->String.split("=")
switch (keyValue->Array.get(0), keyValue->Array.get(1)) {
| (Some(key), Some(value)) => t->setParamArray(~key, ~value=decodeValue(value))
| _ => ()
}
})

dict
t
} else {
fromString(search)
}
}

let getParamByKey = (parsedParams, key) =>
parsedParams->Dict.get(key)->Option.getOr([])->Array.get(0)
@return(nullable) @send
external getParamByKey: (t, string) => option<string> = "get"

let getArrayParamByKey = (parsedParams, key) => parsedParams->Dict.get(key)
@send
external getArrayParamByKey: (t, string) => array<string> = "getAll"
let getArrayParamByKey = (t, key) =>
switch t->getArrayParamByKey(key) {
| [] => None
| array => Some(array)
}

let getParamInt = (parsedParams, key) =>
switch parsedParams->getParamByKey(key) {
Expand Down

0 comments on commit 1ac1e08

Please sign in to comment.