-
Notifications
You must be signed in to change notification settings - Fork 570
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
Conversation
9bf0b5d
to
20cae29
Compare
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"
.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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! :)
@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 |
I'm not sure why it fails on appveyor and works locally :( |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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
1a563f3
to
3c8d34a
Compare
There was a problem hiding this 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>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
r? @remi-stripe |
c082108
to
5df993a
Compare
5df993a
to
fa31092
Compare
LGTM assuming the tests pass (since we had a few failures on the branch and not locally on this work) |
fa31092
to
2df527c
Compare
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 callthis.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