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

Try out an operation macro #878

Merged
merged 10 commits into from
Jul 4, 2022
Merged

Try out an operation macro #878

merged 10 commits into from
Jul 4, 2022

Conversation

rylev
Copy link
Contributor

@rylev rylev commented Jun 30, 2022

This introduces an operation! macro which reduces the amount of boiler plate around declaring operations considerably.

This is how it looks when called:

operation! {
    CreateCollection,
    client: DatabaseClient,
    collection_name: String,
    partition_key: PartitionKey,
    ?consistency_level: ConsistencyLevel,
    ?indexing_policy: IndexingPolicy,
    ?offer: Offer
}

And it expands to:

#[derive(Debug, Clone)]
pub struct CreateCollectionBuilder {
    client: DatabaseClient,
    collection_name: String,
    partition_key: PartitionKey,
    consistency_level: Option<ConsistencyLevel>,
    indexing_policy: Option<IndexingPolicy>,
    offer: Option<Offer>,
    context: Context,
}

impl CreateCollectionBuilder {
    pub(crate) fn new(
        client: DatabaseClient,
        collection_name: String,
        partition_key: PartitionKey,
    ) -> Self {
        Self {
            client,
            collection_name,
            partition_key,
            consistency_level: None,
            indexing_policy: None,
            offer: None,
            context: Context::new(),
        }
    }

    setters! {
        consistency_level: ConsistencyLevel => Some(consistency_level),
        indexing_policy: IndexingPolicy => Some(indexing_policy),
        offer: Offer => Some(offer),
        context: Context => context,
    }
}

#[cfg(feature = "into_future")]
impl std::future::IntoFuture for CreateCollectionBuilder {
    type IntoFuture = CreateCollection;
    type Output = <CreateCollection as std::future::Future>::Output;
    fn into_future(self) -> Self::IntoFuture {
        Self::into_future(self)
    }
}

/// The future returned by calling `into_future` on the builder.
pub type CreateCollection =
    futures::future::BoxFuture<'static, azure_core::Result<CreateCollectionResponse>>;

This would ensure all of our operation builders follow the same pattern.

Some current limitations:

  • Does not work for pageable operations
  • Does not work for operations that require a generic param

I believe both of these limitations are pretty easy to overcome, but I wanted to see what folks thought.

Thoughts?

Copy link
Contributor

@bmc-msft bmc-msft left a comment

Choose a reason for hiding this comment

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

I agree that Pageable would be trivial to implement, given it boils down to not creating the BoxFuture type and not creating the IntoFuture impl.

My only feedback would be bike shedding over the use of ? as an indicator of Option.

The only other case I am aware of that this does not currently handle well is Azure Table's transaction builder, where rather than exposing one of the values via a setter, there are methods that append to one of the values.

For reference:

impl TransactionBuilder {
pub(crate) fn new(partition_key_client: PartitionKeyClient) -> Self {
Self {
partition_key_client,
transaction: Transaction::new(),
timeout: None,
context: Context::new(),
}
}
setters! {
timeout: Timeout => Some(timeout),
context: Context => context,
}
/// Insert a new entity into a table
///
/// Ref: https://docs.microsoft.com/en-us/rest/api/storageservices/insert-entity
pub fn insert<E: Serialize>(mut self, entity: E) -> azure_core::Result<Self> {
let body = serde_json::to_string(&entity)?;
let mut url = self.partition_key_client.table_client().url().to_owned();
url.path_segments_mut()
.map_err(|()| Error::message(ErrorKind::Other, "invalid table URL"))?
.pop()
.push(self.partition_key_client.table_client().table_name());
let mut request = Request::new(url, Method::Post);
request.insert_header(ACCEPT, "application/json;odata=fullmetadata");
request.insert_header(CONTENT_TYPE, "application/json");
request.set_body(body);
self.transaction.add(TransactionOperation::new(request));
Ok(self)
}

@rylev
Copy link
Contributor Author

rylev commented Jun 30, 2022

@bmc-msft any thoughts on what we should use for option instead of the ??

@bmc-msft
Copy link
Contributor

@bmc-msft any thoughts on what we should use for option instead of the ??

Not at this time. It's livable and given it's only for internal use, we can move to it and adjust if we need to.

It would be good to document the macro with the example you provided.

@rylev rylev force-pushed the operation-macro branch from 665fd36 to eafdc19 Compare July 1, 2022 10:57
@rylev
Copy link
Contributor Author

rylev commented Jul 1, 2022

Ok I'm happy where this ended up. There are about 1000 fewer lines of operations code in Cosmos, and this will only grow as we add more operations from other SDKs.

The following is the syntax we currently have. In the interest of keeping the macro definition simple I'd like to avoid getting too fancy, but we could certainly improve some things. I'll make note of some ideas. Also, please note that the usage below is the most gnarly it can possibly be (i.e., it uses all the features of the macro for instruction purposes). No actual usage is this complex.

operation! {
    #[stream] // This instructs the macro to avoid generating the `IntoFuture` definition and instead assume it's a stream
    ListAttachments<T: Serialize + Send>,
    client: DocumentClient, // Which client is used always comes next
    document: T,  // Then a list of all the required options. We could require the user to annotate these with `#[required]`
    ?consistency_level: ConsistencyLevel // Then all the optional options marked by `?`. If we require the use of `#[required]` then we could drop the `?` here.
    #[skip] // This instructs the follow field to not generate a setter automatically. This is useful when you want a more complex setter 
    partition_key_serialized: String
}

Thoughts?

@rylev rylev requested a review from bmc-msft July 1, 2022 12:49
@@ -13,6 +13,12 @@ pub enum IndexingDirective {
Exclude,
}

impl Default for IndexingDirective {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using derive_default_enum with a bumped MSRV? Given the soon-to-be-stabilized IntoFuture, we're likely to use this in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can switch to that when we bump the MSRV for IntoFuture.

@rylev rylev merged commit a549851 into Azure:main Jul 4, 2022
@rylev rylev deleted the operation-macro branch July 4, 2022 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants