Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Merge pull request #126 from contiamo/use-mutate-open-api-generation
Browse files Browse the repository at this point in the history
Generate `useMutate` components
  • Loading branch information
Tejas Kumar authored May 17, 2019
2 parents 464cf1a + bc4df4b commit 803ec96
Show file tree
Hide file tree
Showing 11 changed files with 517 additions and 268 deletions.
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ const MyAnimalsList = props => (
Here are all my {props.animal}
s!
</h1>
<ul>{animals.map(animal => <li>{animal}</li>)}</ul>
<ul>
{animals.map(animal => (
<li>{animal}</li>
))}
</ul>
</>
)}
</div>
Expand All @@ -234,7 +238,11 @@ const MyAnimalsList = props => (
Here are all my {props.animal}
s!
</h1>
<ul>{animals.map(animal => <li>{animal}</li>)}</ul>
<ul>
{animals.map(animal => (
<li>{animal}</li>
))}
</ul>
</div>
)
}
Expand All @@ -256,7 +264,13 @@ It is possible to render a `Get` component and defer the fetch to a later stage.
<p>Are you ready to unleash all the magic? If yes, click this button!</p>
<button onClick={get}>GET UNICORNS!!!!!!</button>

{unicorns && <ul>{unicorns.map((unicorn, index) => <li key={index}>{unicorn}</li>)}</ul>}
{unicorns && (
<ul>
{unicorns.map((unicorn, index) => (
<li key={index}>{unicorn}</li>
))}
</ul>
)}
</div>
)}
</Get>
Expand All @@ -281,7 +295,11 @@ const myNestedData = props => (
{data => (
<div>
<h1>Here's all the things I want</h1>
<ul>{data.map(thing => <li>{thing}</li>)}</ul>
<ul>
{data.map(thing => (
<li>{thing}</li>
))}
</ul>
</div>
)}
</Get>
Expand All @@ -302,7 +320,11 @@ const SearchThis = props => (
{data => (
<div>
<h1>Here's all the things I search</h1>
<ul>{data.map(thing => <li>{thing}</li>)}</ul>
<ul>
{data.map(thing => (
<li>{thing}</li>
))}
</ul>
</div>
)}
</Get>
Expand Down Expand Up @@ -373,13 +395,12 @@ const Movies = ({ dispatch }) => (
<li>
{movie.name}
<Mutate verb="DELETE">
{(deleteMovie, {loading: isDeleting}) => (<button
onClick={() => deleteMovie(movie.id).then(() => dispatch('DELETED'))}
loading={isDeleting}
>
Delete!
</button>)
}</Mutate>
{(deleteMovie, { loading: isDeleting }) => (
<button onClick={() => deleteMovie(movie.id).then(() => dispatch("DELETED"))} loading={isDeleting}>
Delete!
</button>
)}
</Mutate>
</li>
))
}
Expand Down Expand Up @@ -516,6 +537,12 @@ Your components can then be generated by running `npm run generate-fetcher`. Opt
}
```

#### Validation of the specification

To enforce the best quality as possible of specification, we have integrate the amazing open-api linter from ibm ([OpenAPI Validator](https://github.com/IBM/openapi-validator)). We strongly encourage you to setup your custom rules with a `.validaterc` file, you can find all useful information about this configuration [here](https://github.com/IBM/openapi-validator/#configuration).

If it's too noisy, you don't have the time or can't control the open-api specification: just add `--no-validation` flag to the command and this validation step will be skipped :wink:
#### Import from GitHub
Adding the `--github` flag to `restful-react import` instead of a `--file` allows us to **create React components from an OpenAPI spec _remotely hosted on GitHub._** <sup>_(how is this real life_ 🔥 _)_</sup>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"devDependencies": {
"@operational/scripts": "1.4.0-1c795b9",
"@types/chalk": "^2.2.0",
"@types/commander": "^2.12.2",
"@types/inquirer": "0.0.44",
"@types/lodash": "^4.14.123",
Expand Down Expand Up @@ -85,6 +86,7 @@
},
"dependencies": {
"case": "^1.6.1",
"chalk": "^2.4.2",
"commander": "^2.19.0",
"ibm-openapi-validator": "^0.3.1",
"inquirer": "^6.2.2",
Expand Down
7 changes: 5 additions & 2 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export interface States<TData, TError> {
error?: GetState<TData, TError>["error"];
}

export type MutateMethod<TData, TRequestBody> = (data?: TRequestBody) => Promise<TData>;
export type MutateMethod<TData, TRequestBody> = (
data: TRequestBody,
mutateRequestOptions?: RequestInit,
) => Promise<TData>;

/**
* Meta information returned to the fetchable
Expand Down Expand Up @@ -115,7 +118,7 @@ class ContextlessMutate<TData, TError, TQueryParams, TRequestBody> extends React
this.abortController.abort();
}

public mutate = async (body?: string | TRequestBody, mutateRequestOptions?: RequestInit) => {
public mutate = async (body: TRequestBody, mutateRequestOptions?: RequestInit) => {
const {
__internal_hasExplicitBase,
base,
Expand Down
15 changes: 8 additions & 7 deletions src/bin/restful-react-import.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chalk from "chalk";
import program from "commander";
import { existsSync, readFileSync, writeFileSync } from "fs";
import inquirer from "inquirer";
Expand All @@ -6,10 +7,13 @@ import request from "request";

import importOpenApi from "../scripts/import-open-api";

const log = console.log; // tslint:disable-line:no-console

program.option("-o, --output [value]", "output file destination");
program.option("-f, --file [value]", "input file (yaml or json openapi specs)");
program.option("-g, --github [value]", "github path (format: `owner:repo:branch:path`)");
program.option("-t, --transformer [value]", "transformer function path");
program.option("--no-validation", "skip the validation step (provided by ibm-openapi-validator)");
program.parse(process.argv);

(async () => {
Expand All @@ -27,7 +31,7 @@ program.parse(process.argv);
const { ext } = parse(program.file);
const format = [".yaml", ".yml"].includes(ext.toLowerCase()) ? "yaml" : "json";

return importOpenApi(data, format, transformer);
return importOpenApi(data, format, transformer, program.validation);
} else if (program.github) {
let accessToken: string;
const githubTokenPath = join(__dirname, ".githubToken");
Expand Down Expand Up @@ -85,7 +89,7 @@ program.parse(process.argv);
program.github.toLowerCase().includes(".yaml") || program.github.toLowerCase().includes(".yml")
? "yaml"
: "json";
resolve(importOpenApi(JSON.parse(body).data.repository.object.text, format, transformer));
resolve(importOpenApi(JSON.parse(body).data.repository.object.text, format, transformer, program.validation));
});
});
} else {
Expand All @@ -94,11 +98,8 @@ program.parse(process.argv);
})()
.then(data => {
writeFileSync(join(process.cwd(), program.output), data);

// tslint:disable-next-line:no-console
console.log(`🎉 Your OpenAPI spec has been converted into ready to use restful-react components!`);
log(chalk.green(`🎉 Your OpenAPI spec has been converted into ready to use restful-react components!`));
})
.catch(err => {
// tslint:disable-next-line:no-console
console.error(err);
log(chalk.red(err));
});
21 changes: 21 additions & 0 deletions src/scripts/ibm-openapi-validator.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
declare module "ibm-openapi-validator" {
interface OpenAPIError {
path: string;
message: string;
}

interface ValidatorResults {
errors: OpenAPIError[];
warnings: OpenAPIError[];
}

/**
* Returns a Promise with the validation results.
*
* @param openApiDoc An object that represents an OpenAPI document.
* @param defaultMode If set to true, the validator will ignore the .validaterc file and will use the configuration defaults.
*/
function validator(openApiDoc: any, defaultMode = false): Promise<ValidatorResults>;

export default validator;
}
76 changes: 65 additions & 11 deletions src/scripts/import-open-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { pascal } from "case";
import chalk from "chalk";
import openApiValidator from "ibm-openapi-validator";
import get from "lodash/get";
import groupBy from "lodash/groupBy";
import isEmpty from "lodash/isEmpty";
Expand Down Expand Up @@ -318,16 +320,16 @@ export const generateRestfulComponent = (
}`
: `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${errorTypes}, ${
queryParamsType ? componentName + "QueryParams" : "void"
}, ${requestBodyTypes}`;
}, ${verb === "delete" ? "string" : requestBodyTypes}`;

const genericsTypesWithoutError =
const genericsTypesForHooksProps =
verb === "get"
? `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${
queryParamsType ? componentName + "QueryParams" : "void"
}`
: `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${
queryParamsType ? componentName + "QueryParams" : "void"
}, ${requestBodyTypes}`;
}`;

