Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c9b99ed
Introduce VersionNumber newtype.
fisx Feb 10, 2023
31c9889
Tweak hlint.
fisx Feb 11, 2023
86c4b1f
Fixup
fisx Feb 11, 2023
53758c3
hi ci
fisx Feb 12, 2023
a8d8ed7
Tweak changelog entry and haddocs.
fisx Feb 13, 2023
d2e52a1
More haddocks.
fisx Feb 13, 2023
ccd4650
Update libs/wire-api/src/Wire/API/Routes/Version.hs
fisx Feb 13, 2023
5d05349
Revert "Tweak hlint."
fisx Feb 13, 2023
e599619
Tweak hlint *locally*.
fisx Feb 13, 2023
ecf5249
D'OH!
fisx Feb 13, 2023
6780480
Merge remote-tracking branch 'refs/remotes/origin/play-with-version-t…
fisx Feb 13, 2023
4aa8e48
hi ci
fisx Feb 13, 2023
7a2bdde
hi ci
fisx Feb 13, 2023
5ecadff
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Feb 17, 2023
7e60211
Generic rendering of Version strings via `versionInt`.
fisx Feb 17, 2023
416abf1
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Feb 17, 2023
8b64085
Add pregenerated v3 swagger
pcapriotti Mar 2, 2023
81d062f
Finalise API v3
pcapriotti Mar 2, 2023
aad079e
Use v2 for welcome messages in tests
pcapriotti Mar 3, 2023
262ccb4
Add CHANGELOG entry
pcapriotti Mar 3, 2023
d09820d
Set v4 as the development version
pcapriotti Mar 3, 2023
a11e0da
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Mar 5, 2023
7589b2d
hi ci
fisx Mar 5, 2023
f02ae72
Update golden tests
pcapriotti Mar 6, 2023
d3b4f24
Add assertion for v4 to version test
pcapriotti Mar 6, 2023
d4d6ae4
Use v2 welcome in end2end tests
pcapriotti Mar 6, 2023
c21c33a
hi ci
fisx Mar 6, 2023
d2a2c39
Merge branch 'pcapriotti/finalise-v3' into play-with-version-types
fisx Mar 6, 2023
45f30a1
run-services: add status reponses to failure msg
smatting Mar 6, 2023
030f805
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Mar 6, 2023
0bbbdd8
using proof in test with max expiration time until 2038
battermann Mar 6, 2023
5d7b5f5
changelog
battermann Mar 6, 2023
5cae797
Merge remote-tracking branch 'origin/battermann/fix-dpop-access-token…
fisx Mar 6, 2023
5199887
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Mar 6, 2023
5213043
Fix versionMiddleware.
fisx Mar 7, 2023
33aefc6
Test versionMiddleware.
fisx Mar 7, 2023
759750f
Recover human-readable error messages (almost).
fisx Mar 7, 2023
1fdee85
Merge remote-tracking branch 'origin/develop' into play-with-version-…
fisx Mar 7, 2023
c454df8
Fixup
fisx Mar 7, 2023
e94dd83
hi ci
fisx Mar 7, 2023
1360860
Fix: make version parser in middleware more selective.
fisx Mar 7, 2023
18382ab
bike shed.
fisx Mar 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5-internal/play-with-version-types
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce VersionNumber newtype (see `/libs/wire-api/src/Wire/API/Routes/Version.hs` for explanation)
9 changes: 9 additions & 0 deletions libs/wire-api/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
, hex
, hostname-validate
, hscim
, hspec
, hspec-wai
, http-api-data
, http-media
, http-types
Expand Down Expand Up @@ -87,6 +89,7 @@
, tagged
, tasty
, tasty-expected-failure
, tasty-hspec
, tasty-hunit
, tasty-quickcheck
, text
Expand Down Expand Up @@ -223,6 +226,9 @@ mkDerivation {
filepath
hex
hscim
hspec
hspec-wai
http-types
imports
iso3166-country-codes
iso639
Expand All @@ -238,11 +244,13 @@ mkDerivation {
saml2-web-sso
schema-profunctor
servant
servant-server
servant-swagger-ui
string-conversions
swagger2
tasty
tasty-expected-failure
tasty-hspec
tasty-hunit
tasty-quickcheck
text
Expand All @@ -253,6 +261,7 @@ mkDerivation {
uri-bytestring
uuid
vector
wai
wire-message-proto-lens
];
license = lib.licenses.agpl3Only;
Expand Down
112 changes: 74 additions & 38 deletions libs/wire-api/src/Wire/API/Routes/Version.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
-- This file is part of the Wire Server implementation.
Expand Down Expand Up @@ -28,23 +29,23 @@ module Wire.API.Routes.Version

-- * Version
Version (..),
VersionNumber (..),
supportedVersions,
developmentVersions,
readVersionNumber,
mkVersion,
toPathComponent,

-- * Servant combinators
Until,
From,
)
where

import Control.Error (note)
import Control.Lens ((?~))
import Data.Aeson (FromJSON, ToJSON (..))
import qualified Data.Aeson as Aeson
import Data.Bifunctor
import Data.ByteString.Conversion (ToByteString (builder))
import qualified Data.Binary.Builder as Builder
import Data.ByteString.Conversion (ToByteString (builder), toByteString')
import qualified Data.ByteString.Lazy as LBS
import Data.Domain
import Data.Schema
Expand All @@ -57,52 +58,87 @@ import Servant
import Servant.Swagger
import Wire.API.Routes.Named
import Wire.API.VersionInfo
import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform))

