typespec icon indicating copy to clipboard operation
typespec copied to clipboard

Have an explicit way of defining the api version parameter

Open timotheeguerin opened this issue 2 years ago • 4 comments

Currently it is just a name convention where we look for the parameter name api-version (in azure). This is hacky, instead we could

  1. Only look for a parameter using the Versions enum
  2. hAve a ApiVersion scalar in the versioning library that define that value

Issue with 1. is it might be harder to compose, azure core libraries are not expecting that value today and we would need to add an extra template parameter.

timotheeguerin avatar Jan 24 '24 19:01 timotheeguerin

I think 1. is the ideal solution as the versioning system is well-established. I don't know what Azure's use case is currently, but I propose extending the @versioned attribute in such a way you can optionally specify a parameter name to inject like:

@versioned(Versions, "versionParameter")
@service
@route("/api/{versionParameter}")
namespace Services {
    ...
}

To prevent name clashing, I also think this parameter should be opt-in only so if you don't set it, there isn't a default that's used.

jpx15 avatar Oct 07 '24 01:10 jpx15

@timotheeguerin Would be curious to get your thoughts on this. I should have pinged on the original comment.

jpx15 avatar Nov 01 '24 00:11 jpx15

I think the issue with this is versioning library assume some http parameter here and the 2 should be independent. I should be able to use versioning with other protocol.

What I was more thinking is that you have either in the @server or route/query be a parameter where the type is the version enum.

timotheeguerin avatar Nov 01 '24 02:11 timotheeguerin

Versioning in Rest API's can be done in multiple ways, amongst others:

  • making the version part of the url/route
  • through specific headers
  • through contentType

We're use the 1st options and are now running into issues with auto-generating route handlers (using openapiglue in Fastify) based on OAS emitted from TypeSpec, when having multiple versions of our api

This is because typespec emits a yaml file per version and between the different yamls there are duplicate routes, as there's no way to include the actual version in the route (of for example the namespace)

I tried the approach outlined in https://github.com/microsoft/typespec/issues/2322#issuecomment-1690510417 and it doesn't solve the duplicate route definitions, besides the feedback already provided in https://github.com/microsoft/typespec/issues/2322#issuecomment-1693774622, which I agree with

I think the issue with this is versioning library assume some http parameter here and the 2 should be independent.

I don't quite see how a setup suggested in https://github.com/microsoft/typespec/issues/2819#issuecomment-2395738323 has the notion of assuming a http parameter, as the versions are defined through @versioned(Versions) on the namespace and the Versions enum inside the namespace and the versionParameter would be just an emit-time variable.

I think this suggested approach would go a long way wrt to flexibility, as it would allow authors to use the actual version value being emitted anywhere where they like, such as routes, headers or cookies etc when for example emitting OAS

Any chance to move forward on this?

p-bakker avatar May 08 '25 07:05 p-bakker

Any updates on this? Sounds like a simple reserved version variable should do the trick.

adminy avatar Jun 24 '25 17:06 adminy

@timotheeguerin @markcowl Sry to nag here, but this api versioning thing is causing us problems, so am wondering how to move forward...