let output = `${
needAResponseComponent
Expand Down Expand Up @@ -366,18 +368,18 @@ export const ${componentName} = (${
`;

// Hooks version
if (verb === "get" /* TODO: Remove this condition after `useMutate` implementation */) {
output += `export type Use${componentName}Props = Omit<Use${Component}Props<${genericsTypesWithoutError}>, "path"${
verb === "get" ? "" : ` | "verb"`
}>${paramsInPath.length ? ` & {${paramsTypes}}` : ""};
output += `export type Use${componentName}Props = Omit<Use${Component}Props<${genericsTypesForHooksProps}>, "path"${
verb === "get" ? "" : ` | "verb"`
}>${paramsInPath.length ? ` & {${paramsTypes}}` : ""};
${operation.summary ? "// " + operation.summary : ""}
export const use${componentName} = (${
paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props"
}: Use${componentName}Props) => use${Component}<${genericsTypes}>(\`${route}\`, props);
paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props"
}: Use${componentName}Props) => use${Component}<${genericsTypes}>(${
verb === "get" ? "" : `"${verb.toUpperCase()}", `
}\`${route}\`, props);
`;
}

if (headerParams.map(({ name }) => name.toLocaleLowerCase()).includes("prefer")) {
output += `export type Poll${componentName}Props = Omit<PollProps<${genericsTypes}>, "path">${
Expand Down Expand Up @@ -468,24 +470,76 @@ export interface ${pascal(name)}Response ${type}`;
);
};

