Skip to content

Commit

Permalink
docs: better docs for what's done so far
Browse files Browse the repository at this point in the history
  • Loading branch information
ckohen committed Feb 8, 2024
1 parent 0602395 commit 1e9cf03
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 22 deletions.
61 changes: 56 additions & 5 deletions packages/structures/src/Structure.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
/* eslint-disable jsdoc/check-param-names */
import { kData } from './utils/symbols.js';
import type { ReplaceOmittedWithUnknown } from './utils/types.js';

/**
* Additional options needed to appropriately construct / patch data
*
* @internal
*/
export interface StructureExtraOptions {
/**
* The template used to remove unwanted properties from raw data storage
*/
template?: {};
}

/**
* Represents a data model from the Discord API
*
* @privateRemarks
* Explanation of the type complexity surround Structure:
*
* There are two layers of Omitted generics, one here, which allows omitting things at the library level so we do not accidentally
Expand All @@ -12,26 +28,61 @@ import type { ReplaceOmittedWithUnknown } from './utils/types.js';
* In order to safely set and access this data, the constructor and patch take data as "partial" and forcibly assigns it to kData. To acommodate this,
* kData stores properties as `unknown` when it is omitted, which allows accessing the property in getters even when it may not actually be present.
* This is the most technically correct way of represnting the value, especially since there is no way to guarantee runtime matches the "type cast."
*
* @internal
*/

