Bindings for libraries returning random objects

Hello,

I want to create bindings for this simple library:

https://www.npmjs.com/package/docopt

The easy part was declaring the functions to execute and declare that they return a plain JS object (AKA POJO), so far so god. This is what I has:

    type options = Js.t({.}); 
    [@bs.module "docopt"][@bs.val]
    external docopt : string => args = "docopt";

    let parse = str => docopt(str) |> argsFromJs;

However, when I want to turn those into an ocaml/reason record is where I start having problems. For example, this is what I tried:

[@bs.deriving jsConverter]
type args = {
    upgrade: bool,
    get: bool
};

let options = Docopt.parse(help) |> argsFromJs;

That gives me a type error obviously:

Error: This expression has type
         Js.t(({.. get: bool, upgrade: bool} as 'a)) => args
       but an expression was expected of type
         ReasonRancherCli.Docopt.args => 'b
       Type Js.t('a) is not compatible with type
         ReasonRancherCli.Docopt.args = ReasonRancherCli.Docopt.args

Im not sure how should I do this…
Should I try to make the bindings as a Functor and provide a type record to it, or what is the correct approach here ?

Well, I’m making some advance…

This works, I get a random JS object from my JS function and I just convert it using the derived converter

[@bs.deriving jsConverter]
type args = {
    upgrade: bool,
    upgradeFinish: bool,
    get: bool,
};

type a = args;

[@bs.module "docopt"] [@bs.val]
external docopt: string => Js.t({..}) = "docopt";

let parse = str => docopt(str) |> argsFromJs;

However, I need to specify a @bs.as annotation for upgradeFinish, it should be read from upgrade-finish (which is sadly a valid JS property). How can I accomplish this ?

Seems there is no intermediate option.
If I want to rename things I have to use @bs.deriving abstract, but then I can’t convert the entire record, I need to access each property by using fieldNameGet(record). Also I have to modify the docopt function to make it report the args type, otherwhise I can not access the fields. Also, this last approach forces me to declare the type and the function on the same module.
This is what I have now:

[@bs.deriving abstract]
type args = {
    upgrade: bool,
    [@bs.as "upgrade-finish" ]
    upgradeFinish: bool,
    config: bool,
    saveEnv: bool,
    [@bs.as "--environment" ]
    enviroment: string,
    print: bool,
    get: bool,
};


[@bs.module "docopt"] [@bs.val]
external docopt: string => args = "docopt";

let parse = str => {
    let arguments = docopt(str);
    if (upgradeFinishGet(arguments))
      "Upgrade finish"
    else
      "Other stuff"
}

The last part is the bad one, I want to convert the Js record to a sum type of possible actions. Seems that the only option I have is to use several nested if else. Is there any better way ? Can I convert a record to a sum type on any other way ?

Regards

Well, well, I’m very proud of myself.
Finally I achieved a decent level of abstraction through functors. I have to say that the help I got at this thread to understand function was key.

I don’t know why this was not working before but it works now. Obviously I was doing wrong with functors, but I don’t understand or remember what.
Here is my current bindings implementation

module type T = {
    type t;
};
    
module Parser = (T:T) => {

    
    [@bs.deriving abstract]
    type options = {
        help: bool,
        /* version: Js.nullable(string), */
        options_first: bool,
        exit: bool,
    };
    
    [@bs.module "docopt"] [@bs.val]
    external docopt: (string, options) => T.t = "docopt";
    
    let parse = (~help=true, ~options_first=false, ~exit=true, doc) =>
    docopt(doc, options(~help, ~options_first, ~exit));
}

Some things I’m not very hapy with are:

  • Don’t know how to accept a nullable string from Ocaml/Reason so I can not accept version field
  • I don’t make any type checking on the provided type and I just blindly trust the user
  • Users are forced to create a module from my submodule, I don’t know if this can be made simpler

Here is an example usage:

[@bs.deriving abstract]
type args = {
  [@bs.as "--version"]
  version: bool,
  /* Upgrade and upgrade-finish options  */
  upgrade: bool,
  [@bs.as "upgrade-finish"]
  upgradeFinish: bool,
  [@bs.as "<stackName>"]
  stackName: string,
  [@bs.as "<imageName>"]
  imageName: Js.nullable(string),
  [@bs.as "<serviceName>"]
  serviceName: string,
  [@bs.as "--environment"]
  enviroment: string,
  /* Config options */
  config: bool,
  saveEnv: bool,
  print: bool,
  /* Read compose files */
  get: bool,
  dockerCompose: bool,
  rancherCompose: bool,
  [@bs.as "--output"]
  output: bool,
  [@bs.as "<fileName>"]
  fileName: string,
};

let parse = str => {
  module D = Docopt.Parser({type t = args})
  let args = D.parse(str)
  Js.log(args);
/* several extra lines to do something useful */
}

Regards

1 Like

For nullable strings you could try to use string option

@mseri that is what I want, but I don’t find a streamline way of converting it to Js.nullable …
The output object must be a javascript object, so on the type I have to use Js.nullable(string) . If I accept an optional version string, how can I convert that to nullable string ?

    let parse = (~help=true, ~options_first=false, ~exit=true, ~version=? , doc) =>
        docopt(doc, options(~help, ~options_first, ~version, ~exit));

This is the error I got

  This has type:
    option('a)
  But somewhere wanted:
    Js.nullable(string)

BuckleScript has several ways of converting nullable to option, but doesn’t the opposite does not seems to exist…

Ok,

Seems that I have two options now
I can use option(string) on the type declaration or use nullable stuff.
The advantage of just using option is that it does not require any conversion and no runtime overhead. The problem could be that it is translated to undefined instead of null. In this case, it seems to work fine because the target library seems to don’t care about it being null or undefined, but there are cases where this may not suffice.

So just doing:

    [@bs.deriving abstract]
    type options = {
        help: bool,
        version: option(string),
        options_first: bool,
        exit: bool,
    };
    
    [@bs.module "docopt"] [@bs.val]
    external docopt: (string, options) => T.t = "docopt";
    
    let parse = (~help=true, ~options_first=false, ~exit=true, ~version=? , doc) =>
    docopt(doc, options(~help, ~options_first, ~version, ~exit));

Seems to fit the bill for this particular case, but I think it will be more correct to do

    [@bs.deriving abstract]
    type options = {
        help: bool,
        version: Js.nullable(string),
        options_first: bool,
        exit: bool,
    };
    
    [@bs.module "docopt"] [@bs.val]
    external docopt: (string, options) => T.t = "docopt";
    
    let parse = (~help=true, ~options_first=false, ~exit=true, ~version=? , doc) =>
    docopt(doc, options(~help, ~options_first, ~version=Js.Nullable.fromOption(version), ~exit));

But the generated JS requires external libraries, and I don’t think I need those…

2 Likes