Skip to content

Instantly share code, notes, and snippets.

@broerjuang
Created July 11, 2019 17:56
Show Gist options
  • Save broerjuang/181dd1a958833c84e651e10a58f37893 to your computer and use it in GitHub Desktop.
Save broerjuang/181dd1a958833c84e651e10a58f37893 to your computer and use it in GitHub Desktop.
Renata
module SharedTypes = {
type search = list((string, string));
type path = list(string);
type body = Js.Json.t;
type params = {
search: option(search),
path: option(path),
body: option(body),
formData: option(Fetch.formData),
};
};
open SharedTypes;
module Utils = {
let createElement =
(
make,
makeProps:
(~children: 'b, ~key: string=?, unit) => {. "children": 'b},
children,
) => {
React.createElementVariadic(make, makeProps(~children, ()), [||]);
};
let fromParamsToKey = params => {
ReUtils.Routes.(
switch (params.search, params.path) {
| (Some(s), Some(p)) => s->createQueryString ++ p->createPath
| (Some(s), None) => s->createQueryString
| (None, Some(p)) => p->createPath
| _ => "initialData"
}
);
};
};
module Decode = Atdgen_codec_runtime.Decode;
type networkPolicy =
| CacheFirst
| CacheAndNetwork
| NetworkOnly;
type error = {
code: option(int),
message: string,
};
module type GeneralConfig = {
type response('data);
let readResponse: Decode.t('data) => Decode.t(response('data));
let setResponseState:
(response('data), option('data) => unit, error => unit) => unit;
};
module type RenataConfig = {
type data;
let decoder: Decode.t(data);
let handler: params => Js.Promise.t(Js.Json.t);
};
module Init = (GC: GeneralConfig, RC: RenataConfig) => {
type data = option(RC.data);
type generalResponse = GC.response(data);
type currentState =
| Idle
| Error(error)
| Loaded(data)
| Loading;
type key = string;
type state = {
data: list((key, data)),
currentState,
};
type callback = {
onCompleted: data => unit,
onError: error => unit,
};
type action =
| FetchRequested(networkPolicy, params, callback)
| Internal_FreshFetch(params, callback)
| FetchFailed(error, callback)
| FetchSucceed(key, data, callback)
| Reset;
type value = {
state: currentState,
send: action => unit,
};
module RenataContext = {
let defaultValue = {state: Idle, send: _ => ()};
let context = React.createContext(defaultValue);
module Provider = {
let make = context->React.Context.provider;
[@bs.obj]
external makeProps:
(~value: value=?, ~children: React.element, ~key: string=?, unit) =>
{
.
"value": value,
"children": React.element,
} =
"";
};
};
module Provider = {
[@react.component]
let make = (~children) => {
let (state, setState) =
React.useState(() => {data: [], currentState: Idle});
let rec send = action => {
switch (action) {
| FetchRequested(networkPolicy, params, cb) =>
let key = params->Utils.fromParamsToKey;
let data =
switch (state.data |> List.assoc(key)) {
| data => Some(data)
| exception Not_found => None
};
switch (networkPolicy) {
| CacheFirst =>
switch (data) {
| Some(data) =>
setState(prevState =>
{data: prevState.data, currentState: Loaded(data)}
)
| _ =>
setState(prevState =>
{data: prevState.data, currentState: Loading}
);
send(Internal_FreshFetch(params, cb));
}
| CacheAndNetwork =>
switch (data) {
| Some(data) =>
setState(prevState =>
{data: prevState.data, currentState: Loaded(data)}
)
| _ => ()
};
send(Internal_FreshFetch(params, cb));
| NetworkOnly =>
setState(_ => {data: [], currentState: Loading});
send(Internal_FreshFetch(params, cb));
};
| Internal_FreshFetch(params, callback) =>
let key = params->Utils.fromParamsToKey;
Js.Promise.(
RC.handler(params)
|> then_(json => {
let response =
json |> GC.readResponse(Decode.optional(RC.decoder));
response |> resolve;
})
|> then_(response => {
let setSuccess = data => {
send(FetchSucceed(key, data |? None, callback));
};
let setError = error => {
send(FetchFailed(error, callback));
};
GC.setResponseState(response, setSuccess, setError)
|> resolve;
})
|> ignore
);
| FetchFailed(error, cb) =>
setState(_ => {...state, currentState: Error(error)});
cb.onError(error);
| FetchSucceed(key, data, callback) =>
setState(prevState =>
{
data: prevState.data @ [(key, data)],
currentState: Loaded(data),
}
);
callback.onCompleted(data);
| Reset => setState(_ => {data: [], currentState: Idle})
};
};
<RenataContext.Provider value={state: state.currentState, send}>
...children
</RenataContext.Provider>;
};
};
module Get = {
let useState =
(
~body=None,
~search=?,
~path=?,
~onCompleted=_ => (),
~onError=_ => (),
~networkPolicy=CacheFirst,
(),
) => {
let {state, send} = React.useContext(RenataContext.context);
let (s, p) =
ReUtils.Routes.(
search->createQueryStringFromOption,
path->createPathToStringFromOption,
);
let fetch = (~networkPolicy=networkPolicy, ()) => {
send(
FetchRequested(
networkPolicy,
{search, path, body, formData: None},
{onCompleted, onError},
),
);
};
let refetch = () => fetch(~networkPolicy=NetworkOnly, ());
React.useEffect2(
() => {
fetch();
if (networkPolicy == NetworkOnly) {
Some(() => send(Reset));
} else {
None;
};
},
(s, p),
);
(state, refetch);
};
};
module Post = {
type callbackValue = {
data,
error: option(error),
};
let usePost =
(~search=?, ~path=?, ~onCompleted=_ => (), ~onError=_ => (), ()) => {
let {state, send} = React.useContext(RenataContext.context);
let submit = (~callback=?, ~formData=?, body) =>
send(
FetchRequested(
NetworkOnly,
{search, path, body, formData},
{
onCompleted: data => {
onCompleted(data);
switch (callback) {
| Some(cb) => cb({data, error: None})
| None => ()
};
},
onError: error => {
onError(error);
switch (callback) {
| Some(cb) => cb({data: None, error: error->Some})
| None => ()
};
},
},
),
);
(state, submit);
};
};
};
@broerjuang
Copy link
Author

