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

Better URL handling in services #1308

Merged
merged 1 commit into from
Oct 1, 2018

Conversation

ob-stripe
Copy link
Contributor

Move URL creation in Service as much as possible, so that concrete services don't need to create their own URLs in every method.

Most services will only need to declare their ObjectName, then use the new parent methods for CRUD + list requests, or call this.ClassUrl() / this.InstanceUrl(id) as appropriate.

Some services will need to override ClassUrl if their object name doesn't cleanly map to the URL.

I'm not sure yet how to handle services for nested resources -- maybe another parent class ServiceNested with more helper methods, similar to these ones but with an additional argument for the parent resource's ID.

ptal @remi-stripe

@ob-stripe ob-stripe force-pushed the ob-service-url branch 2 times, most recently from 9bf0b5d to 20cae29 Compare September 29, 2018 16:40
Copy link
Contributor

@remi-stripe remi-stripe left a comment

Choose a reason for hiding this comment

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

Excited about those changes (and glad I did not convert all Urls during the previous PR). Left some minor comments.

I am not sure what nested resources would look like either but agree that a NestedService looks like the right approach

}

public virtual ApplePayDomain Get(string domainId, RequestOptions requestOptions = null)
{
return this.GetEntity($"{Urls.BaseUrl}/apple_pay/domains/{domainId}", requestOptions);
return this.GetEntityNew(domainId, null, requestOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need null on GET requests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The GetEntityNew method is capable of adding parameters to retrieve requests, even though we don't do this in the library (mostly because the only case where this is needed is expansion, and this is handled separately via the Expand* properties on the service).

That said, I added an override to be able to call the method without an options class.

Copy link
Contributor

Choose a reason for hiding this comment

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

But why since expansion is on the other options and no GET (non list) method takes an option?

}

public virtual ApplePayDomain Delete(string domainId, RequestOptions requestOptions = null)
{
return this.DeleteEntity($"{Urls.BaseUrl}/apple_pay/domains/{domainId}", requestOptions);
return this.DeleteEntityNew(domainId, null, requestOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm assuming there's no way to get rid of the null for most DELETE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this is possible too. I added an override to be able to call DeleteEntityNew without an options class.

{
baseUrl = baseUrl ?? StripeConfiguration.GetApiBase();

return $"{baseUrl}/apple_pay/domains";
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a fan of that override. I would rather calculate the relative path instead. Can we do "ObjectPath" or something instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that makes way more sense for a service actually. I changed ObjectName to BasePath.

}

public virtual Task<Authorization> ApproveAsync(string authorizationId, AuthorizationApproveOptions options = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken))
{
return this.PostAsync($"{classUrl}/{authorizationId}/approve", requestOptions, cancellationToken, options);
return this.PostRequestAsync<Authorization>($"{this.InstanceUrl(authorizationId)}/approve", options, requestOptions, cancellationToken);
Copy link
Contributor

@remi-stripe remi-stripe Sep 29, 2018

Choose a reason for hiding this comment

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

I wonder if it'd be cleaner to pass the authorization id + append name? Not sure what that would look like but I feel like it could be cleaner? Sorry it's a bit vague and likely not possible but I was thinking of this.Url(authorizationId, "/approve") or similar?

Copy link
Contributor Author

@ob-stripe ob-stripe Sep 29, 2018

Choose a reason for hiding this comment

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

Hm, not sure it's worth it. this.InstanceUrl(authorizationId, "/approve") would be the same thing as this.InstanceUrl(authorizationId) + "/approve", which is already the same thing as $"{this.InstanceUrl(authorizationId)}/approve".

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah fair, it's not the same approach in my head (and the former is easier to mess up). I ask because I made a mistake a few times yesterday with the /approve, /decline and all where stripe-mock still just worked because params were identical and I had messed up my URL calculation

}

public virtual Product Delete(string productId, RequestOptions requestOptions = null)
{
return this.DeleteEntity($"{Urls.BaseUrl}/products/{WebUtility.UrlEncode(productId)}", requestOptions);
return this.DeleteEntityNew(productId, requestOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

Flagging that I had to remove the encoding for now. I think it makes more sense to do it at the next level. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

InstanceUrl already takes care of encoding the ID! :)

@remi-stripe
Copy link
Contributor

@ob-stripe I did most of the services but left the nested ones for now. I think this one needs a thorough review because so much has changed and it's easy to mess up

@remi-stripe
Copy link
Contributor

I'm not sure why it fails on appveyor and works locally :(

Copy link
Contributor

@remi-stripe remi-stripe left a comment

Choose a reason for hiding this comment

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

Thanks for fixing the Issuing ones, I was sure I did them and forgot to double check

}

public virtual Task<Balance> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken))
{
return this.GetRequestAsync<Balance>($"{Urls.BaseUrl}/{this.BasePath}", null, requestOptions, cancellationToken);
return this.GetRequestAsync<Balance>(this.ClassUrl(), null, requestOptions, cancellationToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain why this works and mine did not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your path was /v1//balance, and stripe-mock doesn't like the double slash.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah I know but it worked locally without any issue which is weird

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, good point. The different path is probably being handled different in .NET Framework and .NET Core.

public virtual File Create(FileCreateOptions options, RequestOptions requestOptions = null)
{
return Mapper<File>.MapFromJson(
Requestor.PostFile(
$"{Urls.BaseFilesUrl}{classPath}",
this.ClassUrl(Urls.BaseFilesUrl),
Copy link
Contributor

Choose a reason for hiding this comment

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

ah neat I totally misread the code when I looked for whether this was supported

Copy link
Contributor

@remi-stripe remi-stripe left a comment

Choose a reason for hiding this comment

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

Some minor comments but I think it's ready otherwise

@@ -6,7 +6,7 @@ namespace Stripe
using System.Threading.Tasks;
using Stripe.Infrastructure;

public class CardService : Service<Card>,
public class CardService : ServiceNested<Card, Customer>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Likely not a problem but flagging since I thought about it. What if we have a generic nested resource in the future (like card and bank account used to be) and we want to have it shared? Would we simply add a parent class/interface or similar instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ServiceNested doesn't actually use the parent resource class for anything, I should probably remove it.

That said, if we wanted to have a single service be able to support different parent URLs, it probably would have to manage the URLs itself instead of delegating it to the Service / ServiceNested parent class.

return this.PostRequest<EntityReturned>(this.InstanceUrl(parentId, id), options, requestOptions);
}

protected Task<EntityReturned> UpdateNestedEntityAsync(string parentId, string id, BaseOptions options = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken))
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor but I wonder if we should avoid the default values on parameters like requestOptions and cancellationToken. Why you ask? Because it's easy to mess up your copy/paste (we both did in this PR) and have cancellationToken be the default value instead of what the upper level passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 Agreed, I also thought of this.

}

public virtual TransferReversal Get(string transferId, string reversalId, RequestOptions requestOptions = null)
{
return this.GetEntity($"{Urls.BaseUrl}/transfers/{transferId}/reversals/{reversalId}", requestOptions);
return this.GetNestedEntity(transferId, reversalId, requestOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

I see you used the version without null for options. but you did not use it everywhere. I know I kinda asked for it but per my previous comment I'm starting to think being explicit with what params we pass and why is likely safer/better after all (soooorry)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha, okay. I prefer the explicit versions as well.

@@ -16,24 +16,26 @@ public DiscountService(string apiKey)
{
}

public override string BasePath => null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh well I guess this is the example of a resource nested under different parents?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, exactly. It's not as pretty, but thankfully those cases should be pretty rare.

@ob-stripe ob-stripe changed the title [WIP] Better URL handling in services Better URL handling in services Sep 30, 2018
@ob-stripe
Copy link
Contributor Author

r? @remi-stripe
cc @stripe/api-libraries

@ob-stripe ob-stripe assigned remi-stripe and unassigned ob-stripe Sep 30, 2018
@remi-stripe remi-stripe assigned ob-stripe and unassigned remi-stripe Oct 1, 2018
@ob-stripe ob-stripe mentioned this pull request Oct 1, 2018
32 tasks
@ob-stripe ob-stripe assigned remi-stripe and unassigned ob-stripe Oct 1, 2018
@remi-stripe
Copy link
Contributor

LGTM assuming the tests pass (since we had a few failures on the branch and not locally on this work)

@ob-stripe ob-stripe merged commit af9e467 into integration-next-major-version Oct 1, 2018
@ob-stripe ob-stripe deleted the ob-service-url branch October 1, 2018 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants