Skip to content

Commit

Permalink
feat: replace null with undefined
Browse files Browse the repository at this point in the history
This is prep for enabling unicorn/no-null, but it's also good practice
and resulted in nicer code.

BREAKING CHANGE: All internal usages of `null` have been replaced with
`undefined`. This includes mapping `null` to `undefined` in Reddit's API
responses. If you were checking any values for existence with `=== null`
you should replace that check with `=== undefined` (or `== null` or
`== undefined`).
  • Loading branch information
thislooksfun committed Feb 4, 2022
1 parent 113c7aa commit 5ab17b6
Show file tree
Hide file tree
Showing 21 changed files with 232 additions and 223 deletions.
7 changes: 4 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "./controls";
import { AnonGateway } from "./gateway/anon";
import { ClientAuth, OauthGateway } from "./gateway/oauth";
import { Maybe } from "./helper/types";

/**
* Options for instantiating a Client
Expand Down Expand Up @@ -226,12 +227,12 @@ export default class Client {
/**
* Get the refresh token for the current session, if there is one.
*
* @returns The refresh token, or `null` if no token exists.
* @returns The refresh token, or `undefined` if no token exists.
*/
getRefreshToken(): string | null {
getRefreshToken(): Maybe<string> {
if (this.gateway instanceof OauthGateway) {
return this.gateway.getRefreshToken();
}
return null;
return undefined;
}
}
4 changes: 2 additions & 2 deletions src/controls/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RedditObject } from "../helper/types";
import type { ListingObject } from "../listings/listing";
import type { CommentData } from "../objects/comment";

import { assertKind, camelCaseKeys } from "../helper/util";
import { assertKind, fromRedditData } from "../helper/util";
import CommentListing from "../listings/comment";
import { fakeMoreListing } from "../listings/util";
import Comment from "../objects/comment";
Expand Down Expand Up @@ -77,7 +77,7 @@ export default class CommentControls extends VoteableControls {
const rDat = raw.data;
const postId = rDat.link_id.slice(3);
rDat.replies = this.listingifyReplies(rDat.replies, rDat.name, postId);
const data: CommentData = camelCaseKeys(rDat);
const data: CommentData = fromRedditData(rDat);
return new Comment(this, data);
}

