-
Notifications
You must be signed in to change notification settings - Fork 119
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
API method signature refactor #78
Conversation
i'm really annoyed to see pat's excellent commits from #77 polluting the diff here. my expectation was that merging that PR and then merging |
@jgravois if you close and re-open this PR it should display fine. GitHub displays stuff like this kinda weird master...r/options-object. |
This LGTM - I like your rule of thumb. I don't see the "Not so good" being a big deal. In your example, isn't just the case that |
I should say the concept LGTM - I haven't reviewed the code yet, which I'm sure is top notch. |
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.
@jgravois this looks good and I think the new standards for the API make sense. I would document this in the wiki somewhere though we need an "API Design" topic for similar.
I also think we should decide what we call the second (or first) option. I see param
, options
, requestOptions
and whateverOptions
used interchangeable so we should probably sync this up at some point.
/** | ||
* Used internally by packages for requests that require application authentication. | ||
*/ | ||
export interface IApplicationRequestOptions extends IRequestOptions { |
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.
There are no methods in the API that only accept an application login. We can probably safely remove this.
@@ -36,7 +36,9 @@ export function fetchToken( | |||
url: string, | |||
params: IFetchTokenParams |
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.
@jgravois I think it might be a little confusing to call this params
and also have params
in all the options objects. Maybe the signature should be (url: string, options: IFetchTokenParams)
@jgravois What if we were to use method overloads to deal with that case? Something like:
This would also remove the not so good case because you can just type the requestOptions.params to be
Some caveats:
Overall, I like the pattern you've set up, I just thought I would put this out as a possibility. |
all excellent suggestions. i'll get to work on implementing them, tidy things up and hope @dbouwman doesn't knock let the air out of my balloon when he gets back from 🏄 |
@noahmulfinger I was thinking about a similar idea, though using union types instead of overloads b/c I thought that overloads are only needed if the type of the function's function geocode(address: string|IGeocodeRequestOptions): Promise<IGeocodeResponse> {
if (typeof address === "string" {
// do singleline geocode
} else if (typeof address === "object") {
// pull address params from requestOptions.params
}
} I'm not totally sure that idea checks out tho. |
@jgravois @noahmulfinger @tomwayson we might be over complicating Johns example here so how about this new rule:
This means that stuff like this is out: geocode({
address: "380 New York St",
postal: 92373
},
{
params: {
outSR: 3857
}
}) and you can geocode like what @tomwayson suggested and do this: interface IGeocodeRequestOptions extends IRequestOptions {
address?: string;
address2?: string;
address3?: string;
neighborhood?: string;
city?: string;
subregion?: string;
/**
* The World Geocoding Service expects US states to be passed in as a 'region'.
*/
region?: string;
postal?: number;
postalExt?: number;
countryCode?: string;
outSr: number;
}
function geocode(address: string|IGeocodeRequestOptions): Promise<IGeocodeResponse> {
if (typeof address === "string" {
// do singleline geocode
} else if (typeof address === "object") {
// pull address params from address
}
} which then means I can do this: geocode('221B Baker St, London');
geocode('221B Baker St, London', {
outSr: 102100
});
geocode({
address: "221B Baker St",
city: "London",
outSr: 102100
}); |
@patrickarlt That rule makes sense to me. The only complication is that, to allow the function calls you listed, the interface and function have to look like the following:
Which would allow the user to pass in two options objects. |
@noahmulfinger not at all. Any param can be encoded inside So the full interface for interface IGeocodeRequestOptions extends IRequestOptions {
address?: string;
address2?: string;
address3?: string;
neighborhood?: string;
city?: string;
subregion?: string;
region?: string;
postal?: number;
postalExt?: number;
countryCode?: string;
outSr: number;
authentication: ...; // from IRequestOptions
httpMethod: ...; // from IRequestOptions
params: ...; // from IRequestOptions any additional params we didn't specify in `IGeocodeRequestOptions` like category, extent ect....
fetch: ...; // from IRequestOptions
}
function geocode(address: string|IGeocodeRequestOptions, options?: IGeocodeRequestOptions): Promise<IGeocodeResponse> {
if (typeof address === "string" {
// do singleline geocode, merge applicable extra params from the second optional `options` param and `options.params`
} else if (typeof address === "object") {
// pull address params from options, merge with additional params from options.params
}
} |
@patrickarlt Ah okay, I misunderstood how that object worked. Also, to allow this function call:
We would need the function definition to look like this:
That seems reasonable. We would just ignore the second param if the first param is an object. |
@noahmulfinger Yup updating my example to make that clearer. |
I don't know the second function geocode(address: string|IGeocodeRequestOptions, options?: IGeocodeRequestOptions): Promise<IGeocodeResponse> Feels a little weird to me and not worth it just to support: geocode('221B Baker St, London', {
outSr: 102100
}); I mean, that's a nice signature, and I'll admit it's better than: geocode({
address: "221B Baker St",
city: "London",
outSr: 102100
}); But personally, I don't think it's worth the head scratching that people will do when they see something like this in the docs: function geocode(address: string|IGeocodeRequestOptions, options?: IGeocodeRequestOptions): Promise<IGeocodeResponse> We'll have to explain that the second request options will be ignored if you pass 2 of them - for many functions. Part of what's confusing to me is that in this particular example, the string argument is a full address, but no such param exists on the So, if users could do this: geocode({
fullAddress: "221B Baker St, London",
outSr: 102100
}); Then I'd say we should abandon the second argument entirely and just go w/: function geocode(address: string|IGeocodeRequestOptions): Promise<IGeocodeResponse> All that said, if everyone likes the second options arg, I'm game. I don't have strong feelings about this, just a feeling. I'm going to noodle around w/ some feature service functions and see how they feel. |
update: test coverage is 100% (again), TypeDoc is in sync and i'm more or less happy with the overall refactor. i'm with @tomwayson regarding the optional second to do:
|
@tomwayson @jgravois I'm fine with: function geocode(address: string|IGeocodeRequestOptions): Promise<IGeocodeResponse>` Lets get this merged. |
@patrickarlt agreed. once @tomwayson looks over this w/ @dbouwman eyes, i'd be ready to publish |
new API design
IParams
is moved insiderequestOptions
requestOptions
to include additional argumentscriticism welcome, but it felt clunky to me to have all higher level wrapper methods require the same options object to be passed because some of them only really need one simple input.
because of this, i came up with yet another rule of thumb proposal for this library.
a. if a method can be executed successfully anonymously
b. and it only usually requires one piece of information from a developer
one custom parameter will be mandatory and followed by an optional
requestOptions
object.if, for any reason,
requestOptions
is mandatory, everything will be stuffed into a single object.i like this for several reasons.
the API design is still generally consistent.
it makes for more concise methods with less property names to remember.
we no longer need to re-order arguments depending on whether or not requestOptions is required.
we avoid dead slots entirely
the not so good
there is one thing (already) that i'm not incredibly fond of.
putting
params
insiderequestOptions
is definitely a convenient catch-all, but it makes for an ugly signature when the wrapper author (in this case me) has only stubbed out some of the options the endpoint supports.caveats
i have not updated the TypeDoc yet and the code is still rough in several places, but i decided it would be prudent to save polishing until after we've had another round of discussion.
whatever becomes of this, we should land #77 first. i'm happy to rebase on top of it if we end up green lighting this work.