-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initial structures design attempt
- Loading branch information
Showing
5 changed files
with
265 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,56 @@ | ||
import { describe, test, expect } from 'vitest'; | ||
import { describe, test, expect, beforeEach } from 'vitest'; | ||
import { Structure } from '../src/Structure.js'; | ||
import { data as kData } from '../src/utils/symbols.js'; | ||
|
||
describe('Base Structure', () => { | ||
const data = { test: true, patched: false }; | ||
// @ts-expect-error Structure constructor is protected | ||
const struct: Structure<typeof data> = new Structure(data); | ||
const data = { test: true, patched: false, removed: true }; | ||
let struct: Structure<typeof data>; | ||
beforeEach(() => { | ||
// @ts-expect-error Structure constructor is protected | ||
struct = new Structure(data); | ||
}); | ||
|
||
test('Data reference is identical (no shallow clone at base level)', () => { | ||
expect(struct[kData]).toBe(data); | ||
test('Data reference is not identical (clone via Object.assign)', () => { | ||
expect(struct[kData]).not.toBe(data); | ||
expect(struct[kData]).toEqual(data); | ||
}); | ||
|
||
test('toJSON shallow clones but retains data equality', () => { | ||
expect(struct.toJSON()).not.toBe(data); | ||
expect(struct[kData]).toEqual(data); | ||
test('Remove properties via template (constructor)', () => { | ||
// @ts-expect-error Structure constructor is protected | ||
const templatedStruct: Structure<typeof data> = new Structure(data, { template: { set removed(_) {} } }); | ||
expect(templatedStruct[kData].removed).toBe(undefined); | ||
// Setters still exist and pass "in" test unfortunately | ||
expect('removed' in templatedStruct[kData]).toBe(true); | ||
expect(templatedStruct[kData]).toEqual({ test: true, patched: false }); | ||
}); | ||
|
||
test('patch clones data and updates in place', () => { | ||
const dataBefore = struct[kData]; | ||
// @ts-expect-error Structure#patch is protected | ||
const patched = struct.patch({ patched: true }); | ||
expect(patched[kData].patched).toBe(true); | ||
// Patch in place | ||
expect(struct[kData]).toBe(patched[kData]); | ||
// Clones | ||
expect(dataBefore.patched).toBe(false); | ||
expect(dataBefore).not.toBe(patched[kData]); | ||
}); | ||
|
||
test('Remove properties via template (patch)', () => { | ||
// @ts-expect-error Structure constructor is protected | ||
const templatedStruct: Structure<typeof data> = new Structure(data, { template: { set removed(_) {} } }); | ||
// @ts-expect-error Structure#patch is protected | ||
templatedStruct.patch({ removed: false }, { template: { set removed(_) {} } }); | ||
expect(templatedStruct[kData].removed).toBe(undefined); | ||
// Setters still exist and pass "in" test unfortunately | ||
expect('removed' in templatedStruct[kData]).toBe(true); | ||
expect(templatedStruct[kData]).toEqual({ test: true, patched: false }); | ||
}); | ||
|
||
test('toJSON clones but retains data equality', () => { | ||
const json = struct.toJSON(); | ||
expect(json).not.toBe(data); | ||
expect(json).not.toBe(struct[kData]); | ||
expect(struct[kData]).toEqual(json); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,32 @@ | ||
import { data as kData } from './utils/symbols.js'; | ||
|
||
/** | ||
* 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 | ||
* access them. This generic should only be used within this library, as passing it higher will make it impossible to safely access kData | ||
* | ||
* The second layer, in the exported structure is effectively a type cast that allows the getters types to match whatever data template | ||
* is used. In order for this to function properly, we need to cast the return values of the getters, | ||
* utilizing this level of Omit to guarantee the original type or never. | ||
* | ||
* @internal | ||
*/ | ||
|
||
export abstract class Structure<DataType, Omitted extends keyof DataType | '' = ''> { | ||
protected [kData]: Readonly<Partial<Omit<DataType, Omitted>>>; | ||
protected [kData]: Readonly<Omit<DataType, Omitted>>; | ||
|
||
protected constructor(data: Readonly<Omit<DataType, Omitted>>, { template }: { template?: {} } = {}) { | ||
this[kData] = Object.assign(template ? Object.create(template) : {}, data); | ||
} | ||
|
||
protected constructor(data: Readonly<Partial<Omit<DataType, Omitted>>>) { | ||
// Do not shallow clone data here as subclasses should do it via a blueprint in their own constructor (also allows them to set the constructor to public) | ||
this[kData] = data; | ||
protected patch(data: Readonly<Partial<Omit<DataType, Omitted>>>, { template }: { template?: {} } = {}): this { | ||
this[kData] = Object.assign(template ? Object.create(template) : {}, this[kData], data); | ||
return this; | ||
} | ||
|
||
public toJSON(): Partial<DataType> { | ||
public toJSON(): DataType { | ||
// This will be DataType provided nothing is omitted, when omits occur, subclass needs to overwrite this. | ||
return { ...this[kData] } as Partial<DataType>; | ||
return { ...this[kData] } as DataType; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
console.log('Hello, from @discordjs/structures'); | ||
export * from './users/index.js'; | ||
export * from './Structure.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import { DiscordSnowflake } from '@sapphire/snowflake'; | ||
import type { APIUser } from 'discord-api-types/v10'; | ||
import { Structure } from '../Structure.js'; | ||
import { data as kData } from '../utils/symbols.js'; | ||
|
||
let UserDataTemplate: Partial<APIUser> = {}; | ||
|
||
/** | ||
* Sets the template used for removing data from the raw data stored for each User | ||
* | ||
* @param template - the template | ||
*/ | ||
export function setUserDataTemplate(template: Partial<APIUser>) { | ||
UserDataTemplate = template; | ||
} | ||
|
||
/** | ||
* Gets the template used for removing data from the raw data stored for each User | ||
* | ||
* @returns the template | ||
*/ | ||
export function getUserDataTemplate(): Readonly<Partial<APIUser>> { | ||
return UserDataTemplate; | ||
} | ||
|
||
/** | ||
* Represents any user on Discord. | ||
*/ | ||
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser> { | ||
public constructor( | ||
/** | ||
* The raw data received from the API for the user | ||
*/ | ||
data: Omit<APIUser, Omitted>, | ||
) { | ||
// Cast here so the getters can access the properties, and provide typesafety by explicitly assigning return values | ||
super(data as APIUser, { template: UserDataTemplate }); | ||
} | ||
|
||
public override patch(data: Partial<APIUser>) { | ||
return super.patch(data, { template: UserDataTemplate }); | ||
} | ||
|
||
/** | ||
* The user's id | ||
*/ | ||
public get id() { | ||
return this[kData].id as 'id' extends Omitted ? never : APIUser['id']; | ||
} | ||
|
||
/** | ||
* The username of the user | ||
*/ | ||
public get username() { | ||
return this[kData].username as 'username' extends Omitted ? never : APIUser['username']; | ||
} | ||
|
||
/** | ||
* The user's 4 digit tag, if a bot or not migrated to unique usernames | ||
*/ | ||
public get discriminator() { | ||
return this[kData].discriminator as 'discriminator' extends Omitted ? never : APIUser['discriminator']; | ||
} | ||
|
||
/** | ||
* The user's display name, the application name for bots | ||
*/ | ||
public get globalName() { | ||
return this[kData].global_name as 'global_name' extends Omitted ? never : APIUser['global_name']; | ||
} | ||
|
||
/** | ||
* The user avatar's hash | ||
*/ | ||
public get avatar() { | ||
return this[kData].avatar as 'avatar' extends Omitted ? never : APIUser['avatar']; | ||
} | ||
|
||
/** | ||
* Whether the user is a bot | ||
*/ | ||
public get bot() { | ||
return (this[kData].bot ?? false) as 'bot' extends Omitted ? never : APIUser['bot']; | ||
} | ||
|
||
/** | ||
* Whether the user is an Official Discord System user | ||
*/ | ||
public get system() { | ||
return (this[kData].system ?? false) as 'system' extends Omitted ? never : APIUser['system']; | ||
} | ||
|
||
/** | ||
* 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> | ||
*/ | ||
public get mfaEnabled() { | ||
return this[kData].mfa_enabled as 'mfa_enabled' extends Omitted ? never : APIUser['mfa_enabled']; | ||
} | ||
|
||
/** | ||
* The user's banner hash | ||
* <info>This property is only set when the user was manually fetched</info> | ||
*/ | ||
public get banner() { | ||
return this[kData].banner as 'banner' extends Omitted ? never : APIUser['banner']; | ||
} | ||
|
||
/** | ||
* The base 10 accent color of the user's banner | ||
* <info>This property is only set when the user was manually fetched</info> | ||
*/ | ||
public get accentColor() { | ||
return this[kData].accent_color as 'accent_color' extends Omitted ? never : APIUser['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> | ||
*/ | ||
public get locale() { | ||
return this[kData].locale as 'locale' extends Omitted ? never : APIUser['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> | ||
*/ | ||
public get verified() { | ||
return this[kData].verified as 'verified' extends Omitted ? never : APIUser['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> | ||
*/ | ||
public get email() { | ||
return this[kData].email as 'email' extends Omitted ? never : APIUser['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> | ||
*/ | ||
public get flags() { | ||
return this[kData].flags as 'flags' extends Omitted ? never : APIUser['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> | ||
*/ | ||
public get premiumType() { | ||
return this[kData].premium_type as 'premium_type' extends Omitted ? never : APIUser['premium_type']; | ||
} | ||
|
||
/** | ||
* The public flags for the user | ||
*/ | ||
public get publicFlags() { | ||
return this[kData].public_flags as 'public_flags' extends Omitted ? never : APIUser['public_flags']; | ||
} | ||
|
||
/** | ||
* The user's avatar decoration hash | ||
*/ | ||
public get avatarDecoration() { | ||
return this[kData].avatar_decoration as 'avatar_decoration' extends Omitted ? never : APIUser['avatar_decoration']; | ||
} | ||
|
||
/** | ||
* The timestamp the user was created at | ||
*/ | ||
public get createdTimestamp() { | ||
return this.id ? DiscordSnowflake.timestampFrom(this.id) : null; | ||
} | ||
|
||
/** | ||
* The time the user was created at | ||
*/ | ||
public get createdAt() { | ||
return this.createdTimestamp ? new Date(this.createdTimestamp) : null; | ||
} | ||
|
||
/** | ||
* 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> | ||
*/ | ||
public get hexAccentColor() { | ||
if (typeof this.accentColor !== 'number') return this.accentColor; | ||
return `#${this.accentColor.toString(16).padStart(6, '0')}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './User.js'; |