-- | Version of the public API. Check the documentation in the *docs* directory
-- for a checklist when adding a new version.
-- | Version of the public API. Serializes to `"v<n>"`. See 'VersionNumber' below for one
-- that serializes to `<n>`. See `/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs`
-- for serialization rules.
--
-- If you add or remove versions from this type, make sure 'versionInt', 'supportedVersions',
-- and 'developmentVersions' stay in sync; everything else here should keep working without
-- change. See also documentation in the *docs* directory.
-- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist
data Version = V0 | V1 | V2 | V3 | V4
deriving stock (Eq, Ord, Bounded, Enum, Show)
deriving stock (Eq, Ord, Bounded, Enum, Show, Generic)
deriving (FromJSON, ToJSON) via (Schema Version)
deriving (Arbitrary) via (GenericUniform Version)

-- | Manual enumeration of version integrals (the `<n>` in the constructor `V<n>`).
--
-- This is not the same as 'fromEnum': we will remove unsupported versions in the future,
-- which will cause `<n>` and `fromEnum V<n>` to diverge. `Enum` should not be understood as
-- a bijection between meaningful integers and versions, but merely as a convenient way to say
-- `allVersions = [minBound..]`.
versionInt :: Integral i => Version -> i
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here: Could be implemented with fromEnum.

versionInt V0 = 0
versionInt V1 = 1
versionInt V2 = 2
versionInt V3 = 3
versionInt V4 = 4

supportedVersions :: [Version]
supportedVersions = [minBound .. V4]

developmentVersions :: [Version]
developmentVersions = [V4]

----------------------------------------------------------------------

versionText :: Version -> Text
versionText = ("v" <>) . toUrlPiece . versionInt @Int

versionByteString :: Version -> ByteString
versionByteString = ("v" <>) . toByteString' . versionInt @Int

instance ToSchema Version where
schema =
enum @Integer "Version" . mconcat $
[ element 0 V0,
element 1 V1,
element 2 V2,
element 3 V3,
element 4 V4
]

mkVersion :: Integer -> Maybe Version
mkVersion n = case Aeson.fromJSON (Aeson.Number (fromIntegral n)) of
Aeson.Error _ -> Nothing
Aeson.Success v -> pure v
schema = enum @Text "Version" . mconcat $ (\v -> element (versionText v) v) <$> [minBound ..]

instance FromHttpApiData Version where
parseHeader = first Text.pack . Aeson.eitherDecode . LBS.fromStrict
parseUrlPiece = parseHeader . Text.encodeUtf8
parseQueryParam v = note ("Unknown version: " <> v) $
getAlt $
flip foldMap [minBound ..] $ \s ->
guard (versionText s == v) $> s

instance ToHttpApiData Version where
toHeader = LBS.toStrict . Aeson.encode
toUrlPiece = Text.decodeUtf8 . toHeader
toHeader = versionByteString
toUrlPiece = versionText

instance ToByteString Version where
builder = toEncodedUrlPiece
builder = Builder.fromByteString . versionByteString

-- | Wrapper around 'Version' that serializes to integers `<n>`, as needed in
-- eg. `VersionInfo`. See `/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs` for
-- serialization rules.
newtype VersionNumber = VersionNumber {fromVersionNumber :: Version}
deriving stock (Eq, Ord, Show, Generic)
deriving newtype (Bounded, Enum)
deriving (FromJSON, ToJSON) via (Schema VersionNumber)
deriving (Arbitrary) via (GenericUniform Version)

