contiamo / restful-react

A consistent, declarative way of interacting with RESTful backends, featuring code-generation from Swagger and OpenAPI specs 🔥
MIT License
1.87k stars 109 forks source link

Custom generator #229

Closed fabien0102 closed 4 years ago

fabien0102 commented 4 years ago

Why

To bring a bit of flexibility on the openAPI generator, I would like to introduce a new customGenerator for advanced configurations.

This is in response for #226 and #223, I tried a first integration in our product with some fetch generator, this works quite well 😃

I could come with something more easy to use and restrictive, but I want to give a try for more customization. Indeed, everybody project is a bit different (axios, basic fetch, rxjs…) so let's the user come with what he need!

You can have a preview of this PR in restful-react@canary

Todo

Example from my integration test (to be cleaned and integrated in the PR)

// restful-react.config.js
customImport: `import { getConfig } from "../../products/IdP/Config"\nimport { customGet, customMutate, CustomGetProps, CustomMutateProps } from "../fetchers"`,
customGenerator: ({ componentName, verb, route, description, genericsTypes, paramsInPath, paramsTypes }) => {
      const propsType = type =>
        `Custom${type}Props<${genericsTypes}>${paramsInPath.length ? ` & {${paramsTypes}}` : ""}`;

      return verb === "get"
        ? `${description}export const ${camel(componentName)} = (${
            paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props"
          }: ${propsType("Get")}) => customGet<${genericsTypes}>(getConfig("backend") + \`${route}\`, props);\n\n`
        : `${description}export const ${camel(componentName)} = (${
            paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props"
          }: ${propsType(
            "Mutate",
          )}) => customMutate<${genericsTypes}>("${verb.toUpperCase()}", getConfig("backend") + \`${route}\`, props);\n\n`;
    },
// fetchers.ts
import Cookies from "js-cookie";
import qs from "qs";
import { GeneralErrorResponse } from "./hub/hub";

export const errors = {
  401: "401 - Not authorized.",
  500: "500 - Server error.",
  502: "502 - Bad Gateway.",
  503: "503 - Service unavailable.",
  504: "504 - Gateway timeout.",
};

export interface CustomGetProps<
  _TData = any,
  _TError = any,
  TQueryParams = {
    [key: string]: any;
  }
> {
  queryParams?: TQueryParams;
}

export const customGet = <
  TData = any,
  _TError = any,
  TQueryParams = {
    [key: string]: any;
  }
>(
  path: string,
  props: { queryParams?: TQueryParams },
) => {
  let url = path;
  if (props.queryParams && Object.keys(props.queryParams).length) {
    url += `?${qs.stringify(props.queryParams)}`;
  }
  return fetch(url, {
    credentials: "include",
    headers: {
      "x-double-cookie": Cookies.get("double-cookie") || "",
      "content-type": "application/json",
    },
  }).then(res => {
    if ((res.headers.get("content-type") || "").includes("application/json")) {
      return (res.json() as unknown) as TData;
    }

    return {
      errors: [
        {
          type: "GeneralError",
          message: (errors as any)[res.status] || errors[500],
        },
      ],
    } as GeneralErrorResponse;
  });
};

export interface CustomMutateProps<
  _TData = any,
  _TError = any,
  TQueryParams = {
    [key: string]: any;
  },
  TRequestBody = any
> {
  body: TRequestBody;
  queryParams?: TQueryParams;
}

export const customMutate = <
  TData = any,
  _TError = any,
  TQueryParams = {
    [key: string]: any;
  },
  TRequestBody = any
>(
  method: string,
  path: string,
  props: { body: TRequestBody; queryParams?: TQueryParams },
) => {
  let url = path;
  if (method === "DELETE" && typeof props.body === "string") {
    url += `/${props.body}`;
  }
  if (props.queryParams && Object.keys(props.queryParams).length) {
    url += `?${qs.stringify(props.queryParams)}`;
  }
  return fetch(url, {
    method,
    body: JSON.stringify(props.body),
    credentials: "include",
    headers: {
      "x-double-cookie": Cookies.get("double-cookie") || "",
      "content-type": "application/json",
    },
  }).then(res => {
    if ((res.headers.get("content-type") || "").includes("application/json")) {
      return (res.json() as unknown) as TData;
    }

    return {
      errors: [
        {
          type: "GeneralError",
          message: (errors as any)[res.status] || errors[500],
        },
      ],
    } as GeneralErrorResponse;
  });
};

export const isGeneralErrorResponse = (data: any): data is GeneralErrorResponse => {
  return data && Array.isArray(data.errors);
};

Notes

For the crazy people around, this PR could open this generator to everything else than react, angular or vue fan boys, this is for you! 😁

mpotomin commented 4 years ago

The draft looks good so far! 👍