Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch code patterns to go from mutating options, to keeping them read only #5267

Open
kyranet opened this issue Jan 26, 2021 · 0 comments
Open

Comments

@kyranet
Copy link
Member

kyranet commented Jan 26, 2021

Is your feature request related to a problem? Please describe.
There are many issues that arise from mutating option objects, for example, they can mutate data in unexpected ways, see #5261 (comment). Furthermore, we do not document in which cases we mutate the options, and when we don't, which causes unconsistent behaviour.

We have been mutating options for a very long time, so the pattern is repeated and used across the entire library, which can take a lot of time to switch, and a single PR would probably be too big to test and review. However, it's also not consistent.

Describe the ideal solution
In fact, we also avoid mutating some options, which is the case for Guild#edit:

edit(data, reason) {
const _data = {};
if (data.name) _data.name = data.name;
if (data.region) _data.region = data.region;
if (typeof data.verificationLevel !== 'undefined') {
_data.verification_level =
typeof data.verificationLevel === 'number'
? Number(data.verificationLevel)
: VerificationLevels.indexOf(data.verificationLevel);
}
if (typeof data.afkChannel !== 'undefined') {
_data.afk_channel_id = this.client.channels.resolveID(data.afkChannel);
}
if (typeof data.systemChannel !== 'undefined') {
_data.system_channel_id = this.client.channels.resolveID(data.systemChannel);
}
if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout);
if (typeof data.icon !== 'undefined') _data.icon = data.icon;
if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner);
if (data.splash) _data.splash = data.splash;
if (data.discoverySplash) _data.discovery_splash = data.discoverySplash;
if (data.banner) _data.banner = data.banner;
if (typeof data.explicitContentFilter !== 'undefined') {
_data.explicit_content_filter =
typeof data.explicitContentFilter === 'number'
? data.explicitContentFilter
: ExplicitContentFilterLevels.indexOf(data.explicitContentFilter);
}
if (typeof data.defaultMessageNotifications !== 'undefined') {
_data.default_message_notifications =
typeof data.defaultMessageNotifications === 'string'
? DefaultMessageNotifications.indexOf(data.defaultMessageNotifications)
: data.defaultMessageNotifications;
}
if (typeof data.systemChannelFlags !== 'undefined') {
_data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags);
}
if (typeof data.rulesChannel !== 'undefined') {
_data.rules_channel_id = this.client.channels.resolveID(data.rulesChannel);
}
if (typeof data.publicUpdatesChannel !== 'undefined') {
_data.public_updates_channel_id = this.client.channels.resolveID(data.publicUpdatesChannel);
}
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
return this.client.api
.guilds(this.id)
.patch({ data: _data, reason })
.then(newData => this.client.actions.GuildUpdate.handle(newData).updated);
}

For all intents and purposes, there are simpler alternatives which not only reduce code clutter, but also optimise very well with V8. My favourite is to create a request object using the following pattern:

edit(options, reason) {
	const data = {
		name: options.name,
		region: options.region,
		afk_timeout: data.afkTimeout,
		// ...
	};

	// Do something with data
}

This pattern is a huge simplification of the previous, which works flawlessly as undefined fields are not serialized into JSON:

JSON.stringify({ foo: 'bar', hello: undefined });
// -> {"foo":"bar"}

Another pattern is to use object destructure, which is used in other places:

createInvite({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) {
return this.client.api
.channels(this.id)
.invites.post({
data: {
temporary,
max_age: maxAge,
max_uses: maxUses,
unique,
},
reason,
})
.then(invite => new Invite(this.client, invite));
}

If we desire to future-proof and let users pass raw options (while we're at it), which can also work for properties that translate 1:1 with Discord's (in both key names and value types), the following pattern is also available, from a project I used to work on:

async edit({ icon, splash, banner, ...options }, reason) {
	const data = {
		icon: icon ? await resolveImageToBase64(icon) : icon,
		splash: splash ? await resolveImageToBase64(splash) : splash,
		banner: banner ? await resolveImageToBase64(banner) : banner,
		// More properties that need translating
		...options
	};

	// Use data
}

This way, if Discord were to add a new property we can patch, all we'd need to do, is to pass the raw property, at least, until we support it officially, this way users can have the option to use bleeding edge features while using stable code, not requiring client.api to bypass the data limitations (which is the other use-case for developers besides bypassing cache, which we're slowly fixing in the latest PRs).

This pattern also has the advantage that translating the code to strict TypeScript would be a lot easier, for example in the last case, we can do const data: RESTPatchAPIGuildJSONBody = { /* ... */ } to have it typed with Discord API's types.

Describe alternatives you've considered
I am open to more alternatives, please do not hesitate to come up with a better idea!

Note: I made this an issue and not a discussion because it's a discussion about a bug, which once resolved (via PRs), this issue will then be "resolved" automatically, something we lack with the discussions feature.

Additional context
Supersedes #5261 and extends its scope to the entire library.

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

No branches or pull requests

2 participants