You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

315 lines
9.2KB

  1. package versioning
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/kataras/iris/v12/context"
  6. "github.com/blang/semver/v4"
  7. )
  8. const (
  9. // APIVersionResponseHeader the response header which its value contains
  10. // the normalized semver matched version.
  11. APIVersionResponseHeader = "X-Api-Version"
  12. // AcceptVersionHeaderKey is the header key of "Accept-Version".
  13. AcceptVersionHeaderKey = "Accept-Version"
  14. // AcceptHeaderKey is the header key of "Accept".
  15. AcceptHeaderKey = "Accept"
  16. // AcceptHeaderVersionValue is the Accept's header value search term the requested version.
  17. AcceptHeaderVersionValue = "version"
  18. // NotFound is the key that can be used inside a `Map` or inside `ctx.SetVersion(versioning.NotFound)`
  19. // to tell that a version wasn't found, therefore the `NotFoundHandler` should handle the request instead.
  20. NotFound = "iris.api.version.notfound"
  21. // Empty is just an empty string. Can be used as a key for a version alias
  22. // when the requested version of a resource was not even specified by the client.
  23. // The difference between NotFound and Empty is important when version aliases are registered:
  24. // - A NotFound cannot be registered as version alias, it
  25. // means that the client sent a version with its request
  26. // but that version was not implemented by the server.
  27. // - An Empty indicates that the client didn't send any version at all.
  28. Empty = ""
  29. )
  30. // ErrNotFound reports whether a requested version
  31. // does not match with any of the server's implemented ones.
  32. var ErrNotFound = fmt.Errorf("version %w", context.ErrNotFound)
  33. // NotFoundHandler is the default version not found handler that
  34. // is executed from `NewMatcher` when no version is registered as available to dispatch a resource.
  35. var NotFoundHandler = func(ctx *context.Context) {
  36. // 303 is an option too,
  37. // end-dev has the chance to change that behavior by using the NotFound in the map:
  38. //
  39. // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  40. /*
  41. 10.5.2 501 Not Implemented
  42. The server does not support the functionality required to fulfill the request.
  43. This is the appropriate response when the server does not
  44. recognize the request method and is not capable of supporting it for any resource.
  45. */
  46. ctx.StopWithPlainError(501, ErrNotFound)
  47. }
  48. // FromQuery is a simple helper which tries to
  49. // set the version constraint from a given URL Query Parameter.
  50. // The X-Api-Version is still valid.
  51. func FromQuery(urlQueryParameterName string, defaultVersion string) context.Handler {
  52. return func(ctx *context.Context) {
  53. version := ctx.URLParam(urlQueryParameterName)
  54. if version == "" {
  55. version = defaultVersion
  56. }
  57. if version != "" {
  58. SetVersion(ctx, version)
  59. }
  60. ctx.Next()
  61. }
  62. }
  63. // If reports whether the "got" matches the "expected" one.
  64. // the "expected" can be a constraint like ">=1.0.0 <2.0.0".
  65. // This function is just a helper, better use the Group instead.
  66. func If(got string, expected string) bool {
  67. v, err := semver.Make(got)
  68. if err != nil {
  69. return false
  70. }
  71. validate, err := semver.ParseRange(expected)
  72. if err != nil {
  73. return false
  74. }
  75. return validate(v)
  76. }
  77. // Match reports whether the request matches the expected version.
  78. // This function is just a helper, better use the Group instead.
  79. func Match(ctx *context.Context, expectedVersion string) bool {
  80. validate, err := semver.ParseRange(expectedVersion)
  81. if err != nil {
  82. return false
  83. }
  84. return matchVersionRange(ctx, validate)
  85. }
  86. func matchVersionRange(ctx *context.Context, validate semver.Range) bool {
  87. gotVersion := GetVersion(ctx)
  88. alias, aliasFound := GetVersionAlias(ctx, gotVersion)
  89. if aliasFound {
  90. SetVersion(ctx, alias) // set the version so next routes have it already.
  91. gotVersion = alias
  92. }
  93. if gotVersion == "" {
  94. return false
  95. }
  96. v, err := semver.Make(gotVersion)
  97. if err != nil {
  98. return false
  99. }
  100. if !validate(v) {
  101. return false
  102. }
  103. versionString := v.String()
  104. if !aliasFound { // don't lose any time to set if already set.
  105. SetVersion(ctx, versionString)
  106. }
  107. ctx.Header(APIVersionResponseHeader, versionString)
  108. return true
  109. }
  110. // GetVersion returns the current request version.
  111. //
  112. // By default the `GetVersion` will try to read from:
  113. // - "Accept" header, i.e Accept: "application/json; version=1.0.0"
  114. // - "Accept-Version" header, i.e Accept-Version: "1.0.0"
  115. //
  116. // However, the end developer can also set a custom version for a handler via a middleware by using the context's store key
  117. // for versions (see `Key` for further details on that).
  118. //
  119. // See `SetVersion` too.
  120. func GetVersion(ctx *context.Context) string {
  121. // firstly by context store, if manually set by a middleware.
  122. version := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetVersionContextKey())
  123. if version != "" {
  124. return version
  125. }
  126. // secondly by the "Accept-Version" header.
  127. if version = ctx.GetHeader(AcceptVersionHeaderKey); version != "" {
  128. return version
  129. }
  130. // thirdly by the "Accept" header which is like"...; version=1.0"
  131. acceptValue := ctx.GetHeader(AcceptHeaderKey)
  132. if acceptValue != "" {
  133. if idx := strings.Index(acceptValue, AcceptHeaderVersionValue); idx != -1 {
  134. rem := acceptValue[idx:]
  135. startVersion := strings.Index(rem, "=")
  136. if startVersion == -1 || len(rem) < startVersion+1 {
  137. return ""
  138. }
  139. rem = rem[startVersion+1:]
  140. end := strings.Index(rem, " ")
  141. if end == -1 {
  142. end = strings.Index(rem, ";")
  143. }
  144. if end == -1 {
  145. end = len(rem)
  146. }
  147. if version = rem[:end]; version != "" {
  148. return version
  149. }
  150. }
  151. }
  152. return ""
  153. }
  154. // SetVersion force-sets the API Version.
  155. // It can be used inside a middleware.
  156. // Example of how you can change the default behavior to extract a requested version (which is by headers)
  157. // from a "version" url parameter instead:
  158. // func(ctx iris.Context) { // &version=1
  159. // version := ctx.URLParamDefault("version", "1.0.0")
  160. // versioning.SetVersion(ctx, version)
  161. // ctx.Next()
  162. // }
  163. // See `GetVersion` too.
  164. func SetVersion(ctx *context.Context, constraint string) {
  165. ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetVersionContextKey(), constraint)
  166. }
  167. // AliasMap is just a type alias of the standard map[string]string.
  168. // Head over to the `Aliases` function below for more.
  169. type AliasMap = map[string]string
  170. // Aliases is a middleware which registers version constraint aliases
  171. // for the children Parties(routers). It's respected by versioning Groups.
  172. //
  173. // Example Code:
  174. // app := iris.New()
  175. //
  176. // api := app.Party("/api")
  177. // api.Use(Aliases(map[string]string{
  178. // versioning.Empty: "1.0.0", // when no version was provided by the client.
  179. // "beta": "4.0.0",
  180. // "stage": "5.0.0-alpha"
  181. // }))
  182. //
  183. // v1 := NewGroup(api, ">=1.0.0 < 2.0.0")
  184. // v1.Get/Post...
  185. //
  186. // v4 := NewGroup(api, ">=4.0.0 < 5.0.0")
  187. // v4.Get/Post...
  188. //
  189. // stage := NewGroup(api, "5.0.0-alpha")
  190. // stage.Get/Post...
  191. func Aliases(aliases AliasMap) context.Handler {
  192. cp := make(AliasMap, len(aliases)) // copy the map here so we are safe of later modifications by end-dev.
  193. for k, v := range aliases {
  194. cp[k] = v
  195. }
  196. return func(ctx *context.Context) {
  197. SetVersionAliases(ctx, cp, true)
  198. ctx.Next()
  199. }
  200. }
  201. // Handler returns a handler which is only fired
  202. // when the "version" is matched with the requested one.
  203. // It is not meant to be used by end-developers
  204. // (exported for version controller feature).
  205. // Use `NewGroup` instead.
  206. func Handler(version string) context.Handler {
  207. validate, err := semver.ParseRange(version)
  208. if err != nil {
  209. return func(ctx *context.Context) {
  210. ctx.StopWithError(500, err)
  211. return
  212. }
  213. }
  214. return makeHandler(validate)
  215. }
  216. // GetVersionAlias returns the version alias of the given "gotVersion"
  217. // or empty. It Reports whether the alias was found.
  218. // See `SetVersionAliases`, `Aliases` and `Match` for more.
  219. func GetVersionAlias(ctx *context.Context, gotVersion string) (string, bool) {
  220. key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
  221. if key == "" {
  222. return "", false
  223. }
  224. v := ctx.Values().Get(key)
  225. if v == nil {
  226. return "", false
  227. }
  228. aliases, ok := v.(AliasMap)
  229. if !ok {
  230. return "", false
  231. }
  232. version, ok := aliases[gotVersion]
  233. if !ok {
  234. return "", false
  235. }
  236. return strings.TrimSpace(version), true
  237. }
  238. // SetVersionAliases sets a map of version aliases when a requested
  239. // version of a resource was not implemented by the server.
  240. // Can be used inside a middleware to the parent Party
  241. // and always before the child versioning groups (see `Aliases` function).
  242. //
  243. // The map's key (string) should be the "got version" (by the client)
  244. // and the value should be the "version constraint to match" instead.
  245. // The map's value(string) should be a registered version
  246. // otherwise it will hit the NotFoundHandler (501, "version not found" by default).
  247. //
  248. // The given "aliases" is a type of standard map[string]string and
  249. // should NOT be modified afterwards.
  250. //
  251. // The last "override" input argument indicates whether any
  252. // existing aliases, registered by previous handlers in the chain,
  253. // should be overridden or copied to the previous map one.
  254. func SetVersionAliases(ctx *context.Context, aliases AliasMap, override bool) {
  255. key := ctx.Application().ConfigurationReadOnly().GetVersionAliasesContextKey()
  256. if key == "" {
  257. return
  258. }
  259. v := ctx.Values().Get(key)
  260. if v == nil || override {
  261. ctx.Values().Set(key, aliases)
  262. return
  263. }
  264. if existing, ok := v.(AliasMap); ok {
  265. for k, v := range aliases {
  266. existing[k] = v
  267. }
  268. }
  269. }