export abstract class Structure<DataType, Omitted extends keyof DataType | '' = ''> {
/**
* The raw data from the API for this struture
*
* @internal
*/
protected [kData]: Readonly<ReplaceOmittedWithUnknown<Omitted, DataType>>;

protected constructor(data: Readonly<Partial<DataType>>, { template }: { template?: {} } = {}) {
/**
* Creates a new structure to represent API data
*
* @param data - the data from the API that this structure will represent
* @param extraOptions - any additional options needed to appropriately construct a structure from the data
* @internal
*/
protected constructor(data: Readonly<Partial<DataType>>, { template }: StructureExtraOptions = {}) {
this[kData] = Object.assign(template ? Object.create(template) : {}, data);
this._optimizeData(data);
}

protected _patch(data: Readonly<Partial<DataType>>, { template }: { template?: {} } = {}): this {
/**
* Patches the raw data of this object in place
*
* @param data - the updated data from the API to patch with
* @param extraOptions - any additional options needed to appropriately patch the data
* @returns this
* @internal
*/
protected _patch(data: Readonly<Partial<DataType>>, { template }: StructureExtraOptions = {}): this {
this[kData] = Object.assign(template ? Object.create(template) : {}, this[kData], data);
this._optimizeData(data);
return this;
}

/**
* Function called to ensure stored raw data is in optimized formats, used in tandem with a data template
*
* @example created_timestamp is an ISO string, this can be stored in optimized form as a number
* @param _data - the raw data received from the API to optimize
* @remarks Implementation to be done in subclasses where needed
* @virtual
* @internal
*/
protected _optimizeData(_data: Partial<DataType>) {}

/**
* Transforms this object to its JSON format with raw API data (or close to it),
* automatically called by `JSON.stringify()` when this structure is stringified
*
* @remarks
* The type of this data is determined by omissions at runtime and is only guaranteed for default omissions
* @privateRemarks
* When omitting properties at the library level, this must be overriden to re-add those properties
*/
public toJSON(): DataType {
// This will be DataType provided nothing is omitted, when omits occur, subclass needs to overwrite this.
return { ...this[kData] } as DataType;
Expand Down
1 change: 1 addition & 0 deletions packages/structures/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './invites/index.js';
export * from './users/index.js';
export * from './Structure.js';
43 changes: 35 additions & 8 deletions packages/structures/src/invites/Invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { type APIInvite, type APIExtendedInvite, RouteBases } from 'discord-api-
import { Structure } from '../Structure.js';
import { kData, kExpiresTimestamp, kCreatedTimestamp } from '../utils/symbols.js';

/**
* Represents an invitation to a discord channel
*
* @typeParam Omitted - Specify the propeties that will not be stored in the raw data field as a union, implement via `DataTemplate`
* @typeParam Extended - Whether the invite is a full extended invite
*/
export class Invite<
Omitted extends keyof APIExtendedInvite | '' = 'created_at' | 'expires_at',
Extended extends boolean = false,
Expand All @@ -17,16 +23,27 @@ export class Invite<

/**
* The template used for removing data from the raw data stored for each Invite
* <info>This template has defaults, if you want to remove additional data and keep the defaults,
* use `Object.defineProperties`. To override the defaults, set this value directly.</info>
*
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
* use `Object.defineProperties`. To override the defaults, set this value directly.
*/
public static DataTemplate: Partial<APIExtendedInvite> = {
set created_at(_: string) {},
set expires_at(_: string) {},
};

/**
* Optimized storage of {@link APIExtendedInvite.expires_at}
*
* @internal
*/
protected [kExpiresTimestamp]: number | null = null;

/**
* Optimized storage of {@link APIExtendedInvite.created_at}
*
* @internal
*/
protected [kCreatedTimestamp]: number | null = null;

public constructor(
Expand All @@ -38,11 +55,17 @@ export class Invite<
super(data, { template: Invite.DataTemplate });
}

/**
* {@inheritDoc Structure._patch}
*/
public override _patch(data: Partial<APIExtendedInvite>) {
super._patch(data, { template: Invite.DataTemplate });
return this;
}

/**
* {@inheritDoc Structure._optimizeData}
*/
protected override _optimizeData(data: Partial<APIExtendedInvite>) {
this[kExpiresTimestamp] = data.expires_at ? Date.parse(data.expires_at) : this[kExpiresTimestamp] ?? null;
this[kCreatedTimestamp] = data.created_at ? Date.parse(data.created_at) : this[kCreatedTimestamp] ?? null;
Expand All @@ -64,15 +87,17 @@ export class Invite<

/**
* The approximate number of online members of the guild this invite is for
* <info>Only available when the invite was fetched from `GET /invites/<code>` with counts</info>
*
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
*/
public get presenceCount() {
return this[kData].approximate_presence_count;
}

/**
* The approximate total number of members of the guild this invite is for
* <info>Only available when the invite was fetched from `GET /invites/<code>` with counts</info>
*
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
*/
public get memberCount() {
return this[kData].approximate_member_count;
Expand Down Expand Up @@ -155,6 +180,9 @@ export class Invite<
return this.url;
}

/**
* {@inheritDoc Structure.toJSON}
*/
public override toJSON() {
const clone = super.toJSON();
if (this[kExpiresTimestamp]) {
Expand All @@ -168,11 +196,10 @@ export class Invite<
return clone;
}

/**
* {@inheritDoc Object.valueOf}
*/
public override valueOf() {
return this.code ?? super.valueOf();
}
}

const test = new Invite({} as APIExtendedInvite);
if (test.code) {
}
1 change: 1 addition & 0 deletions packages/structures/src/invites/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Invite.js';
32 changes: 23 additions & 9 deletions packages/structures/src/users/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { kData } from '../utils/symbols.js';

/**
* Represents any user on Discord.
*
* @typeParam Omitted - Specify the propeties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser, Omitted> {
/**
Expand All @@ -21,6 +23,9 @@ export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<API
super(data, { template: User.DataTemplate });
}

/**
* {@inheritDoc Structure._patch}
*/
public override _patch(data: Partial<APIUser>) {
return super._patch(data, { template: User.DataTemplate });
}
Expand Down Expand Up @@ -76,63 +81,71 @@ export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<API

/**
* Whether the user has mfa enabled
* <info>This property is only set when the user was fetched with an OAuth2 token and the `identify` scope</info>
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
*/
public get mfaEnabled() {
return this[kData].mfa_enabled;
}

/**
* The user's banner hash
* <info>This property is only set when the user was manually fetched</info>
*
* @remarks This property is only set when the user was manually fetched
*/
public get banner() {
return this[kData].banner;
}

/**
* The base 10 accent color of the user's banner
* <info>This property is only set when the user was manually fetched</info>
*
* @remarks This property is only set when the user was manually fetched
*/
public get accentColor() {
return this[kData].accent_color;
}

/**
* The user's primary discord language
* <info>This property is only set when the user was fetched with an Oauth2 token and the `identify` scope</info>
*
* @remarks This property is only set when the user was fetched with an Oauth2 token and the `identify` scope
*/
public get locale() {
return this[kData].locale;
}

/**
* Whether the email on the user's account has been verified
* <info>This property is only set when the user was fetched with an OAuth2 token and the `email` scope</info>
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
*/
public get verified() {
return this[kData].verified;
}

/**
* The user's email
* <info>This property is only set when the user was fetched with an OAuth2 token and the `email` scope</info>
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
*/
public get email() {
return this[kData].email;
}

/**
* The flags on the user's account
* <info> This property is only set when the user was fetched with an OAuth2 token and the `identity` scope</info>
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identity` scope
*/
public get flags() {
return this[kData].flags;
}

/**
* The type of nitro subscription on the user's account
* <info>This property is only set when the user was fetched with an OAuth2 token and the `identify` scope</info>
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
*/
public get premiumType() {
return this[kData].premium_type;
Expand Down Expand Up @@ -169,7 +182,8 @@ export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<API

/**
* The hexadecimal version of the user accent color, with a leading hash
* <info>This property is only set when the user was manually fetched</info>
*
* @remarks This property is only set when the user was manually fetched
*/
public get hexAccentColor() {
if (typeof this.accentColor !== 'number') return this.accentColor;
Expand Down

0 comments on commit 1e9cf03

Please sign in to comment.