Open dscottboggs opened 1 year ago
What do you prefer to do do with URLs that accept multiple optional parameters (like /api/v1/timelines/public), just add all of them with an Option
type, or maybe some kind of a builder pattern?
The previous authors seemed to be fans of using the builder pattern to generate structures which use serde of serialization into a urlencoded string, and I think it fits nicely. There are a couple changes I'm making to the way they did that as a part of v2:
derive_builder
crate#[derive(Builder, Default, Serialize, Deserialize, Clone)]
pub struct TimelineQuery {
#[builder(default)]
pub local: bool,
#[builder(default)]
pub remote: bool,
#[builder(default)]
pub only_media: bool,
#[builder(setter(into, strip_option))]
pub max_id: Option<String>,
#[builder(setter(into, strip_option))]
pub since_id: Option<String>,
#[builder(setter(into, strip_option))]
pub min_id: Option<String>,
#[builder(setter(into, strip_option))]
pub limit: u8,
}
rather than
#[derive(Builder, Default, Serialize, Deserialize, Clone)]
pub struct TimelineQuery {
#[builder(setter(strip_option))]
pub local: Option<bool>,
#[builder(setter(strip_option))]
pub remote: Option<bool>,
#[builder(setter(strip_option))]
pub only_media: Option<bool>,
#[builder(setter(into, strip_option))]
pub max_id: Option<String>,
#[builder(setter(into, strip_option))]
pub since_id: Option<String>,
#[builder(setter(into, strip_option))]
pub min_id: Option<String>,
#[builder(setter(into, strip_option))]
pub limit: Option<u8>,
}
The idea is basically that unnecessary options are un-ergonomic, so anywhere where a default is specified and unlikely to change, make it the default here.
Another thought I have on this is that perhaps it would be a good idea to take a look at this section of the paged_routes!
macro definition and its corresponding invokation to see if we can draw inspiration for making the way we do this more DRY.
Any thoughts?
(1) Sure.
(2) If there's an obvious default then yeah, no need for an Option
. The only disadvantage I see that there's no way to leave it unset, but since the default for booleans are given in the API description (and Eugen has gone on record that existing API won't be messed with), then the logic could be "if value=default then don't set anything", if that ever matters.
(3) That also makes sense, I don't think types for API queries could possibly have any invariants that need to be protected.
Any way I can help out here? Is the comb
branch where all of this work exists currently?
Would love to have this; the admin API will be critical for building automod tools for Mastodon, and I ended up implementing most of it in https://github.com/VyrCossont/mastodon-async/tree/admin-api before noticing this issue.
Hello, yes, I am coming back to this soon but a couple of more urgent projects have come up, I should be wrapping them up soon (at least the urgent part). I really appreciate your work on the admin API, however, as you noted there may be some things which are already done. By all means, create a pull request (targeting the comb
branch) and let's take a look at what needs done to get that merged.
Right on. I'll take a look at the comb
branch and see if anything from my admin-api
branch is reusable or if there's other work I can easily pick up.
Also, my take on builders and defaults is that, while unnecessary options are un-ergonomic:
#[builder(setter(strip_option))]
makes this a non-issue from the builder's caller's perspective.I tried implementing a builder with some mandatory and some optional parameters using derive_builder
and serde_with
:
use derive_builder::Builder;
use mastodon_async_entities::{report::Category, AccountId, RuleId, StatusId};
use serde_with::skip_serializing_none;
/// Form used to create a report
///
/// // Example
///
/// ```
/// use mastodon_async::{entities::{AccountId, report::Category}, requests::AddReportRequest};
/// let request = AddReportRequest::builder(AccountId::new("666")).category(Category::Spam).build();
/// ```
#[skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Builder)]
#[builder(
custom_constructor,
build_fn(private, name = "try_build"),
setter(into, strip_option)
)]
pub struct AddReportRequest {
/// The account being reported.
#[builder(private)]
pub account_id: AccountId,
/// Attach statuses to the report to provide additional context.
#[builder(default)]
pub status_ids: Option<Vec<StatusId>>,
/// The reason for the report.
#[builder(default)]
pub comment: Option<String>,
/// If the account is remote, should the report be forwarded to the remote admin?
#[builder(default)]
pub forward: Option<bool>,
/// Machine-readable category of the report.
#[builder(default)]
pub category: Option<Category>,
/// Rules broken by a [`Category::Violation`] report.
#[builder(default)]
pub rule_ids: Option<Vec<RuleId>>,
}
impl AddReportRequest {
/// Start building a form for creating a report.
pub fn builder(account_id: AccountId) -> AddReportRequestBuilder {
let mut builder = AddReportRequestBuilder::create_empty();
builder.account_id(account_id);
builder
}
}
impl AddReportRequestBuilder {
/// Build the form for creating a report.
pub fn build(&self) -> AddReportRequest {
self.try_build()
.expect("One or more required fields are missing!")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_serialize_request() {
let request = AddReportRequest::builder(AccountId::new("666"))
.category(Category::Spam)
.build();
let ser = serde_json::to_string(&request).expect("Couldn't serialize");
assert_eq!(ser, r#"{"account_id":"666","category":"spam"}"#);
}
}
This gives the caller two options, both safe: they can create an AddReportRequest
directly and the type system forces them to provide mandatory params (as well as None
or Default::default()
for each optional param), or they can call AddReportRequest::builder(account_id)
, which takes the mandatory params, call builder methods for any optional params, and then .build()
.
The boilerplate cost of this is a #[builder(default)]
annotation on every optional param and a #[builder(private)]
annotation on every mandatory param (assuming we want to hide methods that are redundant with the ::builder(…)
method that takes mandatory params), plus the struct-level #[skip_serializing_none]
and #[builder(…)]
annotations and the ::builder(…)
and .build()
methods. We can probably improve on this, but it doesn't seem horrible as is, especially compared to how many lines we can drop that are redundant handwritten builder methods and tests for errors that won't be able to happen any more.
Looks good to me.
@VyrCossont you may want to review the changes in this commit https://github.com/dscottboggs/mastodon-async/pull/91/commits/035637f21f714e4cd84027c05b4c2eedb94cfaef
Filled in the reporting and admin APIs here: https://github.com/VyrCossont/mastodon-async/tree/comb
The above builder pattern is pretty much fine, I think. I'm going to convert the older builders to that style where it makes sense.
This is great! Would you mind creating a PR for this?
Sure, I'll open a draft and let you know once it's ready for review.
Draft in #93. I'm concurrently working on an automod bot that uses mastodon-async
, so the admin APIs I've implemented should get a workout, as well as the status creation API.
It has come to my attention that the Mastodon API has been updated a lot since the original work this was based on, and we need to comb through the API documentation ensuring that each type is implemented according to the spec.
When this is released, we will cut a new v2.0 release. Breaking changes will be a part of this.
Tasks for this PR
Comb through entities
IdentityProofalready deprecated upstream. no sense in adding an entity just to deprecate it.Entities, second pass
IsVariant
cargo doc
(#83)Comb through methods
Finishing up/other tasks
mastodon_async::requests
module is migrated entirely intomastodon_async_entities::forms