/**
* Validate the spec with ibm-openapi-validator (with a custom pretty logger).
*
* @param schema openAPI spec
*/
const validate = async (schema: OpenAPIObject) => {
// tslint:disable:no-console
const log = console.log;

// Catch the internal console.log to add some information if needed
// because openApiValidator() calls console.log internally and
// we want to add more context if it's used
let wasConsoleLogCalledFromBlackBox = false;
console.log = (...props: any) => {
wasConsoleLogCalledFromBlackBox = true;
log(...props);
};
const { errors, warnings } = await openApiValidator(schema);
console.log = log; // reset console.log because we're done with the black box

if (wasConsoleLogCalledFromBlackBox) {
log("More information: https://github.com/IBM/openapi-validator/#configuration");
}
if (warnings.length) {
log(chalk.yellow("(!) Warnings"));
warnings.forEach(i =>
log(
chalk.yellow(`
Message : ${i.message}
Path : ${i.path}`),
),
);
}
if (errors.length) {
log(chalk.red("(!) Errors"));
errors.forEach(i =>
log(
chalk.red(`
Message : ${i.message}
Path : ${i.path}`),
),
);
}
// tslint:enable:no-console
};

/**
* Main entry of the generator. Generate restful-react component from openAPI.
*
* @param data raw data of the spec
* @param format format of the spec
* @param transformer custom function to transform your spec
* @param validation validate the spec with ibm-openapi-validator tool
*/
const importOpenApi = async (
data: string,
format: "yaml" | "json",
transformer?: (schema: OpenAPIObject) => OpenAPIObject,
validation = false,
) => {
const operationIds: string[] = [];
let schema = await importSpecs(data, format);
if (transformer) {
schema = transformer(schema);
}

if (validation) {
await validate(schema);
}

let output = "";

output += generateSchemasDefinition(schema.components && schema.components.schemas);
Expand All @@ -507,7 +561,7 @@ const importOpenApi = async (
imports.push("Get", "GetProps", "useGet", "UseGetProps");
}
if (haveMutate) {
imports.push("Mutate", "MutateProps");
imports.push("Mutate", "MutateProps", "useMutate", "UseMutateProps");
}
if (havePoll) {
imports.push("Poll", "PollProps");
Expand Down
16 changes: 13 additions & 3 deletions src/scripts/tests/__snapshots__/import-open-api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`scripts/import-open-api should parse correctly petstore-expanded.yaml 1
"/* Generated by restful-react */
import React from \\"react\\";
import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps } from \\"restful-react\\";
import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps, useMutate, UseMutateProps } from \\"restful-react\\";
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Expand Down Expand Up @@ -43,6 +43,11 @@ export const AddPet = (props: AddPetProps) => (
/>
);
export type UseAddPetProps = Omit<UseMutateProps<Pet, void>, \\"path\\" | \\"verb\\">;
export const useAddPet = (props: UseAddPetProps) => useMutate<Pet, Error, void, NewPet>(\\"POST\\", \`/pets\`, props);
export type FindPetByIdProps = Omit<GetProps<Pet, Error, void>, \\"path\\"> & {id: number};
Expand All @@ -60,16 +65,21 @@ export type UseFindPetByIdProps = Omit<UseGetProps<Pet, void>, \\"path\\"> & {id
export const useFindPetById = ({id, ...props}: UseFindPetByIdProps) => useGet<Pet, Error, void>(\`/pets/\${id}\`, props);
export type DeletePetProps = Omit<MutateProps<void, Error, void, void>, \\"path\\" | \\"verb\\">;
export type DeletePetProps = Omit<MutateProps<void, Error, void, string>, \\"path\\" | \\"verb\\">;
export const DeletePet = (props: DeletePetProps) => (
<Mutate<void, Error, void, void>
<Mutate<void, Error, void, string>
verb=\\"DELETE\\"
path={\`/pets\`}
{...props}
/>
);
export type UseDeletePetProps = Omit<UseMutateProps<void, void>, \\"path\\" | \\"verb\\">;
export const useDeletePet = (props: UseDeletePetProps) => useMutate<void, Error, void, string>(\\"DELETE\\", \`/pets\`, props);
"
`;
Loading

0 comments on commit 803ec96

Please sign in to comment.