Skip to content

Conversation

oguzkocer
Copy link
Contributor

@oguzkocer oguzkocer commented Jun 19, 2024

Implements endpoint type generation for #[derive(WpDerivedRequest)]. This will replace the FooEndpoint types, such as the UsersEndpoint one here.

I've added UsersRequest type that uses this derive macro, but I've done it inside a temporary generated mod so that it doesn't interfere with the current implementation. This also allows easily expanding the derived macro for inspection. I've left instructions on how to do that as a line comment.

This endpoint type works almost the same way as the current hand written one does with the following changes:

  1. Contextual endpoints, such as list users, will have the context name in the function. So, list(context: WpContext) becomes list_with_edit_context, list_with_embed_context & list_with_view_context. This is how the current request builders work as we have to be able to return a type specific to the given context. These functions will not be called directly - except by unit tests - so it doesn't matter too much which setup we select. Since this is simpler to implement/maintain and also consistent with the request builder functions, I think it's the clear winner.
  2. There are now filter endpoint functions for all endpoint types. We only had filter_list & filter_retrieve before, but now that it's being generated, it makes sense to generate all of them.
  3. _fields query pair is now added inline, instead of calling the append_filter_fields from UrlExtension as there is no reason to have an external function for this anymore.
  4. Instead of ApiBaseUrl.by_extending, we now use ApiBaseUrl.by_extending_and_splitting_by_forward_slash. If we have the "foo/bar" as a segment, we used to add it directly which meant the / character would be encoded - which in my opinion is the correct behaviour. However, /plugins endpoint requires the / character to be preserved, so we had a custom logic for that just for plugins. Now that logic is being used for all endpoints - which at the moment is only users & plugins. I don't like this setup at all, but at the same time, this is a minor issue and I don't want to shift my focus away from the derive macro to address this. So, I think this is good enough for now.

The code generation is split into many small parts and those small parts are unit tested. There are some intermediate steps, such as generating a function signature that are also unit tested. However, anything bigger is not unit tested. Those bigger parts only combine tested smaller units, so it should be a safe setup. We'll also still have unit tests like the ones in users_endpoint::tests module. We also cover the parser with integration tests and usage of generated code with integration tests, so I am pretty happy with the test coverage.


Here is an example usage and its generated code:

#[derive(wp_derive_request_builder::WpDerivedRequest)]
#[SparseField(SparseUserField)]
enum UsersRequest {
    #[contextual_get(url = "/users", params = &UserListParams, output = Vec<crate::SparseUser>)]
    List,
    #[post(url = "/users", params = &crate::UserCreateParams, output = UserWithEditContext)]
    Create,
    #[delete(url = "/users/<user_id>", params = &UserDeleteParams, output = crate::UserDeleteResponse)]
    Delete,
    #[delete(url = "/users/me", params = &UserDeleteParams, output = crate::UserDeleteResponse)]
    DeleteMe,
    #[contextual_get(url = "/users/<user_id>", output = crate::SparseUser)]
    Retrieve,
    #[contextual_get(url = "/users/me", output = crate::SparseUser)]
    RetrieveMe,
    #[post(url = "/users/<user_id>", params = &crate::UserUpdateParams, output = UserWithEditContext)]
    Update,
    #[post(url = "/users/me", params = &crate::UserUpdateParams, output = UserWithEditContext)]
    UpdateMe,
}
#[derive(Debug)]
pub struct UsersRequestEndpoint {
    api_base_url: std::sync::Arc<crate::request::endpoint::ApiBaseUrl>,
}
impl UsersRequestEndpoint {
    pub fn new(
        api_base_url: std::sync::Arc<crate::request::endpoint::ApiBaseUrl>,
    ) -> Self {
        Self { api_base_url }
    }
    pub fn list_with_edit_context(
        &self,
        params: &UserListParams,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Edit.as_str());
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        url.into()
    }
    pub fn list_with_embed_context(
        &self,
        params: &UserListParams,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Embed.as_str());
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        url.into()
    }
    pub fn list_with_view_context(
        &self,
        params: &UserListParams,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::View.as_str());
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        url.into()
    }
    pub fn filter_list(
        &self,
        context: WpContext,
        params: &UserListParams,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        url.query_pairs_mut().append_pair("context", context.as_str());
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn create(&self) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        url.into()
    }
    pub fn filter_create(
        &self,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users"]);
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn delete(
        &self,
        user_id: UserId,
        params: &UserDeleteParams,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        url.into()
    }
    pub fn filter_delete(
        &self,
        user_id: UserId,
        params: &UserDeleteParams,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn delete_me(
        &self,
        params: &UserDeleteParams,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        url.into()
    }
    pub fn filter_delete_me(
        &self,
        params: &UserDeleteParams,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut().extend_pairs(params.query_pairs());
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn retrieve_with_edit_context(
        &self,
        user_id: UserId,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Edit.as_str());
        url.into()
    }
    pub fn retrieve_with_embed_context(
        &self,
        user_id: UserId,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Embed.as_str());
        url.into()
    }
    pub fn retrieve_with_view_context(
        &self,
        user_id: UserId,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::View.as_str());
        url.into()
    }
    pub fn filter_retrieve(
        &self,
        user_id: UserId,
        context: WpContext,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.query_pairs_mut().append_pair("context", context.as_str());
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn retrieve_me_with_edit_context(
        &self,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Edit.as_str());
        url.into()
    }
    pub fn retrieve_me_with_embed_context(
        &self,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::Embed.as_str());
        url.into()
    }
    pub fn retrieve_me_with_view_context(
        &self,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut()
            .append_pair("context", crate::WpContext::View.as_str());
        url.into()
    }
    pub fn filter_retrieve_me(
        &self,
        context: WpContext,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.query_pairs_mut().append_pair("context", context.as_str());
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn update(
        &self,
        user_id: UserId,
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        url.into()
    }
    pub fn filter_update(
        &self,
        user_id: UserId,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash([
                "users",
                &user_id.to_string(),
            ]);
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
    pub fn update_me(&self) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        url.into()
    }
    pub fn filter_update_me(
        &self,
        fields: &[SparseUserField],
    ) -> crate::request::endpoint::ApiEndpointUrl {
        let mut url = self
            .api_base_url
            .by_extending_and_splitting_by_forward_slash(["users", "me"]);
        use crate::SparseField;
        url.query_pairs_mut()
            .append_pair(
                "_fields",
                fields
                    .iter()
                    .map(|f| f.as_str())
                    .collect::<Vec<&str>>()
                    .join(",")
                    .as_str(),
            );
        url.into()
    }
}

@oguzkocer oguzkocer added the Rust label Jun 19, 2024
@oguzkocer oguzkocer added this to the 0.1 milestone Jun 19, 2024
@oguzkocer oguzkocer marked this pull request as ready for review June 19, 2024 21:19
@oguzkocer oguzkocer requested a review from jkmassel June 19, 2024 21:19
Copy link
Contributor

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comprehensive test coverage made this pretty easy to review!

I left one comment for something we could test directly instead of indirectly, but it's not a priority

.expect("ApiBaseUrl is already parsed, so this can't result in an error")
}

pub fn by_extending_and_splitting_by_forward_slash<I>(&self, segments: I) -> Url
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method would probably benefit from some of its own testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's address that at a later time. I am not sure if this function will remain or not.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants