The recommended way is putting your ReScript components in e.g. the src directory, and re-export them in plain pages/*.js files instead.
See details in rescript-nextjs-template docs:
The recommended way is putting your ReScript components in e.g. the src directory, and re-export them in plain pages/*.js files instead.
See details in rescript-nextjs-template docs:
The answer depends on what the objects are used for - there is an entire article in the docs about it
For cases where Js.Dict
makes sense, I found I needed some helper functions over what Js.Dict
itself provides.
For simple cases Js.Json.stringifyAny()
works great, although it returns an optional string as the compiler cannot guarantee the input data is safe to serialise. For complex cases there are multiple threads on the forum that cover JSON encoding and decoding libraries we all use, such as:
I’m used to having guard clauses and early returns. For large functions, they make things really hard to follow. For smaller functions, they make things much easier to understand. I finally came up with this ReScript code:
if filename->isFilenameSafe {
let ext = filename->getFileExt
switch allowedExts->Js.Dict.get(ext) {
| None => res->sendError("invalid fileext")
| Some(responseFunc) => {
let root = Env.assetsDir
let filename = Node.Path.resolve(root, filename)
let fileExists = Node.Fs.existsSync(filename)
if fileExists {
let text = Node.Fs.readFileSync(filename, #utf8)
let requiresApiKey = text->Js.String2.includes("#pragma require_api_key")
let ok =
!requiresApiKey ||
switch apiKey {
| None => false
| Some(apiKey) => {
let db = await getMongoConn()
await isApiKeyValid(db, domain, apiKey)
}
}
if ok {
res->responseFunc(text)
} else {
res->sendError("not allowed")
}
} else {
res->sendError("no such file")
}
}
}
} else {
res->sendError("invalid filename")
}
How could I restructure it to be less psychosis-inducing? Keep in mind that the order of the sequence is important - for example, let’s imagine that it would be an unsafe op to call getFileExt unless isFilenameSafe has already returned true for the given filename.
Any ideas? I’m trying to wrap my head around how to do things more idiomatic in ReScript.
I’d write it this way:
open Belt
module ResultX = {
let asyncFlatMap = (
result: result<'value, 'error>,
fn: 'value => Promise.t<result<'newValue, 'error>>,
): Promise.t<result<'newValue, 'error>> => {
switch result {
| Error(_) as error => error->Promise.resolve
| Ok(value) => fn(value)
}
}
}
module Filename: {
type t
let fromString: string => option<t>
let getExtension: t => string
let toString: t => string
} = {
type t = string
let fromString = filename => {
if filename->isFilenameSafe {
Some(filename)
} else {
None
}
}
let getExtension = filename => filename->getFileExt
let toString = t => t
}
let validateApiKey = () => {
maybeApiKey->Option.mapWithDefault(Promise.resolve(false), apiKey => {
getMongoConn()
->Promise.then(db => {
isApiKeyValid(db, domain, apiKey)
})
->Promise.thenResolve(() => true)
})
}
let fn2 = res => {
rawFilename
->Filename.fromString
->Option.mapWithDefault(Error(#INVALID_FILENAME), filename => Ok(filename))
->Result.flatMap(filename => {
let extension = filename->Filename.getExtension
switch allowedExts->Js.Dict.get(extension) {
| None => Error(#NOT_ALLOWED_EXTENSION)
| Some(responseFunc) => Ok(filename, responseFunc)
}
})
->Result.flatMap(((filename, responseFunc)) => {
let root = Env.assetsDir
let path = Node.Path.resolve(root, filename->Filename.toString)
let isFileExists = Node.Fs.existsSync(path)
if isFileExists {
Ok(path, responseFunc)
} else {
Error(#FILE_NOT_FOUND)
}
})
->ResultX.asyncFlatMap(((path, responseFunc)) => {
let text = Node.Fs.readFileSync(path, #utf8)
let isApiKeyRequired = text->Js.String2.includes("#pragma require_api_key")
if isApiKeyRequired {
validateApiKey()->Promise.thenResolve(isValid => {
if isValid {
Ok(responseFunc, text)
} else {
Error(#UNAUTHORIZED)
}
})
} else {
Ok(responseFunc, text)->Promise.resolve
}
})
->Promise.thenResolve(result => {
switch result {
| Ok(responseFunc, text) => res->responseFunc(text)
| Error(#INVALID_FILENAME) => res->sendError("invalid filename")
| Error(#NOT_ALLOWED_EXTENSION) => res->sendError("invalid fileext")
| Error(#FILE_NOT_FOUND) => res->sendError("no such file")
| Error(#UNAUTHORIZED) => res->sendError("not allowed")
}
})
}
There are some concepts I used:
You just gave me study material for hours - thank you so, so much. Really awesome to see your implementation of the same thing and wrap my head around it.
Wow, TIL you can just do Error(#ERROR_TYPE)
! Thanks for the snippet, I’m actually playing around with railway stuff right now.
I ended up creating a custom PromiseResult
module for dealing Results inside Promises and converting between options/results/promises. Really helped streamline the outer bound of my app
Personally I’ve just extended the Result
module a little bit
// ResultX.res
let mapError = (result, fn) =>
switch result {
| Ok(_) as ok => ok
| Error(error) => Error(fn(error))
}
let fromOption = (option, error) =>
switch option {
| Some(value) => Ok(value)
| None => Error(error)
}
let asyncFlatMap = (result, fn) => {
switch result {
| Error(_) as error => error->Promise.resolve
| Ok(value) => fn(value)
}
}
It covers everything I need.
If there’s a need to chain a promise that contains result, I’d write:
->Promise.then(
ResultX.asyncFlatMap(_, gameId => {
loadGame(~gameId)
})
)
I’d personally just break this into smaller functions. You said yourself, smaller functions are more readable. Generally, if a switch statement is getting unruly, I move it into its own function.
With data-first chaining, this can look really idiomatic.
I’m on my phone so I can’t refactor your code atm
I don’t want to do it in bsconfig.json because it breaks development too much. But in CI, I would like for warnings to be treated as errors without having to patch the config file on each run.
There’s no way to do that. bsconfig.json
is the only way to configure the ReScript build. One does get used to handling the errors in development though, there are strategies you can use. E.g., if it complains about an unused variable, just prefix the variable name with _
.
Thanks.
As an example, I am using option to do a query with MongoDB:
…find({ “someField”: myOptionalParam })
The problem here is that leaving myOptionalParam as undefined causes MongoDB to look for documents where someField is indeed set to undefined. What can I do here to support both cases? Do I just create two functions?
switch user {
| Some(user) => Ok(user)
| None => Error(#NO_SUCH_USER)
}
->flatMap(user => {
if password == user.passwordHash {
Ok(user)
} else {
Error(#INCORRECT_PASSWORD)
}
})
->(
r => {
switch r {
| Ok(user) => {
Js.log(user.email) // <---------------------- fail
None
}
| Error(_) => None
}
}
)
The compiler says the field email doesn’t exist on user
The record field email can't be found.
If it's defined in another module or file, bring it into scope by:
- Prefixing it with said module name: TheModule.email
- Or specifying its type: let theValue: TheModule.theType = {email: VALUE}
Why does this happen? Also feel free to improve my code, maybe I’m thinking about the problem the wrong way.
That is because compiler does know what the type of user is.
eg: if the type of user comes from a module M
then the following code should fix it.
Js.log(user.M.email)
One warning, if you are planning to put this authentication system in production, it would be a security issue. Using a simple string comparison to check password hash equality can open you up to timing attacks. It’s better to use a comparison function provided by a secure hashing library. See more here: Authentication - OWASP Cheat Sheet Series
I figure the compiler cannot deduce the type, but the following works:
let r = switch user {
| Some(user) => Ok(user)
| None => Error(#NO_SUCH_USER)
}->flatMap(user => {
if password == user.passwordHash {
Ok(user)
} else {
Error(#INCORRECT_PASSWORD)
}
})
switch r {
| Ok(user) =>
Some({
"email": user.email,
"isAdmin": user.isAdmin,
})
| Error(_) => None
}
Which is weird to me, because the only difference is putting the first part into a let binding rather than piping the result of what is now the let binding into a function. This might even be a compiler bug, I think.
Thanks @yawaramin. No way is this going into product. It’s called passwordHash for a reason (although for now, I’m just doing a clear text comparison)!
Right now, I set up my externals “as I go”. A few years in the future, this will likely be different. There was a time before TypeScript types were widely available - I remember the chaos.
My workflow right now:
npm install
the lib.external
s for my use case (e.g., MongoDB, find(x)
)find(x, y, z)
“overload” of the function.What do I do at this point? How do you guys handle it?
Do I whip myself to go through the API docs and write proper externals from the beginning? I will lose hours doing this.
Do I, after 6, rewrite the external to include optionals y and z and then rewrite all places where I use find
? This will also “waste” a lot of time.
Do I, after 6, just slap on a new external and call it find2
or whatever is appropriate and start using that? This will save the most amount of time, but it will likely end up with the externals deviating a LOT from the API spec. which will no doubt cause headaches for other devs in the same code base.
Sometimes, my functions have a very long chain of calls (e.g., flatMap etc.) leading up to the end of the function. It sort of visually looks like a lot of function calls inside a function.
How do you guys usually structure your code to make that more neat and easier on the eyes? Right now, I’m just assigning some variable and then putting it at the end. E.g.:
let someFunc = () => {
switch x {
// ...
}
->flatMap(
// ...
)
->flatMap(
// ...
)
}
Which I would rewrite as:
let someFunc = () => {
let r = switch x {
// ...
}
let r = r->flatMap(
// ...
)
let r = r->flatMap(
// ...
)
r
}
What’s your take on this?