broerjuang commented Jul 11, 2019

ReUtils.re

[%raw "require(\"dayjs/locale/id\")"];

DayJs.locale("id");

module Option = {
  module Infix = {
    let (|?) = (option, defaultValue) =>
      switch (option) {
      | Some(value) => value
      | None => defaultValue
      };

    let (>>=) = Belt.Option.flatMap;

    let (<$>) = Belt.Option.map;
  };

  open Infix;

  /** convert option(list(variant)) to string */
  let stringOfVariantsOption =
      (
        ~variantsOption,
        ~stringOfVariant,
        ~defaultString="",
        ~separator=",",
        ~ending="",
        (),
      ) => {
    let stringOfVariants = variants => {
      let reduceVariantsToString = (accumulator, variant, index): string =>
        accumulator
        ++ (index != 0 && index < variants->List.length ? separator : ending)
        ++ variant->stringOfVariant;

      variants->Belt.List.reduceWithIndex("", reduceVariantsToString);
    };

    variantsOption <$> stringOfVariants |? defaultString;
  };
};

module ListUtils = {
  let rec stringOfList = (inputList, conjuction) => {
    switch (inputList) {
    | [] => ""
    | [hd] => hd
    | [hd, ...tl] => hd ++ conjuction ++ stringOfList(tl, conjuction)
    };
  };

  let listOfSplittedString = (inputString, conjunction) =>
    Js.String.split(conjunction, inputString)->Array.to_list;

  let optionOfList = inputList => {
    inputList->List.length > 0 ? Some(inputList) : None;
  };
};

module Css = {
  let mergeCss = inputList => inputList->ListUtils.stringOfList(_, " ");

  let merge = rules => rules->Css.merge;

  let mergeStyle = rules => rules->List.concat->Css.style;

