Skip to content

Instantly share code, notes, and snippets.

@gmmorris
Created May 16, 2018 19:56
Show Gist options
  • Save gmmorris/2b132aaeafe2265dd8a2b95bafa0f3b4 to your computer and use it in GitHub Desktop.
Save gmmorris/2b132aaeafe2265dd8a2b95bafa0f3b4 to your computer and use it in GitHub Desktop.
Bucklescript bindings approaches for complex Object based arguments
var CryptographyJs = require("./cryptography.js");
CryptographyJs.hash({ algorithm: 'md5', length: 10 }, "asd");
CryptographyJs.hash({ algorithm: 'md5' }, "asd");
CryptographyJs.hash({ length: 10 }, "asd");
CryptographyJs.hash({}, "asd");
Cryptography.hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");
Cryptography.hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");
Cryptography.hashWithOpts(hashOptions(~length=10, ()), "asd");
Cryptography.hashWithOpts(hashOptions(), "111");
[@bs.deriving jsConverter]
type hash = [
| [@bs.as "sha256"] `SHA256
| [@bs.as "sha512"] `SHA512
| [@bs.as "sha384"] `SHA384
| [@bs.as "sha1"] `SHA1
| [@bs.as "md5"] `MD5
];
[@bs.deriving abstract]
type hashConfig = {
[@bs.optional]
algorithm: string,
[@bs.optional]
length: int,
};
module Crypt_ = {
[@bs.obj]
external hashOptions :
(~algorithm: Js.Nullable.t(string), ~length: Js.Nullable.t(int), unit) =>
hashConfig =
"";
};
let mapNullable = (opt: option('a), cb: 'a => 'b) =>
switch (opt) {
| Some(a) => Js.Nullable.return(cb(a))
| None => Js.Nullable.undefined
};
let hashOptions = (~algorithm=?, ~length=?, ()) =>
Crypt_.hashOptions(
~algorithm=mapNullable(algorithm, hashToJs),
~length=mapNullable(length, i => i),
(),
);
[@bs.module "./cryptography.js"]
external hashWithOpts : (hashConfig, string) => unit = "hash";
hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");
hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");
hashWithOpts(hashOptions(~length=10, ()), "asd");
hashWithOpts(hashOptions(), "asd");
{ algorithm: 'md5', length: 10 } 'asd'
{ algorithm: 'md5', length: undefined } 'asd'
{ algorithm: undefined, length: 10 } 'asd'
{ algorithm: undefined, length: undefined } 'asd'
[@bs.deriving jsConverter]
type hash = [
| [@bs.as "sha256"] `SHA256
| [@bs.as "sha512"] `SHA512
| [@bs.as "sha384"] `SHA384
| [@bs.as "sha1"] `SHA1
| [@bs.as "md5"] `MD5
];
[@bs.deriving abstract]
type hashConfig = {
[@bs.optional]
algorithm: string,
[@bs.optional]
length: int,
};
module Crypt_ = {
[@bs.obj]
external hashOptions :
(~algorithm: string=?, ~length: int=?, unit) => hashConfig =
"";
};
let mapNullable = (opt: option('a), cb: 'a => 'b) =>
switch (opt) {
| Some(a) => Js.Nullable.return(cb(a))
| None => Js.Nullable.undefined
};
let hashOptions = (~algorithm=?, ~length=?, ()) =>
switch (algorithm) {
| Some(algorithm_) =>
switch (length) {
| Some(length_) =>
Crypt_.hashOptions(
~algorithm=hashToJs(algorithm_),
~length=length_,
(),
)
| None => Crypt_.hashOptions(~algorithm=hashToJs(algorithm_), ())
}
| None =>
switch (length) {
| Some(length_) => Crypt_.hashOptions(~length=length_, ())
| None => Crypt_.hashOptions()
}
};
[@bs.module "./cryptography.js"]
external hashWithOpts : (hashConfig, string) => unit = "hash";
hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");
hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");
hashWithOpts(hashOptions(~length=10, ()), "asd");
hashWithOpts(hashOptions(), "asd");
{ algorithm: 'md5', length: 10 } 'asd'
{ algorithm: 'md5' } 'asd'
{ length: 10 } 'asd'
{} 'asd'
@jchavarri
Copy link

jchavarri commented May 16, 2018

This looks great, I have just some suggestions to make it a bit more compact. Unfortunately there is no dynamic way to loop over the labelled arguments of the function.

[@bs.deriving jsConverter]
type hash = [
  | [@bs.as "sha256"] `SHA256
  | [@bs.as "sha512"] `SHA512
  | [@bs.as "sha384"] `SHA384
  | [@bs.as "sha1"] `SHA1
  | [@bs.as "md5"] `MD5
];

type hashConfig;

[@bs.obj]
external hashOptions :
  (~algorithm: string=?, ~length: int=?, unit) => hashConfig = "";

let hashOptions = (~algorithm=?, ~length=?, ()) =>
  switch (algorithm, length) {
  | (Some(a), Some(l)) => hashOptions(~algorithm=hashToJs(a), ~length=l, ())
  | (Some(a), None) => hashOptions(~algorithm=hashToJs(a), ())
  | (None, Some(l)) => hashOptions(~length=l, ())
  | (None, None) => hashOptions()
  };

[@bs.module "./cryptography.js"]
external hashWithOpts : (hashConfig, string) => unit = "hash";

hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");

hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");

hashWithOpts(hashOptions(~length=10, ()), "asd");

hashWithOpts(hashOptions(), "asd");

/* In a different way */
hashOptions(~algorithm=`MD5, ~length=10, ()) |. hashWithOpts("asd");

hashOptions(~algorithm=`MD5, ()) |. hashWithOpts("asd");

hashOptions(~length=10, ()) |. hashWithOpts("asd");

hashOptions() |. hashWithOpts("asd");

@jchavarri
Copy link

Removing the switch after the comment from Christophe in Discord, using the explicitly passed optional:

[@bs.deriving jsConverter]
type hash = [
  | [@bs.as "sha256"] `SHA256
  | [@bs.as "sha512"] `SHA512
  | [@bs.as "sha384"] `SHA384
  | [@bs.as "sha1"] `SHA1
  | [@bs.as "md5"] `MD5
];

type hashConfig;

[@bs.obj]
external hashOptions :
  (~algorithm: string=?, ~length: int=?, unit) => hashConfig =
  "";

let hashOptions = (~algorithm=?, ~length=?, ()) =>
  hashOptions(
    ~algorithm=?Belt.Option.map(algorithm, hashToJs),
    ~length=?length,
    (),
  );

[@bs.module "./cryptography.js"]
external hashWithOpts : (hashConfig, string) => unit = "hash";

hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");

hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");

hashWithOpts(hashOptions(~length=10, ()), "asd");

hashWithOpts(hashOptions(), "asd");

@christophe-riolo
Copy link

Since the length and () parameters are left as such I'd even suggest partial application, so we can focus only on the parameters that need some processing. Also I added the missing Crypt_ module qualifier.

[@bs.deriving jsConverter]
type hash = [
  | [@bs.as "sha256"] `SHA256
  | [@bs.as "sha512"] `SHA512
  | [@bs.as "sha384"] `SHA384
  | [@bs.as "sha1"] `SHA1
  | [@bs.as "md5"] `MD5
];

type hashConfig;

[@bs.obj]
external hashOptions :
  (~algorithm: string=?, ~length: int=?, unit) => hashConfig =
  "";

let hashOptions = (~algorithm=?) =>
  Crypt_.hashOptions(~algorithm=?Belt.Option.map(algorithm, hashToJs));

[@bs.module "./cryptography.js"]
external hashWithOpts : (hashConfig, string) => unit = "hash";

hashWithOpts(hashOptions(~algorithm=`MD5, ~length=10, ()), "asd");

hashWithOpts(hashOptions(~algorithm=`MD5, ()), "asd");

hashWithOpts(hashOptions(~length=10, ()), "asd");

hashWithOpts(hashOptions(), "asd");

@gmmorris
Copy link
Author

Thanks guys, I'll play around with these ideas tomorrow. :)

@gmmorris
Copy link
Author

gmmorris commented May 19, 2018

OK, this has been very helpful but I'm hitting another wall.
And sorry for asking so many questions but I can't seem to find detailed documentation of these features beyond the Bucklescript manual (https://bucklescript.github.io/bucklescript/Manual.html) which while detailed in general, is quite sparse on the side effects of these annotations (gives many different examples, but each example is quite basic).

The one issue I'm still stuck on is that I actually got the TheAPIThatIWantToSupport.re a little wrong.
Sadly the API of the JS library isn't quite compatible with Reason's semantics, and some of the keys are actually capitalised.
If we apply this to our contrived example above, it would be something like:

var CryptographyJs = require("./cryptography.js");

CryptographyJs.hash({ Algorithm: 'md5', length: 10 }, "asd");
CryptographyJs.hash({ Algorithm: 'md5' }, "asd");
CryptographyJs.hash({ length: 10 }, "asd");
CryptographyJs.hash({}, "asd");

See how the Algorithm field is capitalised?
I've tried explicitly mentioning the algorithm field with a [@bs.as] annotation on the hashConfig but that didn't work.

I tried using a deriving like this:

[@bs.deriving jsConverter]
type hashConfig = {
  [@bs.optional]
  _Algorithm: string,
  [@bs.optional]
  length: int,
};

The _ seems to tell it to convert to a capitalised property, which is good, but I can't figure out how to pass that through to the hashWithOpts without explicitly changing the signature to {. "_Algorithm": string, "length": int} - and that had a weird result because the [bs.obj] seems to return an object rather than the array that the generated hashConfigToJs expects.
I seem to have made a mess of the whole thing . :p

Any ideas?

@gmmorris
Copy link
Author

A...nd got there. Honestly don't get how that wasn't obvious to me. facepalm

type hashConfig;

[@bs.obj]
external hashOptions :
  (~_Algorithm: string=?, ~length: int=?, unit) => hashConfig =
  "";

let hashOptions = (~algorithm=?, ~length=?, ()) =>
  hashOptions(
    ~_Algorithm=?Belt.Option.map(algorithm, hashToJs),
    ~length?,
    (),
  );

Cheers :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment