At least somewhat related, but you can also use ReScript objects if you want to leverage full inference for DI. Here’s an example that uses %typeof, which does not exist yet (but there’s a PoC of):
// The installed NodeJs bindings
module Node = {
module Fs = {
@module("node:fs")
external readFileSync: (string, string) => string = "readFileSync"
}
}
// This is our fn that uses DI via the `ctx` arg
let processFile = (filename, ~ctx) => {
let readfile: %typeof(Node.Fs.readFileSync) = ctx["readfile"]
let content = readfile(filename, "utf8")
content->String.split("\n")->Array.map(line => line->String.trim)
}
// Inject the real thing in prod
let inProd = () => {
let processed = processFile("file.txt", ~ctx={"readfile": Node.Fs.readFileSync})
processed
}
// Mock in test
let inTest = () => {
let processed = processFile(
"file.txt",
~ctx={
"readfile": (_filename, _encoding) => "<mocked>",
},
)
processed
}
You could use first class modules together with ReScript objects to get inferred DI as well:
module Fs = {
external readFileSync: string => string = "readFileSync"
}
module type Fs = module type of Fs
module Other = {
external log: string => unit = "console.log"
}
module type Other = module type of Other
let logContents = (contents, ~ctx) => {
let log: string => unit = ctx["log"]
log(contents)
}
let readAndReturnFile = (~ctx) => {
let module(Fs: Fs) = ctx["fs"]
let readFile: string => string = ctx["readFileSync"]
switch readFile("test.txt") {
| exception _ => Error(#ReadFileError)
| fileContents => Ok(fileContents ++ " hello")
}
}
let main = () => {
let ctx = {
"readFileSync": s => s,
"log": Console.log,
"fs": module(Other: Other),
}
switch readAndReturnFile(~ctx) {
| Ok(contents) => contents->logContents(~ctx)
| Error(#ReadFileError) => ()
}
}