As the OAS emitter creates an OAS file per version with the version recorded inside it (#info.version}, the OAS (file) is for one specific version

When you have different versions of your api, these different versions must surface somewhere on endpoints, being it through the url, a contentType or header.

To be able to express this in the resulting OAS, somehow we need to be able to reference the actual version that is being emitted, so we can include it in the url/contentType/header, which to my knowledge isn't possible right now, which means we're kinda stuck

AFAICS either approach suggested in this issue could work just fine:

@versioned(Versions)
@server("https://my-domain.com/api/{Versions}", "Main entry point")
namespace MyNS;

enum Versions {
  v1: "v1",
  v2: "2.0.0",
}

OR

@versioned(Versions, "myVersionParam")
@server("https://my-domain.com/api/{myVersionParam}", "Main entry point")
namespace MyNS;

enum Versions {
  v1: "v1",
  v2: "2.0.0",
}

The approach suggested in https://github.com/microsoft/typespec/issues/2322#issuecomment-1690510417 isn't correct, as it exposes different versions as different servers while within the context of an OAS for a specific version

Hope you have time to respond and hopefully add this to an upcoming version of TypeSpec

BTW: I think I could even argue that the 'Versions enum' shouldn't even be emitted as a component, as it isn't a thing in an OAS that is already for a specific version

p-bakker avatar Jul 07 '25 11:07 p-bakker

BTW: I think I could even argue that the 'Versions enum' shouldn't even be emitted as a component, as it isn't a thing in an OAS that is already for a specific version Agree on this, we actually made that change a while back for our azure openapi 2.0 emitter but seems never backported it.

In the examples above would you expect the following openapi to be produced?

in openapi.v2.yaml?

servers:
  - url: https://my-domain.com/api/v2
    description: Main entry point

Just trying to understand what is currently blocking for existing emitters.

I would like not to have this magic parameters as when I filed this issue it was exactly to find a solution that didn't rely on a know parameter(which is currently called api-version in Azure)

timotheeguerin avatar Jul 07 '25 15:07 timotheeguerin

Given the input in my comment above, I expect 2 OAS files as output, one for version v1 and one for version v.2.0.0:

OAS file emitted for v1:

openapi: 3.1.0
info:
  title: My API
  version: v1
...
servers:
  - url: https://my-domain.com/api/v1", "Main entry point")
    description: Main entry point

OAS file emitted for v2

openapi: 3.1.0
info:
  title: My API
  version: v2.0.0
...
servers:
  - url: https://my-domain.com/api/v2.0.0", "Main entry point")
    description: Main entry point

What imho is blocking with the current OAS emitter is that I cannot get the 'version' value into the URL, based on which version the OAS file being emitted for

I would like not to have this magic parameters

But if I, as the author of the .tsp file, explicitly provide the name of the parameter (versioned(Versions, "myVersionParameterName") ), then it's not 'magic', no?

p-bakker avatar Jul 07 '25 15:07 p-bakker

That one @versioned(Versions, "myVersionParameterName") gets also a little messy. The versioning library is unaware of the http library and defining this name like that does feel a little too targetted at interpolating the values in a uri template format.

The other things to take into acount is while that is what you want(and im sure would be the case for most) keeping the api version as a parameter in the openapi is something that some might want(Azure sdks do want for example to always be able to call any api versions for reasons)

To come back to my original options doing option 1 despite having blockers it would I think solve the majority of cases and adding option 2 later wouldn't really be an issue.

So we could see the following:

  1. Interplated as a string template
@versioned(Versions)
@server("https://my-domain.com/api/${Versions}", "Main entry point") 
namespace MyNS;

enum Versions {v1, v2}

Produce

#--- v1.yaml
servers:
  - url: https://my-domain.com/api/v1", "Main entry point")
    description: Main entry point
#--- v2.yaml
servers:
  - url: https://my-domain.com/api/v2", "Main entry point")
    description: Main entry point
  1. Interplated as a parameter
@versioned(Versions)
@server("https://my-domain.com/api/{version}", "Main entry point", {version: Versions}) 
namespace MyNS;

enum Versions {v1, v2}
#--- v1.yaml
servers:
  - url: https://my-domain.com/api/{version}", "Main entry point")
    description: Main entry point
    parameters: 
       version: ...
#--- v2.yaml
servers:
  - url: https://my-domain.com/api/{version}", "Main entry point")
    description: Main entry point
    parameters: 
       version: ...

timotheeguerin avatar Jul 07 '25 15:07 timotheeguerin

I think you forgot to remove the , "myVersionParam" in @versioned(Versions, "myVersionParam") in the two options you described above?

The other things to take into acount is while that is what you want(and im sure would be the case for most) keeping the api version as a parameter in the openapi is something that some might want(Azure sdks do want for example to always be able to call any api versions for reasons)

I don't know the Azure api, so am not exactly sure what you mean by keeping the api version as a parameter in the openapi. If it means being able to send the version as a query parameter, then I think in that scenario you'd want the 'version' query parameter on each endpoint with a specific hardcoded value per emitted OAS, because an OAS file is always just for one version, right?

Another completely other approach I looked at (but currently isn't supported either) was something like: const version: string = getVersion(), but I guess thats not feasible either? Also tried a decorator on the const to provide it its value, but decorators on const are a no-go either

p-bakker avatar Jul 07 '25 16:07 p-bakker

I think you forgot to remove the , "myVersionParam" in @versioned(Versions, "myVersionParam") in the two options you described above?

yes my bad, updated

I don't know the Azure api, so am not exactly sure what you mean by keeping the api version as a parameter in the openapi. If it means being able to send the version as a query parameter, then I think in that scenario you'd want the 'version' query parameter on each endpoint with a specific hardcoded value per emitted OAS, because an OAS file is always just for one version, right?

not really, long story short despite having a SDK that it targetted at a specific version(latest) they want to still be able for edge cases to force a different version. Point is api version is a very special parameter there and loosing that info by flattening it into the url would loose that.

Another completely other approach I looked at (but currently isn't supported either) was something like: const version: string = getVersion(), but I guess thats not feasible either? Also tried a decorator on the const to provide it its value, but decorators on const are a no-go either

yeah that would be and maybe is a better alternative to option 2 but definitely be a lot longer before we can do that one as we need to fully design an implement tsp functions first.

timotheeguerin avatar Jul 07 '25 16:07 timotheeguerin

yeah that would be and maybe is a better alternative to option 2

Should I create a separate case for that, not shouldn't I bother?

So, given that option 1 seems to be the easiest to implement and solves the challenge/problem, is there any chance in seeing that land in an upcoming version?

p-bakker avatar Jul 07 '25 16:07 p-bakker

Unfortunately, we need to focus on migrating all Azure services and related components over the next six months. As a result, core TypeSpec is unlikely to receive any major investment during this period. This is something we can most likely have approved in a design meeting as those have been relatively uneventful recently but can't commit to any timeline for implementing it. I'll try to write up a more detailed porposal of the change.

timotheeguerin avatar Jul 07 '25 16:07 timotheeguerin

@timotheeguerin since this might not make it into a release shortly, can you think of any way to to hook into the OAS emitter for every version and then maybe programmatically modify the server url(s?)

p-bakker avatar Jul 10 '25 14:07 p-bakker

No hooks you can use but you can make your own emitter. And use this function from openapi3 emitter to get all the documents then modify and save as you see fit

timotheeguerin avatar Jul 10 '25 15:07 timotheeguerin

Summary of design meeting:

  1. https://github.com/microsoft/typespec/issues/7936 Agree that referencing Versions enum should be understood by emitters as the api version if they care 1.1. https://github.com/microsoft/typespec/issues/7937 Versioning mutator should filter to only keep the curent version member
  2. https://github.com/microsoft/typespec/issues/7935 Add a flag to openapi emitter to flatten the api version parameter into the url

When needs more thinkking

  1. apiVersion scalar has some good idea but needs more thinking on 1.1 Should the mutator mutate it to the current api version enum so it just "works"
  2. Template interpolation has some good idea but might need more general thinking of how to integrate with a generialized server templating like
@server("https://example.com/${route}?api-version=${Versions}")

timotheeguerin avatar Jul 16 '25 18:07 timotheeguerin

Also the url version might be different from the actual semver for the spec, so v1 vs 1.0.2 for example.

EraYaN avatar Jul 22 '25 08:07 EraYaN