A few small questions

How can I use ReScript React components from a JSX project? I cannot get it to work and I’m getting all sorts of esoteric errors that I don’t understand.

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 :slight_smile:

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>
1 Like

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:

1 Like

How do I work with dicts? Should I use Js.Dict? Should I avoid it and use Belt.Map or something? I also need to serialize this to JSON in the end.

The answer depends on what the objects are used for - there is an entire article in the docs about it :slight_smile:

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:

1 Like

Help me restructure this code

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.

1 Like

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:

  • Railway Oriented Programming for chaining result, it’s a powerful alternative of an early return
  • Parse don’t validate for filename
  • Opaque type for filename, so it’s not mistaken with some other random string
4 Likes

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)
    })
  )
4 Likes

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

How can I set “warnings as errors” from the command line?

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.

How do I handle polymorphism idiocy?

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?

You can use Generate Converters & Helpers | ReScript Language Manual

1 Like

For the following code, why does the log statement fail?

        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)
1 Like

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

1 Like

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.