[Beginner-Help] [FFI] Need help in decoding an object

I was trying to build a cli app using commander module and I was trying to port the following example to rescript

import { Command } from 'commander';
const program = new Command();

program
  .name('string-util')
  .description('CLI to some JavaScript string utilities')
  .version('0.8.0');

program.command('split')
  .description('Split a string into substrings and display as an array')
  .argument('<string>', 'string to split')
  .option('--first', 'display just the first substring')
  .option('-s, --separator <char>', 'separator character', ',')
  .action((str, options) => {
    const limit = options.first ? 1 : undefined;
    console.log(str.split(options.separator, limit));
  });

program.parse();

the corresponding .res file looks like this

type t = {"name": string, "description": string, "version": string}

@new @module("commander") external command: unit => t = "Command"

let program = command()

program["name"] = `string-util`

I am getting the following error when I try to build it

❯ yarn res:build
yarn run v1.22.19
$ rescript
rescript: [1/1] src/Demo.cmj
FAILED: src/Demo.cmj

  We've found a bug for you!
  /home/sk/rescript-project-template/src/Demo.res:7:1-7

  5 │ let program = command()
  6 │ 
  7 │ program["name"] = `string-util`
  8 │ 

  This expression has type t
  It has no field name#=
Hint: Did you mean name?

FAILED: cannot make progress due to previous errors.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Even though I have specified the name field in type t it still is not able to find it

when I changed the code to log the output for program["name"]

type t = {"name": string, "description": string, "version": string}

@new @module("commander") external command: unit => t = "Command"

let program = command()

Js.log(program["name"])

then I see name as a function

❯ node src/Demo.bs.js 
[Function: name]

I don’t know what to interpret this as?

Maybe you can use @send.

@send external name: (t, string) => t = "name"

let name = program->name("string-util")
1 Like

Should I still keep the record type t unchanged?

I’d like not to define it as record, just t.

type t
1 Like

I found using minimist together with rescript-struct more comfortable than commander. The biggest benefit is that you automatically get options with the right type.

Here’s an example:

Note, as the creator of rescript-struct I might be biased.

1 Like

Thanks for that! I am actually trying to learn Rescript FFI by building a cli application. My end goal is to be comfortable in creating bindings.

But I really appreciate the help and effort! :slight_smile:

Not preferable for me, but some comments:

type t = {"name": string, "description": string, "version": string}

type t is not record, but object. That’s why you encounter this error It has no field name#= ...

If you want to try with record, then it would be more like:

type rec t = {name: string => t, description: string => t, version: string => t}

But I’d like to use @send for this case.

1 Like

Thanks that gives more insight to the error and explains what I was doing wrong. This is also a nice way to do it but initially I might go in the way you mentioned it using @send.

Thanks again!

1 Like

If you want to see more examples about FFI, then it could be one https://github.com/green-labs/rescript-bindings.

1 Like

Thanks @moondaddi I’ll have a look :slight_smile:

Is it ok if I keep posting here or on a new topic about ffi questions because it’s easier for me to learn it this way if that is ok?

It would be awesome if rescript folks were on an instant messaging platform like discord, zulip or slack but unfortunately there aren’t any that I could find.

I’ll try my best to understand by examples but often I end up getting stuck and asking questions from people in the community.

I hope that is ok :see_no_evil:

Sure, this forum is the place to ask a help and have some discussion about any ReScript things.
If you’re okay, there is one discord channel you can join https://discord.gg/RzShCNp The name is Reason Seoul, but it is ReScript channel :smiley: And you can ask any question in English there.

1 Like

Not sure if it is okay to share not an official channel though :grinning_face_with_smiling_eyes: Here is the organizer @cometkim

1 Like

Thanks for sharing the channel! I just need a place to ask questions and non-official channels are fine imo :smiley:

Also, you can continue asking questions in the forum. I’d even say it’s the best place with a lot of people willing to help. And what’s even more important the questions/answers are indexable being able to help other people in the future.

3 Likes

Thanks @DZakh would definitely keep doing that :100:

1 Like

Like moondaddi mentioned above, I would probably just use an abstract type t here, but, if you want to use an object, you can use it as you are trying to use it with one adjustment.

Since the object comes from a JS binding, you can use the @set attribute. Then you can update the field. (playground)

type t = {@set "name": string, "description": string, "version": string}
//        ^^^^ you need @set here

@new @module("commander") external command: unit => t = "Command"

let program = command()

program["name"] = `string-util`
// ^ this line generates this JS: program.name = "string-util";

See the Object Update section of the manual.

1 Like

Thanks @Ryan for that I didn’t know how to use @set or in general decorators in rescript

1 Like

I did something similar in the discord bindings I wrote while I was learning FFI

Here are the bindings

And here is the implementation

2 Likes

So I made some progress I was able to get help from @moondaddi’s discord community but I got stuck at a place where I had to define the action function(s).

Since the action function is of type

(method) Command.action(fn: (...args: any[]) => void | Promise<void>): Command

I might not be possible to use @variadic because it returns a Promise.

@send external action1: (t, Arg.t => actionType) => t = "action"
@send external action2: (t, (Arg.t, Arg.t) => actionType) => t = "action"
@send external action3: (t, (Arg.t, Arg.t, Arg.t) => actionType) => t = "action"
@send external actionObj: (t, (Arg.t, {..}) => actionType) => t = "action"

where Arg is

module Arg = {
  type t

  external string: t => string = "%identity"
  external int: t => int = "%identity"
  external bool: t => bool = "%identity"
}

So I decided to use actionObj function as I am passing a string and object as parameters (my js code snippet above uses the same type of arguments)

my updated rescript code looks somewhat like :

module Arg = {
  type t

  external string: t => string = "%identity"
  external int: t => int = "%identity"
  external bool: t => bool = "%identity"
}

type t

type descriptionType =
  | String(string)
  | None
type defaultValueType =
  | String(string)
  | Boolean(bool)
  | StringArr(array<string>)
  | None
type actionType =
  | Unit(unit)
  | PromiseUnit(Js.Promise.t<unit>)

@new @module("commander") external make: unit => t = "Command"
@send external name: (t, string) => t = "name"
@send external description: (t, string) => t = "description"
@send external version: (t, string) => t = "version"
@send external command: (t, string) => t = "command"
@send
external option: (t, string, descriptionType, defaultValueType) => t = "option"
@send external action1: (t, Arg.t => actionType) => t = "action"
@send external action2: (t, (Arg.t, Arg.t) => actionType) => t = "action"
@send external action3: (t, (Arg.t, Arg.t, Arg.t) => actionType) => t = "action"
@send external actionObj: (t, (Arg.t, {..}) => actionType) => t = "action"

let program = make()

let name = (p, input) => p->name(input)

let description = (p, input) => p->description(input)

let version = (p, input) => p->version(input)

let command = (p, input) => p->command(input)

let option = (p, flags, description, defaultValue) => p->option(flags, description, defaultValue)

let _ =
  program
  ->name("string-util")
  ->description("CLI to some Javascript string utilities")
  ->version("0.8.0")
  ->command("split")
  ->description("Split a string into substrings and display as an array")
  ->option("--first", String("display just the first substring"), None)
  ->option("-s, --separator <char>", String("separator character"), String(","))
  ->actionObj((str, options) => {
    let limit = if options.first {
      1
    } else {
      Js.undefined
    }
    Js.log(str.split)
  })

which seems ok but it is unable to find first record field in options argument which is passed on by the option function

I am not sure how to change the option function to make this right, any suggestions or ideas would be really helpful.

Thanks for reading this long reply!