Expand Down
13 changes: 7 additions & 6 deletions src/controls/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type Client from "../client";
import type { Query } from "../gateway/types";
import type {
Data,
Maybe,
RedditObject,
SearchSort,
SearchSyntax,
Expand All @@ -10,7 +11,7 @@ import type {
import type { ListingObject, RedditListing } from "../listings/listing";
import type { PostData } from "../objects/post";

import { assertKind, camelCaseKeys } from "../helper/util";
import { assertKind, fromRedditData } from "../helper/util";
import CommentListing from "../listings/comment";
import Listing from "../listings/listing";
import PostListing from "../listings/post";
Expand All @@ -20,7 +21,7 @@ import { LinkPostOptions } from "./subreddit";
import VoteableControls from "./voteable";

function isRemoved(dat: Data) {
if (dat.removed != null) return dat.removed;
if (dat.removed != undefined) return dat.removed;
return !!dat.removal_reason || !!dat.removed_by || !!dat.removed_by_category;
}

Expand Down Expand Up @@ -65,14 +66,14 @@ export default class PostControls extends VoteableControls {
* @param sort The way to sort the search results.
* @param syntax The search syntax to use.
* @param restrictSr Whether or not to restrict the search to the given
* subreddit. If this is `false` or if `subreddit` is `null` this will search
* subreddit. If this is `false` or if `subreddit` is falsy this will search
* all of Reddit.
*
* @returns A listing of posts.
*/
search(
query: string,
subreddit: string | null,
subreddit: Maybe<string>,
time: TimeRange = "all",
sort: SearchSort = "new",
syntax: SearchSyntax = "plain",
Expand All @@ -83,7 +84,7 @@ export default class PostControls extends VoteableControls {
q: query,
sort,
syntax,
restrict_sr: restrictSr && subreddit != null,
restrict_sr: restrictSr && !!subreddit,
};

if (subreddit) opts.subreddit = subreddit;
Expand Down Expand Up @@ -311,7 +312,7 @@ export default class PostControls extends VoteableControls {

rDat.removed = isRemoved(rDat);

const data: PostData = camelCaseKeys(rDat);
const data: PostData = fromRedditData(rDat);

return new Post(this, data);
}
Expand Down
20 changes: 10 additions & 10 deletions src/controls/subreddit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { Comment, Post } from "../objects";
import type { SubredditData } from "../objects/subreddit";
import type { SplitRawPost } from "./post";

import { assertKind, camelCaseKeys } from "../helper/util";
import { assertKind, fromRedditData } from "../helper/util";
import CommentListing from "../listings/comment";
import PostListing from "../listings/post";
import PostOrCommentListing from "../listings/postOrComment";
Expand Down Expand Up @@ -196,10 +196,10 @@ export default class SubredditControls extends BaseControls {
options: BanOptions = {}
): Promise<void> {
const opts: Query = {};
if (options.duration != null) opts.duration = "" + options.duration;
if (options.message != null) opts.ban_message = options.message;
if (options.note != null) opts.note = options.note;
if (options.reason != null) opts.ban_reason = options.reason;
if (options.duration != undefined) opts.duration = "" + options.duration;
if (options.message != undefined) opts.ban_message = options.message;
if (options.note != undefined) opts.note = options.note;
if (options.reason != undefined) opts.ban_reason = options.reason;

await this.friend(sr, name, "banned", opts);
}
Expand Down Expand Up @@ -649,11 +649,11 @@ export default class SubredditControls extends BaseControls {
spoiler: opts.spoiler,
};

if (opts.text != null) req.text = opts.text;
if (opts.url != null) req.url = opts.url;
if (opts.crosspostFullname != null)
if (opts.text != undefined) req.text = opts.text;
if (opts.url != undefined) req.url = opts.url;
if (opts.crosspostFullname != undefined)
req.crosspost_fullname = opts.crosspostFullname;
if (opts.captcha != null) {
if (opts.captcha != undefined) {
req.captcha = opts.captcha.response;
req.iden = opts.captcha.iden;
}
Expand Down Expand Up @@ -699,7 +699,7 @@ export default class SubredditControls extends BaseControls {
rDat.sidebar_html = rDat.description_html;
rDat.description = rDat.public_description;
rDat.description_html = rDat.public_description_html;
const data: SubredditData = camelCaseKeys(rDat);
const data: SubredditData = fromRedditData(rDat);
return new Subreddit(this, data);
}
}
4 changes: 2 additions & 2 deletions src/controls/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
import type Listing from "../listings/listing";
import type { MyUserData, OtherUserData, UserData } from "../objects/user";

import { assertKind, camelCaseKeys } from "../helper/util";
import { assertKind, fromRedditData } from "../helper/util";
import CommentListing from "../listings/comment";
import PostListing from "../listings/post";
import { fakeListingAfter } from "../listings/util";
Expand Down Expand Up @@ -102,7 +102,7 @@ export default class UserControls extends BaseControls {
assertKind("t2", raw);

const rDat = raw.data;
const data: UserData = camelCaseKeys(rDat);
const data: UserData = fromRedditData(rDat);

if ("coins" in rDat) {
return new MyUser(this, data as MyUserData);
Expand Down
23 changes: 11 additions & 12 deletions src/gateway/__tests__/unit/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@ const fcUsernameAuth = () =>
const fcTokenAuth = () => fc.record({ refreshToken: fc.string() });
const fcClientAuth = () => fc.oneof(fcUsernameAuth(), fcTokenAuth());

function fcToken(refresh?: boolean): fc.Arbitrary<Token> {
function fcToken(withRefresh?: boolean): fc.Arbitrary<Token> {
const access = fc.string();
const expiration = fc.integer().map(i => i + Date.now());
const refresh = fc.string({ minLength: 1 });

if (refresh == null) {
if (withRefresh === true) {
return fc.record({ access, expiration, refresh });
} else if (withRefresh === false) {
return fc.record({ access, expiration });
} else {
// No preference given, randomize whether or not there is a refresh token.
return fc.record(
{ access, expiration, refresh: fc.string() },
{ access, expiration, refresh },
{ requiredKeys: ["access", "expiration"] }
);
}

if (refresh) {
return fc.record({ access, expiration, refresh: fc.string() });
} else {
return fc.record({ access, expiration });
}
}

function fcTokenResponse(): fc.Arbitrary<TokenResponse> {
Expand All @@ -41,10 +40,10 @@ function fcTokenResponse(): fc.Arbitrary<TokenResponse> {

// Test class to make protected methods public.
class PublicOauthGateway extends OauthGateway {
public getToken(): Token | null {
public getToken(): Maybe<Token> {
return this.token;
}
public setToken(token: Token | null) {
public setToken(token: Maybe<Token>) {
this.token = token;
}

Expand Down Expand Up @@ -192,7 +191,7 @@ describe("OauthGateway", () => {
});

describe.each([
["not authenticated", null],
["not authenticated", undefined],
[
"access has expired",
{ access: "accessTkn", expiration: Date.now() - 9000 },
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export abstract class Gateway {

protected handleError(msg: string, desc?: string): never {
let errMsg = `Reddit returned an error: ${msg}`;
if (desc != null) errMsg += `: ${desc}`;
if (desc) errMsg += `: ${desc}`;
throw new Error(errMsg);
}

Expand Down
13 changes: 6 additions & 7 deletions src/gateway/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Data, Maybe } from "../helper/types";
import type { Credentials } from "./creds";
import type { BearerAuth } from "./types";

import { camelCaseKeys } from "../helper/util";
import { fromRedditData } from "../helper/util";
import { CredsGateway } from "./creds";
import { Gateway } from "./gateway";

Expand Down Expand Up @@ -74,7 +74,7 @@ type Grant =
export class OauthGateway extends Gateway {
protected initialAuth: Maybe<ClientAuth>;
protected creds: Credentials;
protected token: Token | null;
protected token: Maybe<Token>;

/** @internal */
static async fromAuthCode(
Expand All @@ -98,12 +98,11 @@ export class OauthGateway extends Gateway {
super("https://oauth.reddit.com", userAgent);
this.initialAuth = auth;
this.creds = creds;
this.token = null;
}

/** @internal */
public getRefreshToken(): string | null {
return this.token?.refresh ?? null;
public getRefreshToken(): Maybe<string> {
return this.token?.refresh;
}

protected async auth(): Promise<BearerAuth> {
Expand All @@ -126,7 +125,7 @@ export class OauthGateway extends Gateway {

protected async updateAccessToken(): Promise<void> {
let grant: Grant;
if (this.token?.refresh != null) {
if (this.token?.refresh) {
grant = {
grant_type: "refresh_token",
refresh_token: this.token.refresh,
Expand All @@ -151,7 +150,7 @@ export class OauthGateway extends Gateway {
private async updateTokenFromGrant(grant: Grant) {
const credGate = new CredsGateway(this.creds, this.userAgent);
const raw: Data = await credGate.post("api/v1/access_token", grant);
const tkns: TokenResponse = camelCaseKeys(raw);
const tkns: TokenResponse = fromRedditData(raw);
this.token = {
access: tkns.accessToken,
expiration: Date.now() + tkns.expiresIn * 1000,
Expand Down
13 changes: 10 additions & 3 deletions src/helper/__tests__/unit/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import * as util from "../../util";

describe("camelCaseKeys()", () => {
describe("fromRedditData()", () => {
it("should convert keys to camel case", () => {
const a = { foo_bar: "a", bar_foo: "b" };
const b = { fooBar: "a", barFoo: "b" };

expect(util.camelCaseKeys(a)).toStrictEqual(b);
expect(util.fromRedditData(a)).toStrictEqual(b);
});

it("should preserve camel cased keys", () => {
const a = { fooBar: "a", barFoo: "b" };
const b = { fooBar: "a", barFoo: "b" };

expect(util.camelCaseKeys(a)).toStrictEqual(b);
expect(util.fromRedditData(a)).toStrictEqual(b);
});

it("should replace null with undefined", () => {
const a = { fooBar: null, barFoo: undefined };
const b = { fooBar: undefined, barFoo: undefined };

expect(util.fromRedditData(a)).toStrictEqual(b);
});
});
13 changes: 10 additions & 3 deletions src/helper/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import camelCase from "camelcase";

import { InvalidKindError } from "./errors";

export function camelCaseKeys<T>(obj: Data): T {
/**
* Convert a value from a reddit API response to a snoots data structure.
*
* This performs two steps:
* 1. Replace `null` with `undefined`
* 2. Convert the key from snake_case to camelCase
*/
export function fromRedditData<T>(data: Data): T {
const out: Data = {};
for (const key in obj) {
out[camelCase(key)] = obj[key];
for (const key in data) {
out[camelCase(key)] = data[key] === null ? undefined : data[key];
}
return out as T;
}
Expand Down
2 changes: 1 addition & 1 deletion src/listings/comment/commentListing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class CommentListing extends Listing<Comment> {
constructor(l: RedditListing, ctx: ListingContext) {
let fetcher: Fetcher<Comment> | undefined;

if (l.after != null) {
if (l.after) {
fetcher = new CommentPager(l.after);
}

Expand Down
6 changes: 3 additions & 3 deletions src/listings/comment/more.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Data, RedditObject } from "../../helper/types";
import type Comment from "../../objects/comment";
import type { Fetcher, ListingContext, RedditMore } from "../listing";

import { emptyRedditListing, wrapChildren } from "./../util";
import { emptyRedditListing } from "./../util";
import CommentListing from "./commentListing";

function fixCommentTree(objects: RedditObject[]) {
Expand All @@ -15,7 +15,7 @@ function fixCommentTree(objects: RedditObject[]) {
// Ensure that all the comments have a replies listing.
if (item.kind === "t1") {
if (item.data.replies !== "") throw "?????";
item.data.replies = { kind: "Listing", data: wrapChildren([]) };
item.data.replies = { kind: "Listing", data: { children: [] } };
}
}

Expand Down Expand Up @@ -84,7 +84,7 @@ export default class MoreComments implements Fetcher<Comment> {
children.push(more);
}

return new CommentListing(wrapChildren(children), ctx);
return new CommentListing({ children }, ctx);
}
}
}

0 comments on commit 5ab17b6

Please sign in to comment.