Writing bindings for Google Apps Script (GAS)

Hello,
Thanks to the help of this community I successfully completed a crazy idea: To write some ocaml functions to use inside Google Apps Script for a small stupid spreadsheet that I had.

The way it works now is by having a main index.js file that calls the Ocaml functions that are available under a global Lib namespace. Everything is bundled using parcel and the Idea was to use as few JS code as possible. Because it was easier than I expected I decided to go one step further and write some bindings for the GAS functions I was using and reduce the glue JS code even more.

This are the bindings that I wrote so far. They work, but are not usable inside Ocaml yet.

type spreadsheet
type sheet
type range
external getActiveSpreadsheet : unit -> spreadsheet = "getActiveSpreadsheet" [@@bs.val][@@bs.scope "SpreadsheetApp"]
external getSheets : spreadsheet -> sheet array = "getSheets" [@@bs.send]
external getSheetByName : spreadsheet -> string -> sheet = "getSheetByName" [@@bs.send]
external getDataRange : sheet -> range = "getDataRange"  [@@bs.send]
external getValues : range -> 'a array array = "getValues"  [@@bs.send]

My doubt are on the edges. When it is just obscure GAS stuff I have no doubt, abstract types and functions to interact with them. Is when a GAS function returns data where I have doubts. Usually they are just arrays of arrays of Numbers or Strings. In the example above, the last definition says that you will get an array of arrays of 'a, but that is not true because it will be an array of “stuff” (strings, numbers, floats).
How should I type it in a way that it’s flexible but not cumbersome? For example, I don’t think using a functor will help because you will need to create a functor for every possible return type, in my case if you have 3 sheets with 3 different shapes, you will need 3 functors.
An alternative that I have used was to provide some helper functions to convert from JS to Ocaml types and then unwrap the Ocaml types, like the example I’m doing with Number_or_string.
This is nothing serious and I will just add the bindings that I may need for now, but I want to hear what the community (and potential users) thinks.

If anyone is interested in taking a look on the project, it is here: https://github.com/danielo515/ElectronicProjectsSpreadsheet

Regards

2 Likes

Not answering directly to your question, sorry.

But here is a binding I have been using for around 4 years: https://dubuget.fr/gitea/matthieu/ocaml-google-app.git.

Best regards

Is when a GAS function returns data where I have doubts. Usually they are just arrays of arrays of Numbers or Strings.

For return type polymorphism, you can use GADT with bs.ignore, the rough idea:

type 'a t =  Int : int t | String : string t 
external f : ('a t [@bs.ignore]) -> ... -> 'a = "xx"

I read discuss.ocaml.org from time to time, but checks https://forum.rescript-lang.org/ daily where you can get a quick answer

Not a direct answer, but a very interesting one.
I would love to see some usage examples, specially out of the Range module, because It’s how you manage Ojs.t data what I’m most intrigued by. There are some modules that has a conversion function, but the ones that return actual data do not, and I don’t know how you handle such data later in a type safe way in ocaml.

Thanks for the answer @bobzhang
Is that OCaml or rescript syntax?
It’s impressive how many times I read that two lines of code and I’m still trying to understand it. Are the ...a placeholder or it’s the actual thing to put there?
What is the usage of f? Is it meant to convert to whatever type you want or what?

I’m not sure if I made clear what my doubt is, but what I want is, to run a GAS function that returns an array of unknown JS types and being able to process/manipulate/read those types as safely as possible from OCaml, even if that requires some helper functions to check the type.
So for example, I run Range.getValues(r) and then pass that data as row to below function to work with it:

let pick_columns (a,b) (row: row): (string * int) option =
  let open Number_or_string in
  match classify(row.(a)), classify(row.(b)) with
  | String x, Int y -> Some (x, y)
  | _ -> None

Sorry for the late reply… I just found your question some minutes ago.

I simply use the conversion functions provided by Ojs when needed (Ojs signature).

As an example, here is one function of my codebase (randomly picked) using Ojs.string_o_js:

let next_num_bon () =
  let datas = all_data_range () in
  Range.offset datas ~row_offset:0 ~column_offset:(col "Id")
    ~num_rows:(Range.get_num_rows datas) ~num_columns:1
  |> Range.get_values
  |> Array.fold_left
       (fun last n ->
         let nb =
           try Ojs.string_of_js n.(0) |> int_of_string with Failure _ -> 0
         in
         if nb > last then nb else last)
       0
  |> succ

Best regards

1 Like