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
RFC(builders): The interface, the validation, and the pains you have had with them so far #8015
Comments
Here are my five cent:
|
I don't use builders, so I'm going to try to be objective as to my thoughts on design but I can also touch a little on why I never adopted them. InterfaceI'm a big fan of the I feel partly responsible for the design of the components builders. When I did buttons and select menus in discord.js originally (pre-builders) I based them on the design of the MessageEmbed class. It seemed logical to keep consistency with the other common type of "message addition" in the library. I don't think that was an inherently incorrect decision in context, but it clearly had an undesirable impact later. When these components, and Embeds too for that matter, were moved to builders is where the above precedent introduced a problem. The old builder-class style does not align with the But all of this is largely driven by a desire for a particular style rather than a consideration of the actual requirements of these classes. Somewhat related, its also worth noting that discord.js still maintains entirely separate So to address the questions, I'm first going to ask a couple of my own: Does there need to be consistency of language and style (e.g. casing of properties, exposing properties, Why did Components and Embeds get moved to builders at all? Like it or not, building a component or embed is not at all the same use-case as building an application command. There's a vastly different set of requirements to be considered.
I'm somewhat undecided on this. As much as I like the idea of a seamless flow where you just build and produce JSON, I can't pretend that there aren't use-cases where you'd want to know what has currently been set inside the builder. Having to call toJSON() every time to read one of these properties, because they aren't exposed, isn't a particularly appealing interface. On the other hand, exposing these properties, be it on the class or the data object, exposes them as snake_case. Personally, I see this as correct, but it's a known contentious point that points back to the consistency of language question.
I would agree that
My first thought hated this, but surprisingly I changed very quickly. For a consistent interface I think this would be great, but I think I'll come back to some concerns when I write pain points. ValidationNot a lot to say here but yes, this would be a huge clean-up. What would the toggle switch look like, how would a use disable validation? Pain pointsAs I write these, I feel like I may contradict some of the stuff above, but here we go... I completely understand the need for the builders interface to have a clear, consistent way of doing things, but this RFC seems to focus mainly on adding things to an empty shell. This makes complete sense when approaching it from the design of application command builders - it's extremely unlikely that anyone is trying to do anything dynamic and mutable with these at runtime. Application command builders were a truly stand-alone builder, designed for That doesn't ring true for embeds and components though - consider the use-case of disabling a button on click, or removing/splicing some fields from your embed. I don't see these use-cases being considered in an Component and embed builders were a hybrid model, designed to be a module of discord.js rather than stand-alone, but tried to meet some builders styles anyway. They were then patched, then extended and re-exported, and continue to be stuck in limbo (hence the need for this very RFC) because this separation was either based on incorrect designs, or simply should never have happened at all. You don't build components/embeds at the same time, for the same reason, or in the same way as you build application commands. Nobody (or at least, very few people) want to use components or embeds with the raw API, unlike the approach we recommend for deploy-once commands. The actual use-case needs to come back to the surface to inform these decisions based on what users actually need, not how we want it to look. |
What's the use case for calling As for
Once sapphiredev/shapeshift#125 is merged and released (🤞 this weekend), we can export methods from builders to disable just the validators we use (probably
Such use cases can be replicated with code like |
Addressing the recent comment:
In my opinion, addComponents should stay. Why? Because it's just representing an array of public addBooleanOption(
input: SlashCommandBooleanOption | ((builder: SlashCommandBooleanOption) => SlashCommandBooleanOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandBooleanOption);
}
public addUserOption(input: SlashCommandUserOption | ((builder: SlashCommandUserOption) => SlashCommandUserOption)) {
return this._sharedAddOptionMethod(input, SlashCommandUserOption);
}
public addChannelOption(
input: SlashCommandChannelOption | ((builder: SlashCommandChannelOption) => SlashCommandChannelOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandChannelOption);
} All of these methods have the exact same implementation and same amount of arguments, they only differ in name and type signatures. This begs the question, why not just make one generic unified method instead of duplicating code? Is this current approach more type safe? As far as I can tell no, is it easier to use? Well actually it seems harder to use because it works in less use cases: type Shape = Triangle | Square | Circle
class Box {
private shapes: Shape[];
public addTriangle(...) { ... }
public addSquare(...) { ... }
public addCircle(...) { ... }
} As I mentioned above, all of these methods would have the same exact implementations. The real issue for users comes in when they have shapes stored under the polymorphic function generateShapes(): Shape[] { ... }
const box = new Box();
const shapes = generateShapes();
shapes.map(shape => {
if (shape.type === 'circle') {
box.addCircle(shape);
} else if (shape.type === 'square') {
box.addSquare(shape);
} else {
box.addTriangle(shape);
}
}); Yikes, that's a lot of code for something that should relatively simple. To make matters worse I'll have to continue to update my own code when a new shape gets added because I have to check exhaustively. The core issue here is that the function generateShapes(): Shape[] { ... }
const box = new Box();
const shapes = generateShapes();
box.addShapes(shapes); As for concrete use cases, anywhere where So I end with my main question: "Why is this considered better?" |
I would like to also add that it being in a separate package allows developers to re-use the builders in other projects, that don't necessarily need discord.js (think of HTTP bot frameworks, or even other libraries). The validation is also pretty much a vital part of a HTTP bot's lifecycle, specially when they're used in the HTTP response and not from a subsequent edit, as unlike HTTP calls, we don't get the error back if we send an invalid payload as a reply. An error from builders is a lot more helpful and easier to catch than a silent one from a HTTP call. Furthermore, it also plays a vital role in developer experience in other places, such as testing if all commands can be registered just fine without doing a HTTP request or even connecting to Discord, which is something very needed for bots that accept translations made by community users. Is a script allowed? Is a specific character in a language supported by Discord's RegExp? Not needing to do a deploy to figure out is very helpful in that regard, and at least we can also know that the data is correctly formed with very simple logic, depending on the framework. I also agree with @suneettipirneni's points, that pattern is very similar to what I do with message components. |
While I understand your concerns @suneettipirneni, you need to consider the fact that, at least in the action rows cases, there is no benefit in having an Not to mention that from a users perspective, doing import { ActionRowBuilder } from 'module-name';
const row = new ActionRowBuilder().addButton(button => button.setCustomId().soOn()); versus import { ActionRowBuilder, ButtonBuilder } from 'module-name';
const row = new ActionRowBuilder().addComponents([
new ButtonBuilder().setCustomId().soOn(),
]); would make it more consistent, especially if they're also working with application commands (which they most likely are). Another thing to consider is that an action row will never be polymorphic like in your example. The components inside it will always (at this time) be of one type only, which means we can also make validation of attempts to add different types of components in row much simpler and cleaner (and possibly faster too). Also, maybe a personal hot take, but it's much better to be explicit about implementation than implicit / expected to work with new type. |
I feel like these points contradict each other and ignore a lot of what I said in the first comment. The attitude towards component builders continues to be focused on building them for the first time, with no consideration for editing.
It should not be necessary to clone 5 action rows / 25 buttons using Maybe you wouldn't set all 5 - this is a better argument for |
And that's correct? I mean I don't see the issue in that, it shouldn't be up to builders to aid in editing a components, that's up to you when using them. Of course, the alternative is calling |
the 2nd point is now resolved. ref: #8074 |
If that's a design decision to be made for component builders, then so be it, they can only build. |
Preface: this RFC (Request For Comments) is meant to gather feedback and opinions for the 3 issues that will be mentioned down below. I understand builders tend to be quite a sensitive topic with everyone disagreeing on how it should be done, so please keep the feedback useful. Thanks!
This RFC aims to solve (hopefully once and for all) the three pain points of builders as they currently are done. The end goal is to decide what the interface should look like for all current and possibly future builders! It is split into 3 separate parts, denoted by the headings.
Before you answer, please read through the entire RFC, and don’t jump to answering just one question only. You don’t have to answer question 3 if you have nothing to add to it, but the other two would be super appreciated to receive answers to. Thanks for reading this, and now let’s get started~!
1. The interface
One of the key issue with the current builders is the lack of a consistent interface between the Application Command builders, the Message Component builders and the Embed builder.
As it stands at the time of writing this RFC:
.add*
methods for each option type. The class’s fields represent the structure similar to how the api payload will look like.addComponents
takes in any component, whereasadd*
in application command builders add just that one kind of options), some options that take in arrays allow rest parameters too (something that in my opinion is fine except forset*
methods that should take in an array only). The class stores all data in adata
property, and accesses it for setting/reading the properties in itdata
field. They also allow input incamelCase
format, where no other builder allows (or should allow) that.Several things need to be decided:
toJSON()
calls? In adata
field or as the class fields themselves?addComponents
be removed in favor ofaddButton
,addSelectMenu
and so on? Or should application command builders useaddOption
instead?My opinion on those 3 questions are:
The class fields should represent as closely what the toJSON will return. CallingWell I changed my mind, we should instead use the{ ...builder }
should return the same valid data as callingbuilder.toJSON()
data
field, however no getters should be added pointing to said fieldadd*
per component type)2. The validation
Outside of application command builders, every other builder has 2 classes inside. An “Unsafe” class (which has 0 input validation) and a “Safe” class (which has validation for inputs (and quite fast validation at that)).
From my perspective, this split needs to be removed. The point of builders is to provide a consistent interface for creating api data. Creating two classes that achieve the same thing, even if one of them just extends the other, feels wrong and redundant. All builders should use validation by default¹. This would clean up a lot of the classes, and also remove the issue that other modules using builders need to export both safe and unsafe versions (the latter of which might scare users away).
¹: Validation should instead be able to be turned off at the validation library level (something that we have planned in shapeshift actually (work feedback that we agree would be a good addition)). That way, you can decide in your code if the validation should be turned on or off (for instance turn off validation automatically if in a production environment)
3. The pain points
This point is more less an open page for you to air out what frustrations you’ve encountered with the interface. For this, I’d prefer if you sticked to issues related more to consistency or type issues or performance worries (etc), and not the fact the interface has/will change again. The builders package is still not on version 1, meaning we’re still more-less in development to a degree.
Feel free to add anything that you think would improve the overall experience for everyone.
The text was updated successfully, but these errors were encountered: