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

support for specifying version numbers in API endpoints #1115

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

davepacheco
Copy link
Collaborator

@davepacheco davepacheco commented Sep 24, 2024

Supersedes #862 (and is based on it). Fixes #869.

Also based on #1127. This includes a test that uses that option to make sure that operations have the same names in different versions, even when they have different handler function names.

@davepacheco davepacheco changed the base branch from main to dap/operation-id October 2, 2024 23:44
@davepacheco
Copy link
Collaborator Author

I think this one is nearly ready to go (but after #1127). The one change I'm leaning towards is that I think versions = A..B ought to not include B, for consistency with Rust's range syntax. I think people will be confused to discover that it actually means the thing that Rust denotes with A..=B.

Base automatically changed from dap/operation-id to main October 7, 2024 16:56
@davepacheco davepacheco marked this pull request as ready for review October 7, 2024 21:42
@davepacheco
Copy link
Collaborator Author

@jgallagher and @leftwo raised the excellent point that earlier versions of this PR would cause a Dropshot server to happily serve requests for API versions newer than the server was built with. For now, we can make this the responsibility of the DynamicVersionPolicy impl. Dropshot ships with only one impl, ClientSpecifiesVersionInHeader. I've changed this one to require a max version and reject requests newer than that.

Stepping back: my goal for this work is to provide a minimal base for us to go prototype versioning in some real consumers. I think it's pretty likely that as we do that, we will find changes we want to make to this. I wouldn't be surprised if we wind up with some common impls of DynamicVersionPolicy that we want to fold back into Dropshot. But I don't want to spend too much energy now trying to guess what those things are -- let's see what shakes out of the consumer impls.

@davepacheco davepacheco self-assigned this Oct 10, 2024
Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

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

very nice

dropshot_endpoint/src/metadata.rs Show resolved Hide resolved
let router = api.into_router();
if let VersionPolicy::Unversioned = version_policy {
if router.has_versioned_routes() {
return Err(BuildError::UnversionedServerHasVersionedRoutes);
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this the point of has_versioned_routes i.e. is it mostly here to produce an error message if the user has done something aberrant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup.

api.openapi("Evolving API", semver).write(&mut found).unwrap();
let actual = std::str::from_utf8(found.get_ref()).unwrap();
expectorate::assert_contents(
&format!("tests/test_openapi_v{}.json", version),
Copy link
Collaborator

Choose a reason for hiding this comment

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

what do you think about

Suggested change
&format!("tests/test_openapi_v{}.json", version),
&format!("tests/test_versions_v{}.json", version),

or something to help associate json with rs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 356 to 357
"semver range (from ... until ...) has the \
endpoints out of order",
Copy link
Collaborator

Choose a reason for hiding this comment

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

from version is after until version?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right. Are you suggesting changing the message? I'm fine with that (although I think it's a little hard to parse, using "from" and "until" as adjectives, especially if you're not already in the headspace of these arguments being called the "from version" and the "until version").

Copy link
Collaborator

@ahl ahl Oct 29, 2024

Choose a reason for hiding this comment

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

I found the suggestion that the numbers are out of order potentially misleading. e.g. if I have 1.2.3..1.2.2 and what I actually meant was 1.2.3..1.2.12 . I was trying to come up with some other way of expressing the problem to the user.

.into_iter()
.collect();
return Err(syn::Error::new_spanned(
span,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we want

Suggested change
span,
input.span(),

Maybe? In particular I'm thinking of this test:

dropshot/tests/fail/bad_version_backwards.stderr

error: semver range (from ... until ...) has the endpoints out of order
  --> tests/fail/bad_version_backwards.rs:17:16
   |
17 |     versions = "1.2.3".."1.2.2"
   |    

Should we be underlining both?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This doesn't quite work:

error[E0277]: the trait bound `proc_macro2::Span: ToTokens` is not satisfied
   --> dropshot_endpoint/src/metadata.rs:354:25
    |
353 |                     return Err(syn::Error::new_spanned(
    |                                ----------------------- required by a bound introduced by this call
354 |                         input.span(),
    |                         ^^^^^^^^^^^^ the trait `ToTokens` is not implemented for `proc_macro2::Span`
    |
    = help: the following other types implement trait `ToTokens`:
              &'a T
              &'a mut T
              Abstract
              AndAnd
              AndEq
              AngleBracketedGenericArguments
              Arm
              As
            and 308 others
note: required by a bound in `syn::Error::new_spanned`
   --> /Users/dap/.cargo/registry/src/index.crates.io-6f17d22bba15001f/syn-2.0.79/src/error.rs:189:27
    |
189 |     pub fn new_spanned<T: ToTokens, U: Display>(tokens: T, message: U) -> Self {
    |                           ^^^^^^^^ required by this bound in `Error::new_spanned`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `dropshot_endpoint` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...

I poked around a bit but did not immediately see how to turn that span into something that impls ToTokens (besides what the code is already doing).

v.value()
.parse::<semver::Version>()
.map_err(|e| {
syn::Error::new_spanned(v, format!("expected semver: {}", e))
Copy link
Collaborator

Choose a reason for hiding this comment

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

what do Semver::parse() errors look like? Are those more useful than something like "{} is not of the form MAJOR.MINOR.PATCH"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the messages it produces are okay. Here are a few examples:

versions = "one dot two dot three",
},
quote! {
pub async fn handler_xyz(
_rqctx: RequestContext<()>,
) -> Result<HttpResponseOk<()>, HttpError> {
Ok(())
}
},
);
assert!(!errors.is_empty());
assert_eq!(
errors.get(0).map(ToString::to_string),
Some(
"expected semver: unexpected character 'o' while \
parsing major version number"

versions = "1.2",
},
quote! {
pub async fn handler_xyz(
_rqctx: RequestContext<()>,
) -> Result<HttpResponseOk<()>, HttpError> {
Ok(())
}
},
);
assert!(!errors.is_empty());
assert_eq!(
errors.get(0).map(ToString::to_string),
Some(
"expected semver: unexpected end of input while parsing \
minor version number"

@ahl
Copy link
Collaborator

ahl commented Oct 28, 2024

Do we definitely want to allow users to specify the version number header (i.e. in ClientSpecifiesVersionInHeader)? Or is this flexibility they don't need? If we allow this to be configurable, we'll need a similar sort of configuration when generating clients.

We could document (in OpenAPI) this optional header for all API endpoints; that seems quite annoying. Alternatively we could invent an extension to tell progenitor if there's a version header and--if so--what to call it.

@davepacheco
Copy link
Collaborator Author

Do we definitely want to allow users to specify the version number header (i.e. in ClientSpecifiesVersionInHeader)? Or is this flexibility they don't need? If we allow this to be configurable, we'll need a similar sort of configuration when generating clients.

We could document (in OpenAPI) this optional header for all API endpoints; that seems quite annoying. Alternatively we could invent an extension to tell progenitor if there's a version header and--if so--what to call it.

We discussed this offline and concluded:

  • We'll pick a name that ought to be fine for all of our own stuff (probably api-version).
  • Progenitor will use this name by default, but the behavior will be configurable so that you can have it not specify anything or use a different header name.
  • Dropshot will make it easy to use the same header name. I'm being vague about the specifics here because I expect this part of the Dropshot interface to potentially change a bit when we start incorporating it into consumers so I plan to flesh this out when we do that.

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.

server support for multiple API versions
2 participants