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

Generate useMutate components #126

Merged
merged 5 commits into from
May 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data is not anymore optional, so we can define string for DELETE operation.
This is to have a bit more type safety, mutate() is not possible anymore if a body is required.

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" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sad that it's not TS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we contribute this to @DefinitelyTyped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 stars on github, I've tried but without a lot of hope 😅 I will contribute to definitelyTyped to add this later.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe export this from that other place you do it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more have only one no-console, I prefer to not export this hack ^^ So you need to ask yourself "is it a real usecase or just a console.log for debugging?"


// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've disabled the no-console rule for the block, so I re-enable it ^^

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

};

/**
* 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