group.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package versioning
  2. import (
  3. "strings"
  4. "github.com/kataras/iris/v12/context"
  5. "github.com/kataras/iris/v12/core/router"
  6. "github.com/blang/semver/v4"
  7. )
  8. // API is a type alias of router.Party.
  9. // This is required in order for a Group instance
  10. // to implement the Party interface without field conflict.
  11. type API = router.Party
  12. // Group represents a group of resources that should
  13. // be handled based on a version requested by the client.
  14. // See `NewGroup` for more.
  15. type Group struct {
  16. API
  17. validate semver.Range
  18. deprecation DeprecationOptions
  19. }
  20. // NewGroup returns a version Group based on the given "version" constraint.
  21. // Group completes the Party interface.
  22. // The returned Group wraps a cloned Party of the given "r" Party therefore,
  23. // any changes to its parent won't affect this one (e.g. register global middlewares afterwards).
  24. //
  25. // A version is extracted through the versioning.GetVersion function:
  26. //
  27. // Accept-Version: 1.0.0
  28. // Accept: application/json; version=1.0.0
  29. //
  30. // You can customize it by setting a version based on the request context:
  31. //
  32. // api.Use(func(ctx *context.Context) {
  33. // if version := ctx.URLParam("version"); version != "" {
  34. // SetVersion(ctx, version)
  35. // }
  36. //
  37. // ctx.Next()
  38. // })
  39. //
  40. // OR:
  41. //
  42. // api.Use(versioning.FromQuery("version", ""))
  43. //
  44. // Examples at: _examples/routing/versioning
  45. // Usage:
  46. //
  47. // app := iris.New()
  48. // api := app.Party("/api")
  49. // v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
  50. // v1.Get/Post/Put/Delete...
  51. //
  52. // Valid ranges are:
  53. // - "<1.0.0"
  54. // - "<=1.0.0"
  55. // - ">1.0.0"
  56. // - ">=1.0.0"
  57. // - "1.0.0", "=1.0.0", "==1.0.0"
  58. // - "!1.0.0", "!=1.0.0"
  59. //
  60. // A Range can consist of multiple ranges separated by space:
  61. // Ranges can be linked by logical AND:
  62. // - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7"
  63. //
  64. // but not "1.0.0" or "2.0.0"
  65. // - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0
  66. //
  67. // except 2.0.3-beta.2
  68. //
  69. // Ranges can also be linked by logical OR:
  70. // - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
  71. //
  72. // AND has a higher precedence than OR. It's not possible to use brackets.
  73. //
  74. // Ranges can be combined by both AND and OR
  75. //
  76. // - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`,
  77. //
  78. // but not `4.2.1`, `2.1.1`
  79. func NewGroup(r API, version string) *Group {
  80. version = strings.ReplaceAll(version, ",", " ")
  81. version = strings.TrimSpace(version)
  82. verRange, err := semver.ParseRange(version)
  83. if err != nil {
  84. r.Logger().Errorf("versioning: %s: %s", r.GetRelPath(), strings.ToLower(err.Error()))
  85. return &Group{API: r}
  86. }
  87. // Clone this one.
  88. r = r.Party("/")
  89. // Note that this feature alters the RouteRegisterRule to RouteOverlap
  90. // the RouteOverlap rule does not contain any performance downside
  91. // but it's good to know that if you registered other mode, this wanna change it.
  92. r.SetRegisterRule(router.RouteOverlap)
  93. handler := makeHandler(verRange)
  94. // This is required in order to not populate this middleware to the next group.
  95. r.UseOnce(handler)
  96. // This is required for versioned custom error handlers,
  97. // of course if the parent registered one then this will do nothing.
  98. r.UseError(handler)
  99. return &Group{
  100. API: r,
  101. validate: verRange,
  102. }
  103. }
  104. // Deprecated marks this group and all its versioned routes
  105. // as deprecated versions of that endpoint.
  106. func (g *Group) Deprecated(options DeprecationOptions) *Group {
  107. // store it for future use, e.g. collect all deprecated APIs and notify the developer.
  108. g.deprecation = options
  109. g.API.UseOnce(func(ctx *context.Context) {
  110. WriteDeprecated(ctx, options)
  111. ctx.Next()
  112. })
  113. return g
  114. }
  115. func makeHandler(validate semver.Range) context.Handler {
  116. return func(ctx *context.Context) {
  117. if !matchVersionRange(ctx, validate) {
  118. // The overlapped handler has an exception
  119. // of a type of context.NotFound (which versioning.ErrNotFound wraps)
  120. // to clear the status code
  121. // and the error to ignore this
  122. // when available match version exists (see `NewGroup`).
  123. if h := NotFoundHandler; h != nil {
  124. h(ctx)
  125. return
  126. }
  127. }
  128. ctx.Next()
  129. }
  130. }