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 #64 from elangobharathi/compose-url
Browse files Browse the repository at this point in the history
compose urls for relative paths cases
  • Loading branch information
Tejas Kumar authored Oct 24, 2018
2 parents 9a27976 + 431abc5 commit 5533b65
Show file tree
Hide file tree
Showing 9 changed files with 654 additions and 41 deletions.
7 changes: 7 additions & 0 deletions src/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { ResolveFunction } from "./Get";
export interface RestfulReactProviderProps<T = any> {
/** The backend URL where the RESTful resources live. */
base: string;
/**
* The path that gets accumulated from each level of nesting
* taking the absolute and relative nature of each path into consideration
*/
parentPath?: string;
/**
* A function to resolve data return from the backend, most typically
* used when the backend response needs to be adapted in some way.
Expand All @@ -26,6 +31,7 @@ export interface RestfulReactProviderProps<T = any> {

const { Provider, Consumer: RestfulReactConsumer } = React.createContext<Required<RestfulReactProviderProps>>({
base: "",
parentPath: "",
resolve: (data: any) => data,
requestOptions: {},
onError: noop,
Expand All @@ -44,6 +50,7 @@ export default class RestfulReactProvider<T> extends React.Component<RestfulReac
onError: noop,
resolve: (data: any) => data,
requestOptions: {},
parentPath: "",
...value,
}}
>
Expand Down
228 changes: 209 additions & 19 deletions src/Get.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,6 @@ describe("Get", () => {
await wait(() => expect(children.mock.calls.length).toBe(2));
});

it("should compose the url with the base", async () => {
nock("https://my-awesome-api.fake")
.get("/plop")
.reply(200);

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="/plop">{children}</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
});

it("should set loading to `true` on mount", async () => {
nock("https://my-awesome-api.fake")
.get("/")
Expand Down Expand Up @@ -163,9 +146,9 @@ describe("Get", () => {
expect(children.mock.calls[1][0]).toEqual(null);
expect(children.mock.calls[1][1].error).toEqual({
data:
"invalid json response body at https://my-awesome-api.fake/ reason: Unexpected token < in JSON at position 0",
"invalid json response body at https://my-awesome-api.fake reason: Unexpected token < in JSON at position 0",
message:
"Failed to fetch: 200 OK - invalid json response body at https://my-awesome-api.fake/ reason: Unexpected token < in JSON at position 0",
"Failed to fetch: 200 OK - invalid json response body at https://my-awesome-api.fake reason: Unexpected token < in JSON at position 0",
});
});

Expand Down Expand Up @@ -663,4 +646,211 @@ describe("Get", () => {
expect(apiCalls).toEqual(2);
});
});
describe("Compose paths and urls", () => {
it("should compose the url with the base", async () => {
nock("https://my-awesome-api.fake")
.get("/plop")
.reply(200);
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="/plop">{children}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(2));
});
it("should compose absolute urls", async () => {
nock("https://my-awesome-api.fake")
.get("/people")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute")
.reply(200);
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="/people">{() => <Get path="/absolute">{children}</Get>}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(3));
});
it("should compose relative urls", async () => {
nock("https://my-awesome-api.fake")
.get("/people")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/people/relative")
.reply(200, { path: "/people/relative" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="/people">{() => <Get path="relative">{children}</Get>}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(3));
expect(children.mock.calls[2][0]).toEqual({ path: "/people/relative" });
});
it("should compose absolute urls with base subpath", async () => {
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/people")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute")
.reply(200, { path: "/absolute" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/MY_SUBROUTE">
<Get path="/people">{() => <Get path="/absolute">{children}</Get>}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(3));
expect(children.mock.calls[2][0]).toEqual({ path: "/absolute" });
});
it("should compose relative urls with base subpath", async () => {
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/people")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/people/relative")
.reply(200, { path: "/people/relative" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/MY_SUBROUTE">
<Get path="/people">{() => <Get path="relative">{children}</Get>}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(3));
expect(children.mock.calls[2][0]).toEqual({ path: "/people/relative" });
});
it("should compose properly when base contains a trailing slash", async () => {
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/people")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/people/relative")
.reply(200, { path: "/people/relative" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/MY_SUBROUTE/">
<Get path="/people">{() => <Get path="relative">{children}</Get>}</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(3));
expect(children.mock.calls[2][0]).toEqual({ path: "/people/relative" });
});
it("should compose more nested absolute and relative urls", async () => {
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute-1")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute-1/relative-1")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute-2")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute-2/relative-2")
.reply(200);
nock("https://my-awesome-api.fake/MY_SUBROUTE")
.get("/absolute-2/relative-2/relative-3")
.reply(200, { path: "/absolute-2/relative-2/relative-3" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/MY_SUBROUTE/">
<Get path="/absolute-1">
{() => (
<Get path="relative-1">
{() => (
<Get path="/absolute-2">
{() => <Get path="relative-2">{() => <Get path="relative-3">{children}</Get>}</Get>}
</Get>
)}
</Get>
)}
</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(6));
expect(children.mock.calls[5][0]).toEqual({ path: "/absolute-2/relative-2/relative-3" });
});
it("should compose properly when one of the paths is empty string", async () => {
nock("https://my-awesome-api.fake")
.get("/absolute-1")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/relative-1")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2/relative-2")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2/relative-2/relative-3")
.reply(200, { path: "/absolute-1/absolute-2/relative-2/relative-3" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/absolute-1">
<Get path="">
{() => (
<Get path="relative-1">
{() => (
<Get path="/absolute-2">
{() => <Get path="relative-2">{() => <Get path="relative-3">{children}</Get>}</Get>}
</Get>
)}
</Get>
)}
</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(6));
expect(children.mock.calls[5][0]).toEqual({ path: "/absolute-1/absolute-2/relative-2/relative-3" });
});
it("should compose properly when one of the paths is lone slash and base has trailing slash", async () => {
nock("https://my-awesome-api.fake")
.get("/absolute-1")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/relative-1")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2/relative-2")
.reply(200);
nock("https://my-awesome-api.fake")
.get("/absolute-1/absolute-2/relative-2/relative-3")
.reply(200, { path: "/absolute-1/absolute-2/relative-2/relative-3" });
const children = jest.fn();
children.mockReturnValue(<div />);
render(
<RestfulProvider base="https://my-awesome-api.fake/absolute-1/">
<Get path="/">
{() => (
<Get path="relative-1">
{() => (
<Get path="/absolute-2">
{() => <Get path="relative-2">{() => <Get path="relative-3">{children}</Get>}</Get>}
</Get>
)}
</Get>
)}
</Get>
</RestfulProvider>,
);
await wait(() => expect(children.mock.calls.length).toBe(6));
expect(children.mock.calls[5][0]).toEqual({ path: "/absolute-1/absolute-2/relative-2/relative-3" });
});
});
});
29 changes: 20 additions & 9 deletions src/Get.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { DebounceSettings } from "lodash";
import debounce from "lodash/debounce";
import * as React from "react";
import url from "url";
import RestfulReactProvider, { InjectedProps, RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import { composePath, composeUrl } from "./util/composeUrl";
import { processResponse } from "./util/processResponse";

/**
Expand Down Expand Up @@ -93,6 +93,11 @@ export interface GetProps<TData, TError> {
*
*/
base?: string;
/**
* The accumulated path from each level of parent GETs
* taking the absolute and relative nature of each path into consideration
*/
parentPath?: string;
/**
* How long do we wait between subsequent requests?
* Uses [lodash's debounce](https://lodash.com/docs/4.17.10#debounce) under the hood.
Expand Down Expand Up @@ -148,6 +153,7 @@ class ContextlessGet<TData, TError> extends React.Component<

public static defaultProps = {
base: "",
parentPath: "",
resolve: (unresolvedData: any) => unresolvedData,
};

Expand All @@ -158,8 +164,13 @@ class ContextlessGet<TData, TError> extends React.Component<
}

public componentDidUpdate(prevProps: GetProps<TData, TError>) {
const { base, path, resolve } = prevProps;
if (base !== this.props.base || path !== this.props.path || resolve !== this.props.resolve) {
const { base, parentPath, path, resolve } = prevProps;
if (
base !== this.props.base ||
parentPath !== this.props.parentPath ||
path !== this.props.path ||
resolve !== this.props.resolve
) {
if (!this.props.lazy) {
this.fetch();
}
Expand Down Expand Up @@ -196,13 +207,13 @@ class ContextlessGet<TData, TError> extends React.Component<
};

public fetch = async (requestPath?: string, thisRequestOptions?: RequestInit) => {
const { base, path, resolve } = this.props;
const { base, parentPath, path, resolve } = this.props;
if (this.state.error || !this.state.loading) {
this.setState(() => ({ error: null, loading: true }));
}

const request = new Request(
url.resolve(base!, requestPath || path || ""),
composeUrl(base!, parentPath!, requestPath || path || ""),
this.getRequestOptions(thisRequestOptions),
);
const response = await fetch(request);
Expand Down Expand Up @@ -231,7 +242,7 @@ class ContextlessGet<TData, TError> extends React.Component<
};

public render() {
const { children, wait, path, base } = this.props;
const { children, wait, path, base, parentPath } = this.props;
const { data, error, loading, response } = this.state;

if (wait && data === null && !error) {
Expand All @@ -242,7 +253,7 @@ class ContextlessGet<TData, TError> extends React.Component<
data,
{ loading, error },
{ refetch: this.fetch },
{ response, absolutePath: url.resolve(base!, path) },
{ response, absolutePath: composeUrl(base!, parentPath!, path) },
);
}
}
Expand All @@ -254,14 +265,14 @@ class ContextlessGet<TData, TError> extends React.Component<
* which all requests will be made.
*
* We compose Consumers immediately with providers
* in order to provide new `base` props that contain
* in order to provide new `parentPath` props that contain
* a segment of the path, creating composable URLs.
*/
function Get<TData = any, TError = any>(props: GetProps<TData, TError>) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulReactProvider {...contextProps} base={url.resolve(contextProps.base, props.path)}>
<RestfulReactProvider {...contextProps} parentPath={composePath(contextProps.parentPath, props.path)}>
<ContextlessGet {...contextProps} {...props} />
</RestfulReactProvider>
)}
Expand Down
Loading

0 comments on commit 5533b65

Please sign in to comment.