Guide: Anatomy of an API Route #401
bitfl0wer
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Want to contribute to Chorus by implementing a missing API route, but don't know where and how to start? This forum post should cover the basics about what to write and where. I will be using the "Modify Guild" route as an example.
File Structure
When cloning chorus and looking inside the repository folder, you should see something like this:
Here's a breakdown of all the folders and files we'll be touching in this example:
/src/api/
/src/api/guilds/guilds.rs
/src/types/entities/
Guild
, aChannel
or aUser
look like./src/types/schema/
/src/instance.rs
Instance
andUserMeta
structs, which represent the Instance and the user whose account is being puppeteered by this library respectively./src/ratelimiter.rs
The signature of our new function
Since the new function is supposed to modify a Guild, we will be placing it inside
/src/api/guilds/guilds.rs
'simpl Guild {...}
block. The function signature will look like this:Let's break this down bit by bit.
The function is supposed to be part of the public chorus API, therefore requiring the visibility modifier
pub
. Network requests are sent to the server and awaited in an asynchronous manner, calling for theasync fn
keywords.I personally like using Discord Userdoccers to look at Discord API Documentation, so let's take a look at what it has got to say about the Guild::modify route:
From looking at this, we can already make out the first two function arguments, our new function will require:
user: &mut UserMeta
-part in the function signature.{guild.id}
is the ID of the Guild we want to modify. In the function signature, this is ourguild_id: Snowflake
(since Discord IDs are alwaysSnowflake
s.)If we scroll a bit further down the page, we'll see this:
This is the JSON body we will be sending to the server. We can see that it requires a lot of fields, which is why in the next step, we will be creating a new schema for this route. The schema will be placed inside
/src/types/schema/guilds.rs
and will look like this:A hint on optional parameters
Hint: The documentation page sometimes (in this case, always) specifies a "?" after a field name. This means that the field is optional and can be omitted. This is why all fields in the schema are wrapped in
Option<>
s.The return type
Now that we have covered all function arguments, let's look at the function return type!
The Discord Userdoccers state:
In Chorus/the Discord API, we generally only encounter two different return types:
ChorusResult<..., E>
ChorusResult<()>
: This is the return type for all routes that don't return anything. The()
is the Rust equivalent ofvoid
in C++. TheChorusResult
is a type alias forResult<(), ChorusError>
, which is aResult
that either returnsOk(())
(so - nothing) orErr(ChorusError)
, in case something went wrong.ChorusResult<T>
, where T is an entity: This is the return type for all routes that return something.ChorusResult<...>
means the same thing as it does in the previous example, but instead of()
we have a generic typeT
, which is an entity. In this case, the function will returnOk(T)
if everything went well, orErr(ChorusError)
if something went wrong.In the case of our
modify
function, we will be returningChorusResult<Guild>
, since we expect the server to return the updatedGuild
object.And there we have it! The function signature is complete. Now, let's take a look at the function body.
The function body
Let's take a look at, and then dissect the function body:
ChorusRequest
The first thing we do is create a new
ChorusRequest
. TheChorusRequest
is a struct that contains all the information needed to send a request to the server. It contains the request itself, as well as theLimitType
of the request. TheLimitType
is an enum that describes the type of request we are sending and is important to tell Chorus' ratelimiter what sort of Request it is dealing with. In this case, we are sending a request to modify a Guild, so we will be usingLimitType::Guild(guild_id)
. Theguild_id
is the ID of the Guild we are modifying.The
ChorusRequest
takes two arguments:reqwest::RequestBuilder
.LimitType
of the request.Making the RequestBuilder
If
reqwest::Client
is imported via ause
statement, creating a new RequestBuilder is as easy as callingClient::new().<method>(<url>)
. In this case, we are using thepatch
method, since we are sending a PATCH request to the server (Discord Userdoccers tell us this information). The URL is the API URL of the Instance we are sending the request to, plus the endpoint we are sending the request to. In this case, we are sending a PATCH request tohttps://api.polyphony.rocks/guilds/<guild_id>
, so we are usingformat!("{}/guilds/{}", user.belongs_to.borrow().urls.api, guild_id)
to create the URL.Hint: I recommend reading the reqwest::Client documentation to learn more about the different methods and how to use them.
Adding the Authorization Header
The next thing we do is add the Authorization header to the request. This is done by calling
.header("Authorization", user.token())
on the RequestBuilder. Theuser.token()
method returns the token of the user, which is used to authenticate the request. Discord and Spacebar need this token to know which user is sending the request.Adding the JSON body
Let's add the JSON body to the request, so that the Server can actually know what our user wants to modify about the guild. This is done by calling
.body(serde_json::to_string(&schema).unwrap())
on the RequestBuilder. Theto_string(&schema)
method serializes the schema into a JSON string, which is then added to the request. The.unwrap()
is needed to unwrap theResult
returned byto_string(&schema)
, since thebody
method only acceptsString
s, notResult<String, serde_json::Error>
s. Note that callingserde_json::to_string()
is only possible, if you annotated the schema (in this case:GuildModifySchema
)with#[derive(Serialize)]
.Adding the LimitType
The last thing we do is add the
LimitType
to theChorusRequest
. Just typeLimitType::
into your IDE and let the autocomplete suggestions handle the rest. Note, that you will have to add theguild_id
as an argument to theLimitType::Guild
variant.Great! We now have the
ChorusRequest
and can send it to the server. But how do we do that?Sending the request and handling the response
We have previously talked about the two different return types of Chorus/Discord API endpoints. Luckily for you, Chorus has functions that handles both of them for you:
ChorusRequest::deserialize_response::<T>(user: &mut UserMeta)
, if you expect a response of type T, andChorusRequest::handle_request_as_result(user: &mut UserMeta)
if you expect no meaningful answer from the server.In this case, we will be using the
deserialize_response
function, since we expect the server to return the updatedGuild
object. Thedeserialize_response
function takes one argument: A mutable reference to theUserMeta
struct. This is needed for all the fancy logic which is involved in sending requests to the server. Thedeserialize_response
function needs to know the type of the response, so it can deserialize the response into the correct type. In this case, we are expecting aGuild
object, so we will be usingdeserialize_response::<Guild>(user)
.Hint: Don't forget to .await the function call! :P
The
deserialize_response
function returns aChorusResult<Guild>
, which is the same asResult<Guild, ChorusError>
. This means that we can use the?
operator to return theGuild
object if everything went well, or return theChorusError
if something went wrong. This is done by just adding a?
after the function call.The last thing we do is wrap the
Guild
object in anOk()
and return it. This is done by callingOk(response)
.Yay
Congratulations! You have successfully implemented a new API route! You should now take a look at how to test it, and then submit a pull request to the Chorus repository. If you have any questions, feel free to ask them in the #chorus-discussion channel on the Polyphony Discord server. We will be happy to help you out. :)
Beta Was this translation helpful? Give feedback.
All reactions