  let rootFontSize = 16.;

  let fromPxToRem =
    fun
    | `px(value) => `rem(float_of_int(value) /. rootFontSize)
    | _ => assert(false);

  let fromPxFloatToRem =
    fun
    | `pxFloat(value) => `rem(value /. rootFontSize)
    | _ => assert(false);

  exception NotFound(string);

  let fromHexToString =
    fun
    | `hex(hexValue) => "#" ++ hexValue
    | _ => raise(NotFound("hex is not found"));

  exception NotValid(string);

  let fromHexToRgba = (value, opacity) => {
    let re3 = [%re "/[0-9a-fA-F]{3}/"];
    let re6 = [%re "/[0-9a-fA-F]{6}/"];

    switch (value) {
    | `hex(value) when String.length(value) == 6 && re6->Js.Re.test_(value) =>
      let r = ("0x" ++ String.sub(value, 0, 2))->int_of_string;
      let g = ("0x" ++ String.sub(value, 2, 2))->int_of_string;
      let b = ("0x" ++ String.sub(value, 4, 2))->int_of_string;
      Css.rgba(r, g, b, opacity);
    | `hex(value) when String.length(value) == 3 && re3->Js.Re.test_(value) =>
      let r = ("0x" ++ String.sub(value, 0, 1))->int_of_string;
      let g = ("0x" ++ String.sub(value, 1, 1))->int_of_string;
      let b = ("0x" ++ String.sub(value, 2, 1))->int_of_string;
      Css.rgba(r, g, b, opacity);
    | `hex(value)
        when !(String.length(value) == 3 || String.length(value) == 6) =>
      raise(NotValid("Halp! The hex string length should be either 6 or 3."))
    | _ =>
      raise(
        NotValid(
          "Halp! The hex is not valid hence cannot be formatted to rgb.",
        ),
      )
    };
  };

  let preset = () => {
    /* This Css coming from reason module */

    open Css;
    global(
      "html",
      [
        width(`percent(100.)),
        height(`percent(100.)),
        margin(zero),
        overflow(`hidden),
      ],
    );
    global(
      "body",
      [
        width(`percent(100.)),
        height(`percent(100.)),
        margin(zero),
        padding(zero),
        overflow(`hidden),
      ],
    );

    global(
      "#root",
      [
        width(`percent(100.)),
        height(`percent(100.)),
        overflow(`hidden),
        display(`flex),
        flexDirection(`column),
      ],
    );
  };
};

module Routes = {
  module QS = {
    /*
      Define a type that can be either a single string or a list of strings
     */

    type qsItem =
      | Single(string)
      | Multiple(list(string));

    /*
      Make a string “safe” by
      1) Changing all + to a space (decodeURI doesn’t do that)
      2) URI decoding (change things like %3f to ?)
      3) Changing <, >, and & to &lt; &gt; and &amp;
     */

    let makeSafe = (s: string): string =>
      Js.Global.decodeURI(Js.String.replaceByRe([%re "/\\+/g"], " ", s))
      |> Js.String.replaceByRe([%re "/&/g"], "&amp;")
      |> Js.String.replaceByRe([%re "/</g"], "&lt;")
      |> Js.String.replaceByRe([%re "/>/g"], "&gt;");

    /*
      This is the function used by fold_left in parseQS.
      Split a key/value pair on "="
      If the split succeeds, then get the current value for the key from the dictionary.
      If the key doesn’t exist, then add the new value as a Single value
      If the key exists:
       If it is a Single item, then modify the value as a Multiple consisting
        of the old Single value and this new value
       If it is a Multiple (list of items), then add this new value to the
        list of items
     */

    let addKeyValue =
        (accumulator: Js.Dict.t(qsItem), kvPair: string): Js.Dict.t(qsItem) =>
      switch (Js.String.split("=", kvPair)) {
      | [|"", ""|] => accumulator
      | [|key, codedValue|] =>
        let value = makeSafe(codedValue);
        switch (Js.Dict.get(accumulator, key)) {
        | None => Js.Dict.set(accumulator, key, Single(value))
        | Some(v) =>
          switch (v) {
          | Single(s) => Js.Dict.set(accumulator, key, Multiple([value, s]))
          | Multiple(m) =>
            Js.Dict.set(accumulator, key, Multiple([value, ...m]))
          }
        };
        accumulator;
      | _ => accumulator
      };

    /* parseQS creates a dictionary (keyed by string) of qsItems */
    let parseQS = (qString: string): Js.Dict.t(qsItem) => {
      let result: Js.Dict.t(qsItem) = Js.Dict.fromList([]);

      let kvPairs = Js.String.split("&", qString);
      Array.fold_left(addKeyValue, result, kvPairs);
    };

    let getSingleValue = (~query, ~key, ~defaultValue) => {
      let values = Js.Dict.get(query, key);
      switch (values) {
      | Some(Single(v)) => v
      | _ => defaultValue
      };
    };

    /** convert a comma separated value - qs key into list of variants */
    let getVariantsFromCSVFormattedQSKey =
        (~query, ~key, ~defaultValue="", ~variantOfString, ()) => {
      let variantOptions =
        getSingleValue(~query, ~key, ~defaultValue)
        |> Js.String.split(",")
        |> Array.to_list
        |> List.map(variantOfString);

      variantOptions->Belt.List.reduce([], (accumulator, itemOption) =>
        switch (itemOption) {
        | None => accumulator
        | Some(item) => [item, ...accumulator]
        }
      );
    };

    /** convert qs key value to optional value */
    let getValueOptionFromQSSingleValue =
        (~query, ~key, ~defaultValue="", ()): option('a) => {
      let optionOfString = (inputString: string): option(string) => {
        switch (inputString) {
        | "" => None
        | stringValue => Some(stringValue)
        };
      };

      getSingleValue(~query, ~key, ~defaultValue)->optionOfString;
    };
  };

  let rec createPath = (path: list(string)) =>
    switch (path) {
    | [] => ""
    | [hd] => "/" ++ hd
    | [hd, ...tl] => "/" ++ hd ++ createPath(tl)
    };

  let createPathToStringFromOption =
    fun
    | Some(path) => createPath(path)
    | None => "";

  let rec createQueryString = xs =>
    switch (xs) {
    | [] => ""
    | [hd] => fst(hd) ++ "=" ++ snd(hd)
    | [hd, ...tl] =>
      fst(hd) ++ "=" ++ snd(hd) ++ "&" ++ createQueryString(tl)
    };

  let createQueryStringFromOption = qs =>
    switch (qs) {
    | Some(query) => createQueryString(query)
    | None => ""
    };
};

[@bs.val] external unsafeJsonParse: string => 'a = "JSON.parse";

module Modules = {
  [@bs.val] external require: string => unit = "require";
};

let createDangerousHtmlMarkup: string => Js.t('a) = html => {"__html": html};

module Time = {
  let formatSecondsToHHMMSS = timeInSecs => {
    let pad = i =>
      if (i < 10) {
        "0" ++ i->string_of_int;
      } else {
        i->string_of_int;
      };

    let hours = timeInSecs / 3600;
    let minutes = timeInSecs mod 3600 / 60;
    let seconds = timeInSecs mod 60;

    hours->pad ++ ":" ++ minutes->pad ++ ":" ++ seconds->pad;
  };
  let formatSecondsToMMSS = timeInSecs => {
    let pad = i =>
      if (i < 10) {
        "0" ++ i->string_of_int;
      } else {
        i->string_of_int;
      };

    let minutes = timeInSecs mod 3600 / 60;
    let seconds = timeInSecs mod 60;

    minutes->pad ++ ":" ++ seconds->pad;
  };

  let formatTime = num => {
    switch (string_of_int(num)) {
    | ""
    | "0" => "00"
    | v when num < 10 => "0" ++ v
    | v => v
    };
  };

  let getDateWithTime = (~date, ~time: (int, int)) => {
    let (hours, minutes) = time;
    DayJs.(
      parse(date)->add(hours, "hour")->add(minutes, "minute")->toISOString
    );
  };

  let getDateWithTimeSeconds = (~date, ~time: (int, int, int)) => {
    let (hours, minutes, seconds) = time;
    DayJs.(
      parse(date)
      ->add(hours, "hour")
      ->add(minutes, "minute")
      ->add(seconds, "second")
      ->toISOString
    );
  };
};

module Regex = {
  module Pattern = {
    let email = [%bs.re
      {|/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/|}
    ];

    let phoneNumber = [%bs.re {|/^\d+$/|}];
  };
};

module Validator = {
  module ReFormality = {
    let phoneNumber =
        (phoneNumber: 'a): Formality__Validation.Result.result('a) => {
      switch (phoneNumber) {
      | "" => Error("No handphone wajib diisi")
      | _ as v when !v->Js.Re.test_(Regex.Pattern.phoneNumber, _) =>
        Error("Nomor telepon harus terdiri dari angka")
      | _ as v when String.length(v) > 15 =>
        Error("Nomor telepon harus memiliki panjang 3 sampai 15 karakter")
      | _ => Ok(Valid)
      };
    };
  };
};

module String = {
  let getRandomString = (~max=5, ()) => {
    let min = 0;
    let text = ref("");

    let possibleString = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    let stringLength = String.length(possibleString);

    for (_ in min to max) {
      let randomIndex =
        Js.Math.floor(Js.Math.random() *. float_of_int(stringLength));
      text := text^ ++ String.make(1, possibleString.[randomIndex]);
    };

    text^;
  };

  type truncateMode =
    | Ellipsis
    | NoEllipsis;

  let truncate = (inputString, maxCharCount, ~mode=Ellipsis, ()): string => {
    let ellipsis = "...";
    let inputStringLength = Js.String.length(inputString);

    inputStringLength > maxCharCount
      ? Js.String.slice(~from=0, ~to_=maxCharCount, inputString)
        ++ (mode == Ellipsis ? ellipsis : "")
      : inputString;
  };

  let stripTags = str => {
    let result = Js.String.replaceByRe([%bs.re "/(<([^>]+)>)/ig"], "", str);
    Js.String.trim(result);
  };

  let removeExt = filename => {
    let filenameWithoutExt =
      Js.String.split(".", filename)->Js.Array.slice(~start=0, ~end_=-1);
    Js.Array.joinWith(".", filenameWithoutExt);
  };

  let getExt = filename => {
    let fileNameWithExt = Js.String.split(".", filename);
    let length = Js.Array.length(fileNameWithExt);
    Js.Array.unsafe_get(fileNameWithExt, length - 1);
  };
};

module Intl = {
  /**
    Javascript has a number formatter (part of the Internationalization API).
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
   */
  type intl;
  [@bs.new] [@bs.scope "Intl"]
  external newNumberFormat: string => intl = "NumberFormat";

  [@bs.send] external format: (intl, int) => string = "";

  let formatNumber = (number: int) => {
    let instance = newNumberFormat("id");
    format(instance, number);
  };
};

@broerjuang
Copy link
Author

Usage

Create Renata Instance

module Create =
  RenataManager.Init({
    type response('data) = General_bs.response('data);
    let readResponse = General_bs.read_response;
    let setResponseState = (response: response('data), setSuccess, setError) =>
      if (response.status != "error" && response.status != "") {
        setSuccess(response.data);
      } else {
        let error: RenataManager.error = {
          code: response.errorCode,
          message:
            ApiError.getMessage(
              response.errorCode,
              response.errorDescription,
            ),
        };
        setError(error);
      };
  });

Store Instance

module SomeStore =
  Renata.Create({
    type data = SomeStore_bs.pagination;
    let decoder = SomeStore_bs.read_pagination;
    let handler = {
      ApiClient.send(~method=`Get, ~endpoint="/end-point");
    };
  });

module StoreThatPostData =
  Renata.Create({
    type data = SomeStore_bs.pagination;
    let decoder = SomeStore_bs.read_pagination;
    let handler = {
      ApiClient.send(~method=`Post, ~endpoint="/end-point");
    };
  });

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