instance ToSchema VersionNumber where
schema =
enum @Integer "VersionNumber" . mconcat $ (\v -> element (versionInt v) (VersionNumber v)) <$> [minBound ..]

-- | `Version` as it appears in an URL path
--
-- >>> toPathComponent V1
-- "v1"
toPathComponent :: Version -> ByteString
toPathComponent v = "v" <> toHeader v
instance FromHttpApiData VersionNumber where
parseHeader = first Text.pack . Aeson.eitherDecode . LBS.fromStrict
parseUrlPiece = parseHeader . Text.encodeUtf8

supportedVersions :: [Version]
supportedVersions = [minBound .. maxBound]
instance ToHttpApiData VersionNumber where
toHeader = LBS.toStrict . Aeson.encode
toUrlPiece = Text.decodeUtf8 . toHeader

developmentVersions :: [Version]
developmentVersions = [V4]
instance ToByteString VersionNumber where
builder = toEncodedUrlPiece

-- | Information related to the public API version.
--
Expand All @@ -111,8 +147,8 @@ developmentVersions = [V4]
-- backend, in order to decide how to form request paths, and how to deal with
-- federated backends and qualified user IDs.
data VersionInfo = VersionInfo
{ vinfoSupported :: [Version],
vinfoDevelopment :: [Version],
{ vinfoSupported :: [VersionNumber],
vinfoDevelopment :: [VersionNumber],
vinfoFederation :: Bool,
vinfoDomain :: Domain
}
Expand All @@ -130,7 +166,7 @@ instance ToSchema VersionInfo where
example :: VersionInfo
example =
VersionInfo
{ vinfoSupported = supportedVersions,
{ vinfoSupported = VersionNumber <$> supportedVersions,
vinfoDevelopment = [maxBound],
vinfoFederation = False,
vinfoDomain = Domain "example.com"
Expand Down
40 changes: 27 additions & 13 deletions libs/wire-api/src/Wire/API/Routes/Version/Wai.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,50 @@

module Wire.API.Routes.Version.Wai where

import Control.Monad.Except (throwError)
import Data.ByteString.Conversion
import qualified Data.Text.Lazy as LText
import Data.EitherR (fmapL)
import Data.String.Conversions (cs)
import qualified Data.Text as T
import Imports
import qualified Network.HTTP.Types as HTTP
import Network.Wai
import Network.Wai.Middleware.Rewrite
import Network.Wai.Utilities.Error
import Network.Wai.Utilities.Response
import Web.HttpApiData (parseUrlPiece, toUrlPiece)
import Wire.API.Routes.Version

-- | Strip off version prefix. Return 404 if the version is not supported.
versionMiddleware :: Set Version -> Middleware
versionMiddleware disabledAPIVersions app req k = case parseVersion (removeVersionHeader req) of
Nothing -> app req k
Just (req', n) -> case mkVersion n of
Just v | v `notElem` disabledAPIVersions -> app (addVersionHeader v req') k
_ ->
k $
errorRs' $
mkError HTTP.status404 "unsupported-version" $
"Version " <> LText.pack (show n) <> " is not supported"

parseVersion :: Request -> Maybe (Request, Integer)
Right (req', v) ->
if v `elem` disabledAPIVersions
then err (toUrlPiece v)
else app (addVersionHeader v req') k
Left (BadVersion v) -> err v
Left NoVersion -> app req k
where
err :: Text -> IO ResponseReceived
err v =
k . errorRs' . mkError HTTP.status404 "unsupported-version" $
"Version " <> cs v <> " is not supported"

data ParseVersionError = NoVersion | BadVersion Text

parseVersion :: Request -> Either ParseVersionError (Request, Version)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if this function cannot be generalized regarding its Monad: Shouldn't a constraint MonadError m => ... be sufficient?

(This is nitpicking as far as nitpicking can get ... 😉 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, that would be sufficient, but i intentionally decided against it. i like both the abstract interpretation and the precise type that offers exactly what's needed, and not more.

always happy to nit-pick, though! :)

parseVersion req = do
(version, pinfo) <- case pathInfo req of
[] -> Nothing
[] -> throwError NoVersion
(x : xs) -> pure (x, xs)
n <- readVersionNumber version
unless (looksLikeVersion version) $
throwError NoVersion
n <- fmapL (const $ BadVersion version) $ parseUrlPiece version
pure (rewriteRequestPure (\(_, q) _ -> (pinfo, q)) req, n)

looksLikeVersion :: Text -> Bool
looksLikeVersion version = case T.splitAt 1 version of (h, t) -> h == "v" && T.all isDigit t

removeVersionHeader :: Request -> Request
removeVersionHeader req =
req
Expand Down
10 changes: 0 additions & 10 deletions libs/wire-api/src/Wire/API/VersionInfo.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ module Wire.API.VersionInfo
vinfoObjectSchema,

-- * Version utilities
readVersionNumber,
versionHeader,
VersionHeader,

Expand All @@ -36,8 +35,6 @@ import qualified Data.CaseInsensitive as CI
import Data.Metrics.Servant
import Data.Schema
import Data.Singletons
import qualified Data.Text as Text
import qualified Data.Text.Read as Text
import GHC.TypeLits
import Imports
import qualified Network.Wai as Wai
Expand All @@ -51,13 +48,6 @@ import Wire.API.Routes.ClientAlgebra
vinfoObjectSchema :: ValueSchema NamedSwaggerDoc v -> ObjectSchema SwaggerDoc [v]
vinfoObjectSchema sch = field "supported" (array sch)

readVersionNumber :: Text -> Maybe Integer
readVersionNumber v = do
('v', rest) <- Text.uncons v
case Text.decimal rest of
Right (n, "") -> pure n
_ -> Nothing

type VersionHeader = "X-Wire-API-Version"

versionHeader :: CI.CI ByteString
Expand Down
3 changes: 3 additions & 0 deletions libs/wire-api/test/unit/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Main
where

import Imports
import System.IO.Unsafe (unsafePerformIO)
import Test.Tasty
import qualified Test.Wire.API.Call.Config as Call.Config
import qualified Test.Wire.API.Conversation as Conversation
Expand All @@ -33,6 +34,7 @@ import qualified Test.Wire.API.Roundtrip.HttpApiData as Roundtrip.HttpApiData
import qualified Test.Wire.API.Roundtrip.MLS as Roundtrip.MLS
import qualified Test.Wire.API.Routes as Routes
import qualified Test.Wire.API.Routes.Version as Routes.Version
import qualified Test.Wire.API.Routes.Version.Wai as Routes.Version.Wai
import qualified Test.Wire.API.Swagger as Swagger
import qualified Test.Wire.API.Team.Export as Team.Export
import qualified Test.Wire.API.Team.Member as Team.Member
Expand Down Expand Up @@ -63,5 +65,6 @@ main =
Conversation.tests,
MLS.tests,
Routes.Version.tests,
unsafePerformIO Routes.Version.Wai.tests,
RawJson.tests
]
3 changes: 3 additions & 0 deletions libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import qualified Wire.API.Provider.Service as Provider.Service
import qualified Wire.API.Provider.Service.Tag as Provider.Service.Tag
import qualified Wire.API.Push.Token as Push.Token
import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra
import qualified Wire.API.Routes.Version as Routes.Version
import qualified Wire.API.SystemSettings as SystemSettings
import qualified Wire.API.Team as Team
import qualified Wire.API.Team.Conversation as Team.Conversation
Expand Down Expand Up @@ -316,6 +317,8 @@ tests =
testRoundTrip @User.Search.TeamContact,
testRoundTrip @(Wrapped.Wrapped "some_int" Int),
testRoundTrip @Conversation.Action.SomeConversationAction,
testRoundTrip @Routes.Version.Version,
testRoundTrip @Routes.Version.VersionNumber,
testRoundTrip @TeamsIntra.GuardLegalholdPolicyConflicts,
testRoundTrip @TeamsIntra.TeamStatus,
testRoundTrip @TeamsIntra.TeamStatusUpdate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Servant.API
import qualified Test.Tasty as T
import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===))
import Type.Reflection (typeRep)
import qualified Wire.API.Routes.Version
import qualified Wire.API.User
import qualified Wire.API.User.Search
import qualified Wire.Arbitrary as Arbitrary ()
Expand All @@ -30,7 +31,9 @@ tests :: T.TestTree
tests =
T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "HttpApiData roundtrip tests" $
[ testRoundTrip @Wire.API.User.InvitationCode,
testRoundTrip @Wire.API.User.Search.PagingState
testRoundTrip @Wire.API.User.Search.PagingState,
testRoundTrip @Wire.API.Routes.Version.Version,
testRoundTrip @Wire.API.Routes.Version.VersionNumber
]

testRoundTrip ::
Expand Down
Loading