Add toJSON methods (#1859)

* tojson things

* fix client

* ignore private properties

* remove extra property descriptors

* handle primitive flattening

* remove unused import

* add toJSON to collections

* reduce stateful props

* state

* allow custom prop names when flattening

* fix client

* fix build

* fix flatten docs

* remove guild.available, cleanup permissions, remove arbitrary id reduction

* fix util import

* add valueOf as needed, update member props

* fix incorrect merge

* update permissionoverwrites and permissions

remove serialization of permissions in PermissionOverwrites#toJSON.
change Permissions#toJSON to serialize permissions, by default excluding
admin checks.

* change Permissions#toJSON to return the primitive

* Permissions#toJSON explicitly return bitfield
This commit is contained in:
Will Nelson
2018-03-01 21:00:21 -08:00
committed by Isabella
parent 24571e465b
commit cf7dcba1a5
28 changed files with 236 additions and 0 deletions

View File

@@ -105,6 +105,10 @@ class BaseClient extends EventEmitter {
clearInterval(interval); clearInterval(interval);
this._intervals.delete(interval); this._intervals.delete(interval);
} }
toJSON(...props) {
return Util.flatten(this, { domain: false }, ...props);
}
} }
module.exports = BaseClient; module.exports = BaseClient;

View File

@@ -411,6 +411,15 @@ class Client extends BaseClient {
); );
} }
toJSON() {
return super.toJSON({
readyAt: false,
broadcasts: false,
pings: false,
presences: false,
});
}
/** /**
* Adds a ping to {@link Client#pings}. * Adds a ping to {@link Client#pings}.
* @param {number} startTime Starting time of the ping * @param {number} startTime Starting time of the ping

View File

@@ -1,3 +1,5 @@
const Util = require('../util/Util');
/** /**
* Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models). * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
*/ */
@@ -23,6 +25,14 @@ class Base {
this._patch(data); this._patch(data);
return clone; return clone;
} }
toJSON(...props) {
return Util.flatten(this, ...props);
}
valueOf() {
return this.id;
}
} }
module.exports = Base; module.exports = Base;

View File

@@ -114,6 +114,10 @@ class Channel extends Base {
} }
return channel; return channel;
} }
toJSON(...props) {
return super.toJSON({ createdTimestamp: true }, ...props);
}
} }
module.exports = Channel; module.exports = Channel;

View File

@@ -205,6 +205,10 @@ class ClientApplication extends Base {
toString() { toString() {
return this.name; return this.name;
} }
toJSON() {
return super.toJSON({ createdTimestamp: true });
}
} }
module.exports = ClientApplication; module.exports = ClientApplication;

View File

@@ -327,6 +327,16 @@ class ClientUser extends Structures.get('User') {
return this.client.api.users('@me').channels.post({ data }) return this.client.api.users('@me').channels.post({ data })
.then(res => this.client.channels.add(res)); .then(res => this.client.channels.add(res));
} }
toJSON() {
return super.toJSON({
friends: false,
blocked: false,
notes: false,
settings: false,
guildSettings: false,
});
}
} }
module.exports = ClientUser; module.exports = ClientUser;

View File

@@ -61,6 +61,15 @@ class Emoji extends Base {
toString() { toString() {
return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name; return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name;
} }
toJSON() {
return super.toJSON({
guild: 'guildID',
createdTimestamp: true,
url: true,
identifier: true,
});
}
} }
module.exports = Emoji; module.exports = Emoji;

View File

@@ -951,6 +951,19 @@ class Guild extends Base {
return this.name; return this.name;
} }
toJSON() {
const json = super.toJSON({
available: false,
createdTimestamp: true,
nameAcronym: true,
presences: false,
voiceStates: false,
});
json.iconURL = this.iconURL();
json.splashURL = this.splashURL();
return json;
}
/** /**
* Creates a collection of this guild's roles, sorted by their position and IDs. * Creates a collection of this guild's roles, sorted by their position and IDs.
* @returns {Collection<Role>} * @returns {Collection<Role>}

View File

@@ -1,6 +1,7 @@
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const Snowflake = require('../util/Snowflake'); const Snowflake = require('../util/Snowflake');
const Webhook = require('./Webhook'); const Webhook = require('./Webhook');
const Util = require('../util/Util');
/** /**
* The target type of an entry, e.g. `GUILD`. Here are the available types: * The target type of an entry, e.g. `GUILD`. Here are the available types:
@@ -220,6 +221,10 @@ class GuildAuditLogs {
return 'ALL'; return 'ALL';
} }
toJSON() {
return Util.flatten(this);
}
} }
/** /**
@@ -371,6 +376,10 @@ class GuildAuditLogsEntry {
get createdAt() { get createdAt() {
return new Date(this.createdTimestamp); return new Date(this.createdTimestamp);
} }
toJSON() {
return Util.flatten(this, { createdTimestamp: true });
}
} }
GuildAuditLogs.Actions = Actions; GuildAuditLogs.Actions = Actions;

View File

@@ -419,6 +419,17 @@ class GuildMember extends Base {
return `<@${this.nickname ? '!' : ''}${this.user.id}>`; return `<@${this.nickname ? '!' : ''}${this.user.id}>`;
} }
toJSON() {
return super.toJSON({
guild: 'guildID',
user: 'userID',
displayName: true,
speaking: false,
lastMessage: false,
lastMessageID: false,
});
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel // These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */ /* eslint-disable no-empty-function */
send() {} send() {}

