Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active November 7, 2019 06:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/1eae40a87297c05d45f8bff14f7d8729 to your computer and use it in GitHub Desktop.
Save OliverJAsh/1eae40a87297c05d45f8bff14f7d8729 to your computer and use it in GitHub Desktop.
TypeScript URL helpers
import urlHelpers, { Url, UrlWithParsedQuery, UrlWithStringQuery } from 'url';
import queryStringHelpers from 'querystring';
import { Option } from 'funfix-core';
import pipe from 'lodash/flow';
import { ParsedUrlQuery } from 'querystring';
const isNonEmptyString = (str: string) => str.length > 0;
// https://github.com/gcanti/fp-ts/blob/15f4f701ed2ba6b0d306ba7b8ca9bede223b8155/src/function.ts#L127
const flipCurried = <A, B, C>(fn: (a: A) => (b: B) => C) => (b: B) => (a: A) =>
fn(a)(b);
const getPathnameFromParts = (parts: string[]) =>
`/${parts.map(encodeURIComponent).join('/')}`;
const getPartsFromPathname = (pathname: string) =>
pathname
.split('/')
.filter(isNonEmptyString)
.map(decodeURIComponent);
const buildSearchFromQueryString = (queryString: string) => `?${queryString}`;
const buildSearchFromQuery = pipe(queryStringHelpers.stringify, buildSearchFromQueryString);
const parseUrlWithQueryString = (url: string): UrlWithParsedQuery =>
urlHelpers.parse(
url,
// Parse the query string
true,
);
const mapUrl = (
fn: ({ parsedUrl }: { parsedUrl: UrlWithStringQuery }) => UrlWithStringQuery,
) =>
pipe(
({ url }: { url: string }) => urlHelpers.parse(url),
parsedUrl => fn({ parsedUrl }),
urlHelpers.format,
);
const mapUrlWithParsedQuery = (
fn: ({ parsedUrl }: { parsedUrl: UrlWithParsedQuery }) => UrlWithParsedQuery,
) =>
pipe(
({ url }: { url: string }) => parseUrlWithQueryString(url),
parsedUrl => fn({ parsedUrl }),
urlHelpers.format,
);
const addQueryToParsedUrl = ({
queryToAppend,
}: {
queryToAppend: ParsedUrlQuery;
}) => ({ parsedUrl }: { parsedUrl: UrlWithParsedQuery }): Url => {
const { protocol, host, hash, pathname, query: existingQuery } = parsedUrl;
const newQuery = { ...existingQuery, ...queryToAppend };
const newSearch = buildSearchFromQuery(newQuery);
// We omit some parsed values (e.g. `query`) as the formatted equivalents take precendence (e.g.
// `search`), so they are not required.
const newParsedUrl = {
protocol,
host,
hash,
pathname,
search: newSearch,
};
return newParsedUrl;
};
const addQueryToUrl = flipCurried(
pipe(
addQueryToParsedUrl,
mapUrlWithParsedQuery,
),
);
const parsePath = pipe(
urlHelpers.parse,
({ search, pathname }) => ({ search, pathname }),
);
const replacePathInParsedUrl = ({ newPath }: { newPath: string }) => ({
parsedUrl,
}: {
parsedUrl: UrlWithStringQuery;
}) =>
pipe(
() => parsePath(newPath),
newPathParsed => ({ ...parsedUrl, ...newPathParsed }),
)();
const replacePathInUrl = flipCurried(
pipe(
replacePathInParsedUrl,
mapUrl,
),
);
const appendPathnameToParsedUrl = ({
pathnameToAppend,
}: {
pathnameToAppend: string;
}) => ({ parsedUrl }: { parsedUrl: UrlWithStringQuery }) => {
const pathnameParts = Option.of(parsedUrl.pathname)
.map(getPartsFromPathname)
.getOrElse([]);
const pathnamePartsToAppend = getPartsFromPathname(pathnameToAppend);
const newPathnameParts = [...pathnameParts, ...pathnamePartsToAppend];
const newPathname = getPathnameFromParts(newPathnameParts);
return { ...parsedUrl, pathname: newPathname };
};
const appendPathnameToUrl = flipCurried(
pipe(
appendPathnameToParsedUrl,
mapUrl,
),
);
const replaceHashInParsedUrl = ({
newHash,
}: {
newHash: string | undefined;
}) => ({ parsedUrl }: { parsedUrl: UrlWithStringQuery }) => ({
...parsedUrl,
hash: newHash,
});
const replaceHashInUrl = flipCurried(
pipe(
replaceHashInParsedUrl,
mapUrl,
),
);
// Examples
//
console.log(
addQueryToUrl({ url: 'http://foo.com/' })({ queryToAppend: { a: 'b' } }) // => http://foo.com/?a=b
)
console.log(
addQueryToUrl({ url: 'http://foo.com/?a=b&b=c' })({ queryToAppend: { c: 'd' } }) // => http://foo.com/?a=b&b=c&c=d
)
console.log(
replacePathInUrl({ url: 'https://foo.com/foo?example' })({ newPath: '/bar' }),
); // => https://foo.com/bar
console.log(appendPathnameToUrl({ url: '/foo' })({ pathnameToAppend: '/bar' })); // => /foo/bar
console.log(
appendPathnameToUrl({ url: '/foo?example' })({ pathnameToAppend: '/bar' }),
); // => /foo/bar?example
console.log(replaceHashInUrl({ url: '/foo' })({ newHash: '#bar' })); // => /foo#bar
console.log(replaceHashInUrl({ url: '/foo#bar' })({ newHash: undefined })); // => /foo
console.log(replaceHashInUrl({ url: '/foo#bar' })({ newHash: '#baz' })); // => /foo#baz
@OliverJAsh
Copy link
Author

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