E.g.
let myMap = myDict->Js.Dict.entries->Belt.Map.String.fromArray
I don’t know if it is a bad idea, but it for sure adds some runtime. What do you mean by convert? Is it just an external or does it produce extra JS code?
E.g.
let myMap = myDict->Js.Dict.entries->Belt.Map.String.fromArray
I don’t know if it is a bad idea, but it for sure adds some runtime. What do you mean by convert? Is it just an external or does it produce extra JS code?
It’s better to convert these objects into strongly-typed record objects with a schema i.e. the ‘parse, don’t validate’ philosophy.
Nevermind that, I got it to work. However, I cannot do nested components. For example, I like to have Page and Page.Section, but it seems I cannot do that and get it to work in regular JSX
Unless I missed some prior questions, the following should get you a Page
and Page.Section
components. Of course the outer module Page
could also be a Page.res
file instead
module Page = {
@react.component
let make = (~children) => <div className={"section"}>{children}</div>
module Section = {
@react.component
let make = (~children) => <div className={"section")>{children}</div>
}
}
// Usage
<Page><Page.Section>{React.string("Hello")}</Page.Section><Page.Section>{React.string("Subcomponent")}</Page.Section></Page>
Thank you.
Yes, but I want to use them from “normal JSX” files. I really don’t, but I have to because Next.js maps file names to routes and under each dir, I need an index.res file and that doesn’t work with ReScript because the filename has to be unique per project, so therefor I’m going to use .jsx files for the pages and ReScript for everything else.
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)