View File

@@ -149,6 +149,21 @@ class Invite extends Base {
toString() { toString() {
return this.url; return this.url;
} }
toJSON() {
return super.toJSON({
url: true,
expiresTimestamp: true,
presenceCount: false,
memberCount: false,
textChannelCount: false,
voiceChannelCount: false,
uses: false,
channel: 'channelID',
inviter: 'inviterID',
guild: 'guildID',
});
}
} }
module.exports = Invite; module.exports = Invite;

View File

@@ -556,6 +556,18 @@ class Message extends Base {
toString() { toString() {
return this.content; return this.content;
} }
toJSON() {
return super.toJSON({
channel: 'channelID',
author: 'authorID',
application: 'applicationID',
guild: 'guildID',
cleanContent: true,
member: false,
reactions: false,
});
}
} }
module.exports = Message; module.exports = Message;

View File

@@ -1,3 +1,5 @@
const Util = require('../util/Util');
/** /**
* Represents an attachment in a message. * Represents an attachment in a message.
* @param {BufferResolvable|Stream} file The file * @param {BufferResolvable|Stream} file The file
@@ -108,6 +110,10 @@ class MessageAttachment {
*/ */
this.width = data.width; this.width = data.width;
} }
toJSON() {
return Util.flatten(this);
}
} }
module.exports = MessageAttachment; module.exports = MessageAttachment;

View File

@@ -302,6 +302,10 @@ class MessageEmbed {
return this; return this;
} }
toJSON() {
return Util.flatten(this, { hexColor: true });
}
/** /**
* Transforms the embed object to be processed. * Transforms the embed object to be processed.
* @returns {Object} The raw data of this embed * @returns {Object} The raw data of this embed

View File

@@ -1,4 +1,5 @@
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const Util = require('../util/Util');
const GuildMember = require('./GuildMember'); const GuildMember = require('./GuildMember');
/** /**
@@ -139,6 +140,13 @@ class MessageMentions {
return false; return false;
} }
toJSON() {
return Util.flatten(this, {
members: true,
channels: true,
});
}
} }
/** /**

View File

@@ -1,4 +1,5 @@
const GuildEmoji = require('./GuildEmoji'); const GuildEmoji = require('./GuildEmoji');
const Util = require('../util/Util');
const ReactionEmoji = require('./ReactionEmoji'); const ReactionEmoji = require('./ReactionEmoji');
const ReactionUserStore = require('../stores/ReactionUserStore'); const ReactionUserStore = require('../stores/ReactionUserStore');
@@ -55,6 +56,11 @@ class MessageReaction {
return this._emoji; return this._emoji;
} }
toJSON() {
return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' });
}
_add(user) { _add(user) {
if (!this.users.has(user.id)) { if (!this.users.has(user.id)) {
this.users.set(user.id, user); this.users.set(user.id, user);

View File

@@ -1,4 +1,5 @@
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
/** /**
* Represents a permission overwrite for a role or member in a guild channel. * Represents a permission overwrite for a role or member in a guild channel.
@@ -59,6 +60,10 @@ class PermissionOverwrites {
.delete({ reason }) .delete({ reason })
.then(() => this); .then(() => this);
} }
toJSON() {
return Util.flatten(this);
}
} }
module.exports = PermissionOverwrites; module.exports = PermissionOverwrites;

View File

@@ -1,3 +1,4 @@
const Util = require('../util/Util');
const { ActivityTypes, ActivityFlags } = require('../util/Constants'); const { ActivityTypes, ActivityFlags } = require('../util/Constants');
/** /**
@@ -56,6 +57,10 @@ class Presence {
this.activity ? this.activity.equals(presence.activity) : !presence.activity this.activity ? this.activity.equals(presence.activity) : !presence.activity
); );
} }
toJSON() {
return Util.flatten(this);
}
} }
/** /**

View File

@@ -1,3 +1,4 @@
const Util = require('../util/Util');
const Emoji = require('./Emoji'); const Emoji = require('./Emoji');
/** /**
@@ -15,6 +16,14 @@ class ReactionEmoji extends Emoji {
*/ */
this.reaction = reaction; this.reaction = reaction;
} }
toJSON() {
return Util.flatten(this, { identifier: true });
}
valueOf() {
return this.id;
}
} }
module.exports = ReactionEmoji; module.exports = ReactionEmoji;

View File

@@ -353,6 +353,10 @@ class Role extends Base {
return `<@&${this.id}>`; return `<@&${this.id}>`;
} }
toJSON() {
return super.toJSON({ createdTimestamp: true });
}
/** /**
* Compares the positions of two roles. * Compares the positions of two roles.
* @param {Role} role1 First role to compare * @param {Role} role1 First role to compare

View File

@@ -258,6 +258,19 @@ class User extends Base {
return `<@${this.id}>`; return `<@${this.id}>`;
} }
toJSON(...props) {
const json = super.toJSON({
createdTimestamp: true,
defaultAvatarURL: true,
tag: true,
lastMessage: false,
lastMessageID: false,
}, ...props);
json.avatarURL = this.avatarURL();
json.displayAvatarURL = this.displayAvatarURL();
return json;
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel // These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */ /* eslint-disable no-empty-function */
send() {} send() {}

View File

@@ -1,3 +1,5 @@
const Util = require('../util/Util');
/** /**
* Represents a user connection (or "platform identity"). * Represents a user connection (or "platform identity").
*/ */
@@ -43,6 +45,10 @@ class UserConnection {
*/ */
this.integrations = data.integrations; this.integrations = data.integrations;
} }
toJSON() {
return Util.flatten(this);
}
} }
module.exports = UserConnection; module.exports = UserConnection;

View File

@@ -74,6 +74,10 @@ class UserProfile extends Base {
} }
return flags; return flags;
} }
toJSON() {
return super.toJSON({ flags: true });
}
} }
module.exports = UserProfile; module.exports = UserProfile;

View File

@@ -1,3 +1,5 @@
const Util = require('../util/Util');
/** /**
* Represents a Discord voice region for guilds. * Represents a Discord voice region for guilds.
*/ */
@@ -45,6 +47,10 @@ class VoiceRegion {
*/ */
this.sampleHostname = data.sample_hostname; this.sampleHostname = data.sample_hostname;
} }
toJSON() {
return Util.flatten(this);
}
} }
module.exports = VoiceRegion; module.exports = VoiceRegion;

View File

@@ -1,4 +1,5 @@
const Collection = require('../../util/Collection'); const Collection = require('../../util/Collection');
const Util = require('../../util/Util');
const EventEmitter = require('events'); const EventEmitter = require('events');
/** /**
@@ -172,6 +173,10 @@ class Collector extends EventEmitter {
if (reason) this.stop(reason); if (reason) this.stop(reason);
} }
toJSON() {
return Util.flatten(this);
}
/* eslint-disable no-empty-function, valid-jsdoc */ /* eslint-disable no-empty-function, valid-jsdoc */
/** /**
* Handles incoming events from the `handleCollect` function. Returns null if the event should not * Handles incoming events from the `handleCollect` function. Returns null if the event should not

View File

@@ -1,3 +1,5 @@
const Util = require('./Util');
/** /**
* A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has
* an ID, for significantly improved performance and ease-of-use. * an ID, for significantly improved performance and ease-of-use.
@@ -425,6 +427,10 @@ class Collection extends Map {
sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) {
return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0])));
} }
toJSON() {
return this.map(e => typeof e.toJSON === 'function' ? e.toJSON() : Util.flatten(e));
}
} }
module.exports = Collection; module.exports = Collection;

View File

@@ -102,6 +102,10 @@ class Permissions {
return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin));
} }
toJSON() {
return this.bitfield;
}
valueOf() { valueOf() {
return this.bitfield; return this.bitfield;
} }

View File

@@ -12,6 +12,41 @@ class Util {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`); throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
} }
/**
* Flatten an object. Any properties that are collections will get converted to an array of keys.
* @param {Object} obj The object to flatten.
* @param {...Object<string, boolean|string>} [props] Specific properties to include/exclude.
* @returns {Object}
*/
static flatten(obj, ...props) {
const isObject = d => typeof d === 'object' && d !== null;
if (!isObject(obj)) return obj;
props = Object.assign(...Object.keys(obj).filter(k => !k.startsWith('_')).map(k => ({ [k]: true })), ...props);
const out = {};
for (let [prop, newProp] of Object.entries(props)) {
if (!newProp) continue;
newProp = newProp === true ? prop : newProp;
const element = obj[prop];
const elemIsObj = isObject(element);
const valueOf = elemIsObj && typeof element.valueOf === 'function' ? element.valueOf() : null;
// If it's a collection, make the array of keys
if (element instanceof require('./Collection')) out[newProp] = Array.from(element.keys());
// If it's an array, flatten each element
else if (Array.isArray(element)) out[newProp] = element.map(e => Util.flatten(e));
// If it's an object with a primitive `valueOf`, use that value
else if (valueOf && !isObject(valueOf)) out[newProp] = valueOf;
// If it's a primitive
else if (!elemIsObj) out[newProp] = element;
}
return out;
}
/** /**
* Splits a string into multiple chunks at a designated character that do not exceed a specific length. * Splits a string into multiple chunks at a designated character that do not exceed a specific length.
* @param {string} text Content to split * @param {string} text Content to split