Merge pull request #29 from Start9Labs/feature/emver

emver for registry appears complete, more testing required but should…
This commit is contained in:
Keagan McClelland
2020-11-02 11:20:00 -07:00
committed by GitHub
15 changed files with 410 additions and 296 deletions

View File

@@ -30,6 +30,7 @@ ip-from-header: "_env:YESOD_IP_FROM_HEADER:false"
resources-path: "_env:RESOURCES_PATH:/var/www/html/resources"
ssl-path: "_env:SSL_PATH:/var/ssl"
ssl-auto: "_env:SSL_AUTO:true"
registry-hostname: "_env:REGISTRY_HOSTNAME:registry.start9labs.com"
tor-port: "_env:TOR_PORT:447"
static-bin-dir: "_env:STATIC_BIN:/usr/local/bin/"

View File

@@ -174,6 +174,7 @@ appMain = do
startApp :: RegistryCtx -> IO ()
startApp foundation = do
when (sslAuto . appSettings $ foundation) $ do
-- set up ssl certificates
putStrLn @Text "Setting up SSL"
_ <- setupSsl $ appSettings foundation
@@ -207,7 +208,17 @@ startWeb foundation = do
let actions = (action, torAction)
setWebProcessThreadId (join (***) asyncThreadId actions) foundation
void $ both waitCatch actions
(clearRes, torRes) <- both waitCatch actions
case clearRes of
Left e -> do
putStr @Text "Clearnet ServerError: "
print e
Right _ -> pure ()
case torRes of
Left e -> do
putStr @Text "Tor ServerError: "
print e
Right _ -> pure ()
shouldRestart <- takeMVar (appShouldRestartWeb foundation)
when shouldRestart $ do
putMVar (appShouldRestartWeb foundation) False

View File

@@ -7,24 +7,25 @@ module Database.Queries where
import Startlude
import Database.Persist.Sql
import Lib.Types.AppIndex
import Lib.Types.Semver
import Lib.Types.Emver
import Model
import Orphans.Emver ( )
fetchApp :: MonadIO m => AppIdentifier -> ReaderT SqlBackend m (Maybe (Entity SApp))
fetchApp appId = selectFirst [SAppAppId ==. appId] []
fetchAppVersion :: MonadIO m => AppVersion -> Key SApp -> ReaderT SqlBackend m (Maybe (Entity Version))
fetchAppVersion appVersion appId = selectFirst [VersionNumber ==. appVersion, VersionAppId ==. appId] []
fetchAppVersion :: MonadIO m => Version -> Key SApp -> ReaderT SqlBackend m (Maybe (Entity SVersion))
fetchAppVersion appVersion appId = selectFirst [SVersionNumber ==. appVersion, SVersionAppId ==. appId] []
createApp :: MonadIO m => AppIdentifier -> StoreApp -> ReaderT SqlBackend m (Maybe (Key SApp))
createApp appId StoreApp {..} = do
time <- liftIO getCurrentTime
insertUnique $ SApp time Nothing storeAppTitle appId storeAppDescShort storeAppDescLong storeAppIconType
createAppVersion :: MonadIO m => Key SApp -> VersionInfo -> ReaderT SqlBackend m (Maybe (Key Version))
createAppVersion :: MonadIO m => Key SApp -> VersionInfo -> ReaderT SqlBackend m (Maybe (Key SVersion))
createAppVersion sId VersionInfo {..} = do
time <- liftIO getCurrentTime
insertUnique $ Version time
insertUnique $ SVersion time
Nothing
sId
versionInfoVersion
@@ -32,7 +33,7 @@ createAppVersion sId VersionInfo {..} = do
versionInfoOsRequired
versionInfoOsRecommended
createMetric :: MonadIO m => Key SApp -> Key Version -> ReaderT SqlBackend m ()
createMetric :: MonadIO m => Key SApp -> Key SVersion -> ReaderT SqlBackend m ()
createMetric appId versionId = do
time <- liftIO $ getCurrentTime
insert_ $ Metric time appId versionId

View File

@@ -1,3 +1,4 @@
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
@@ -11,8 +12,7 @@ import Startlude
import Control.Monad.Logger
import Data.Aeson
import qualified Data.Attoparsec.ByteString.Char8
as Atto
import qualified Data.Attoparsec.Text as Atto
import qualified Data.ByteString.Lazy as BS
import Data.Char
import Data.Conduit
@@ -35,9 +35,8 @@ import Yesod.Persist.Core
import Foundation
import Lib.Registry
import Lib.Semver
import Lib.Types.AppIndex
import Lib.Types.Semver
import Lib.Types.Emver
import Lib.Types.FileSystem
import Lib.Error
import Lib.External.AppMgr
@@ -58,15 +57,16 @@ instance Show FileExtension where
show (FileExtension f Nothing ) = f
show (FileExtension f (Just e)) = f <.> e
userAgentOsVersionParser :: Atto.Parser AppVersion
userAgentOsVersionParser :: Atto.Parser Version
userAgentOsVersionParser = do
void $ (Atto.string "EmbassyOS" <|> Atto.string "AmbassadorOS" <|> Atto.string "MeshOS") *> Atto.char '/'
semverParserBS
parseVersion
getEmbassyOsVersion :: Handler (Maybe AppVersion)
getEmbassyOsVersion :: Handler (Maybe Version)
getEmbassyOsVersion = userAgentOsVersion
where
userAgentOsVersion = (hush . Atto.parseOnly userAgentOsVersionParser <=< requestHeaderUserAgent) <$> waiRequest
userAgentOsVersion =
(hush . Atto.parseOnly userAgentOsVersionParser . decodeUtf8 <=< requestHeaderUserAgent) <$> waiRequest
getAppsManifestR :: Handler TypedContent
getAppsManifestR = do
@@ -99,8 +99,7 @@ getAppManifestR appId = do
let appDir = (<> "/") . (</> show av) . (</> toS appId) $ appsDir
manifest <- handleS9ErrT $ getManifest appMgrDir appDir appExt
pure $ TypedContent "application/json" (toContent manifest)
where
appExt = Extension (toS appId) :: Extension "s9pk"
where appExt = Extension (toS appId) :: Extension "s9pk"
getAppConfigR :: AppIdentifier -> Handler TypedContent
getAppConfigR appId = do
@@ -113,8 +112,7 @@ getAppConfigR appId = do
let appDir = (<> "/") . (</> show av) . (</> toS appId) $ appsDir
config <- handleS9ErrT $ getConfig appMgrDir appDir appExt
pure $ TypedContent "application/json" (toContent config)
where
appExt = Extension (toS appId) :: Extension "s9pk"
where appExt = Extension (toS appId) :: Extension "s9pk"
getAppR :: Extension "s9pk" -> Handler TypedContent
getAppR e = do
@@ -129,7 +127,9 @@ getApp rootDir ext@(Extension appId) = do
Just t -> pure t
appVersions <- liftIO $ getAvailableAppVersions rootDir ext
putStrLn $ "valid appversion for " <> (show ext :: String) <> ": " <> show appVersions
case getSpecifiedAppVersion spec appVersions of
let satisfactory = filter ((<|| spec) . fst . unRegisteredAppVersion) appVersions
let best = fst . getMaxVersion <$> foldMap (Just . MaxVersion . (, fst . unRegisteredAppVersion)) satisfactory
case best of
Nothing -> notFound
Just (RegisteredAppVersion (appVersion, filePath)) -> do
exists <- liftIO $ doesFileExist filePath >>= \case
@@ -137,7 +137,7 @@ getApp rootDir ext@(Extension appId) = do
False -> pure NonExistent
determineEvent exists (extension ext) filePath appVersion
where
determineEvent :: FileExistence -> String -> FilePath -> AppVersion -> HandlerFor RegistryCtx TypedContent
determineEvent :: FileExistence -> String -> FilePath -> Version -> HandlerFor RegistryCtx TypedContent
-- for app files
determineEvent Existent "s9pk" fp av = do
_ <- recordMetrics appId rootDir av
@@ -152,7 +152,7 @@ chunkIt fp = do
addHeader "Content-Length" (show sz)
respondSource typeOctet $ CB.sourceFile fp .| awaitForever sendChunkBS
recordMetrics :: String -> FilePath -> AppVersion -> HandlerFor RegistryCtx ()
recordMetrics :: String -> FilePath -> Version -> HandlerFor RegistryCtx ()
recordMetrics appId rootDir appVersion = do
let appId' = T.pack appId
manifest <- liftIO $ getAppManifest rootDir

View File

@@ -7,15 +7,17 @@ import Startlude
import Data.Aeson
import Yesod.Core.Content
import Lib.Types.Semver
import Lib.Types.Emver
import Orphans.Emver ( )
data AppVersionRes = AppVersionRes
{ appVersionVersion :: AppVersion
, appVersionMinCompanion :: Maybe AppVersion
} deriving (Eq, Show)
{ appVersionVersion :: Version
, appVersionMinCompanion :: Maybe Version
}
deriving (Eq, Show)
instance ToJSON AppVersionRes where
toJSON AppVersionRes{ appVersionVersion, appVersionMinCompanion } = object $
["version" .= appVersionVersion] <> case appVersionMinCompanion of
toJSON AppVersionRes { appVersionVersion, appVersionMinCompanion } =
object $ ["version" .= appVersionVersion] <> case appVersionMinCompanion of
Nothing -> []
Just x -> ["minCompanion" .= x]

View File

@@ -14,7 +14,7 @@ import Yesod.Core
import Foundation
import Handler.Types.Status
import Lib.Registry
import Lib.Types.Semver
import Lib.Types.Emver
import Settings
import System.FilePath ( (</>) )
import Util.Shared
@@ -34,7 +34,7 @@ getVersionSysR :: Text -> Handler (Maybe AppVersionRes)
getVersionSysR sysAppId = runMaybeT $ do
sysDir <- (</> "sys") . resourcesDir . appSettings <$> getYesod
avr <- MaybeT $ getVersionWSpec sysDir sysExt
pure $ avr { appVersionMinCompanion = Just $ AppVersion (1, 1, 0, 0) }
pure $ avr { appVersionMinCompanion = Just $ Version (1, 1, 0, 0) }
where sysExt = Extension (toS sysAppId) :: Extension ""
getVersionWSpec :: KnownSymbol a => FilePath -> Extension a -> Handler (Maybe AppVersionRes)

View File

@@ -7,6 +7,7 @@ module Lib.Registry where
import Startlude
import qualified Data.Attoparsec.Text as Atto
import Data.HashMap.Lazy hiding ( mapMaybe )
import qualified GHC.Read ( Read(..) )
import qualified GHC.Show ( Show(..) )
@@ -14,30 +15,32 @@ import System.Directory
import System.FilePath
import Yesod.Core
import Lib.Semver
import Lib.Types.Semver
import Lib.Types.Emver
type Registry = HashMap String (HashMap AppVersion FilePath)
type Registry = HashMap String (HashMap Version FilePath)
newtype RegisteredAppVersion = RegisteredAppVersion (AppVersion, FilePath) deriving (Eq, Show)
instance HasAppVersion RegisteredAppVersion where
version (RegisteredAppVersion (av, _)) = av
newtype RegisteredAppVersion = RegisteredAppVersion { unRegisteredAppVersion :: (Version, FilePath) } deriving (Eq, Show)
data MaxVersion a = MaxVersion
{ getMaxVersion :: (a, a -> Version)
}
instance Semigroup (MaxVersion a) where
(MaxVersion (a, f)) <> (MaxVersion (b, g)) = if f a > g b then MaxVersion (a, f) else MaxVersion (b, g)
-- retrieve all valid semver folder names with queried for file: rootDirectory/appId/[0.0.0 ...]/appId.extension
getAvailableAppVersions :: KnownSymbol a => FilePath -> Extension a -> IO [RegisteredAppVersion]
getAvailableAppVersions rootDirectory ext@(Extension appId) = do
versions <- mapMaybe (readMaybe . toS) <$> getSubDirectories (rootDirectory </> appId)
fmap catMaybes . for versions $ \v ->
getVersionedFileFromDir rootDirectory ext v
>>= \case
versions <- mapMaybe (hush . Atto.parseOnly parseVersion . toS) <$> getSubDirectories (rootDirectory </> appId)
fmap catMaybes . for versions $ \v -> getVersionedFileFromDir rootDirectory ext v >>= \case
Nothing -> pure Nothing
Just appFile -> pure . Just $ RegisteredAppVersion (v, appFile)
where
getSubDirectories path = (fmap (fromRight []) . try @SomeException $ listDirectory path) >>= filterM (doesDirectoryExist . (path </>))
getSubDirectories path = (fmap (fromRight []) . try @SomeException $ listDirectory path)
>>= filterM (doesDirectoryExist . (path </>))
-- /root/appId/version/appId.ext
getVersionedFileFromDir :: KnownSymbol a => FilePath -> Extension a -> AppVersion -> IO (Maybe FilePath)
getVersionedFileFromDir rootDirectory ext@(Extension appId) v = getUnversionedFileFromDir (rootDirectory </> appId </> show v) ext
getVersionedFileFromDir :: KnownSymbol a => FilePath -> Extension a -> Version -> IO (Maybe FilePath)
getVersionedFileFromDir rootDirectory ext@(Extension appId) v =
getUnversionedFileFromDir (rootDirectory </> appId </> show v) ext
-- /root/appId.ext
getUnversionedFileFromDir :: KnownSymbol a => FilePath -> Extension a -> IO (Maybe FilePath)
@@ -66,8 +69,7 @@ instance KnownSymbol a => Read (Extension a) where
readsPrec _ s = case (symbolVal $ Proxy @a) of
"" -> [(Extension s, "")]
other -> [ (Extension file, "") | ext' == "" <.> other ]
where
(file, ext') = splitExtension s
where (file, ext') = splitExtension s
withPeriod :: String -> String
withPeriod word@(a : _) = case a of

View File

@@ -1,42 +0,0 @@
module Lib.Semver where
import Startlude
import Lib.Types.Semver
(<||) :: HasAppVersion a => a -> AppVersionSpec -> Bool
(<||) _ AppVersionAny = True
(<||) a (AppVersionSpec SVEquals av1) = version a == av1
(<||) a (AppVersionSpec SVLessThan av1) = version a < av1
(<||) a (AppVersionSpec SVGreaterThan av1) = version a > av1
(<||) a (AppVersionSpec SVLessThanEq av1) = version a <= av1
(<||) a (AppVersionSpec SVGreaterThanEq av1) = version a >= av1
(<||) a (AppVersionSpec SVGreatestWithMajor av1) = major av == major av1 && av >= av1 -- "maj.*"
where av = version a
(<||) a (AppVersionSpec SVGreatestWithMajorMinor av1) = major av == major av1 && minor av == minor av1 && av >= av1 -- "maj.min.*"
where av = version a
major :: AppVersion -> Word16
major (AppVersion (a, _, _, _)) = a
minor :: AppVersion -> Word16
minor (AppVersion (_, a, _, _)) = a
patch :: AppVersion -> Word16
patch (AppVersion (_, _, a, _)) = a
build :: AppVersion -> Word16
build (AppVersion (_, _, _, a)) = a
hasGiven :: (AppVersion -> Word16) -> AppVersion -> AppVersion -> Bool
hasGiven projection av = (== projection av) . projection
getSpecifiedAppVersion :: HasAppVersion a => AppVersionSpec -> [a] -> Maybe a
getSpecifiedAppVersion avSpec = appVersionMax . filter (<|| avSpec)
class HasAppVersion a where
version :: a -> AppVersion
instance HasAppVersion AppVersion where
version = id
appVersionMax :: HasAppVersion a => [a] -> Maybe a
appVersionMax [] = Nothing
appVersionMax as = Just $ maximumBy (compare `on` version) as

View File

@@ -2,24 +2,24 @@
{-# LANGUAGE TemplateHaskell #-}
module Lib.Types.AppIndex where
import Startlude
import Startlude hiding ( Any )
import Control.Monad.Fail
import Data.Aeson
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NE
import Lib.Semver
import Lib.Types.Semver
import Lib.Types.Emver
import Orphans.Emver ( )
type AppIdentifier = Text
data VersionInfo = VersionInfo
{ versionInfoVersion :: AppVersion
{ versionInfoVersion :: Version
, versionInfoReleaseNotes :: Text
, versionInfoDependencies :: HM.HashMap Text AppVersionSpec
, versionInfoOsRequired :: AppVersionSpec
, versionInfoOsRecommended :: AppVersionSpec
, versionInfoDependencies :: HM.HashMap Text VersionRange
, versionInfoOsRequired :: VersionRange
, versionInfoOsRecommended :: VersionRange
}
deriving (Eq, Show)
@@ -31,8 +31,8 @@ instance FromJSON VersionInfo where
versionInfoVersion <- o .: "version"
versionInfoReleaseNotes <- o .: "release-notes"
versionInfoDependencies <- o .:? "dependencies" .!= HM.empty
versionInfoOsRequired <- o .:? "os-version-required" .!= AppVersionAny
versionInfoOsRecommended <- o .:? "os-version-recommended" .!= AppVersionAny
versionInfoOsRequired <- o .:? "os-version-required" .!= Any
versionInfoOsRecommended <- o .:? "os-version-recommended" .!= Any
pure VersionInfo { .. }
instance ToJSON VersionInfo where
@@ -82,12 +82,12 @@ instance ToJSON AppManifest where
toJSON = toJSON . unAppManifest
filterOsRequired :: AppVersion -> StoreApp -> Maybe StoreApp
filterOsRequired :: Version -> StoreApp -> Maybe StoreApp
filterOsRequired av sa = case NE.filter ((av <||) . versionInfoOsRequired) (storeAppVersionInfo sa) of
[] -> Nothing
(x : xs) -> Just $ sa { storeAppVersionInfo = x :| xs }
filterOsRecommended :: AppVersion -> StoreApp -> Maybe StoreApp
filterOsRecommended :: Version -> StoreApp -> Maybe StoreApp
filterOsRecommended av sa = case NE.filter ((av <||) . versionInfoOsRecommended) (storeAppVersionInfo sa) of
[] -> Nothing
(x : xs) -> Just $ sa { storeAppVersionInfo = x :| xs }

252
src/Lib/Types/Emver.hs Normal file
View File

@@ -0,0 +1,252 @@
{- |
Module : Lib.Types.Emver
Description : Semver with 4th digit extension for Embassy
License : Start9 Non-Commercial
Maintainer : keagan@start9labs.com
Stability : experimental
Portability : portable
This module was designed to address the problem of releasing updates to Embassy Packages where the upstream project was
either unaware of or apathetic towards supporting their application on the Embassy platform. In most cases, the original
package will support <https://semver.org/spec/v2.0.0.html semver2>. This leaves us with the problem where we would like
to preserve the original package's version, since one of the goals of the Embassy platform is transparency. However, on
occasion, we have screwed up and published a version of a package that needed to have its metadata updated. In this
scenario we were left with the conundrum of either unilaterally claiming a version number of a package we did not author
or let the issue persist until the next update. Neither of these promote good user experiences, for different reasons.
This module extends the semver standard linked above with a 4th digit, which is given PATCH semantics.
-}
module Lib.Types.Emver
( major
, minor
, patch
, revision
, satisfies
, (<||)
, (||>)
, VersionRange(Anchor, Any, None)
, Version(..)
, AnyRange(..)
, AllRange(..)
, conj
, disj
, exactly
, parseVersion
, parseRange
)
where
import Prelude
import qualified Data.Attoparsec.Text as Atto
import Data.Function
import Data.Functor ( (<&>)
, ($>)
)
import Control.Applicative ( liftA2
, Alternative((<|>))
)
import Data.String ( IsString(..) )
import qualified Data.Text as T
-- | AppVersion is the core representation of the SemverQuad type.
newtype Version = Version { unVersion :: (Word, Word, Word, Word) } deriving (Eq, Ord)
instance Show Version where
show (Version (x, y, z, q)) =
let postfix = if q == 0 then "" else '.' : show q in show x <> "." <> show y <> "." <> show z <> postfix
instance IsString Version where
fromString s = either error id $ Atto.parseOnly parseVersion (T.pack s)
-- | A change in the value found at 'major' implies a breaking change in the API that this version number describes
major :: Version -> Word
major (Version (x, _, _, _)) = x
-- | A change in the value found at 'minor' implies a backwards compatible addition to the API that this version number
-- describes
minor :: Version -> Word
minor (Version (_, y, _, _)) = y
-- | A change in the value found at 'patch' implies that the implementation of the API has changed without changing the
-- invariants promised by the API. In many cases this will be incremented when repairing broken functionality
patch :: Version -> Word
patch (Version (_, _, z, _)) = z
-- | This is the fundamentally new value in comparison to the original semver 2.0 specification. It is given the same
-- semantics as 'patch' above, which begs the question, when should you update this value instead of that one. Generally
-- speaking, if you are both the package author and maintainer, you should not ever increment this number, as it is
-- redundant with 'patch'. However, if you maintain a package on some distribution channel, and you are /not/ the
-- original author, then it is encouraged for you to increment 'quad' instead of 'patch'.
revision :: Version -> Word
revision (Version (_, _, _, q)) = q
-- | 'Operator' is the type that specifies how to compare against the target version. Right represents the ordering,
-- Left negates it
type Operator = Either Ordering Ordering
-- | 'VersionRange' is the algebra of sets of versions. They can be constructed by having an 'Anchor' term which
-- compares against the target version, or can be described with 'Conj' which is a conjunction, or 'Disj', which is a
-- disjunction. The 'Any' and 'All' terms are primarily there to round out the algebra, but 'Any' is also exposed due to
-- its usage in semantic versioning in general. The 'None' term is not useful to the end user as there would be no
-- reasonable usage of it to describe version sets. It is included for its utility as a unit on 'Disj' and possibly as
-- a zero on 'Conj'
--
-- Laws (reflected in implementations of smart constructors):
-- Commutativity of conjunction: Conj a b === Conj b a
-- Commutativity of disjunction: Disj a b === Disj b a
-- Associativity of conjunction: Conj (Conj a b) c === Conj a (Conj b c)
-- Associativity of disjunction: Disj (Disj a b) c === Disj a (Disj b c)
-- Identity of conjunction: Any `Conj` a === a
-- Identity of disjunction: None `Disj` a === a
-- Zero of conjunction: None `Conj` a === None
-- Zero of disjunction: Any `Disj` a === Any
-- Distributivity of conjunction over disjunction: Conj a (Disj b c) === Disj (Conj a b) (Conj a c)
-- Distributivity of disjunction over conjunction: Disj a (Conj b c) === Conj (Disj a b) (Disj a c)
data VersionRange
= Anchor Operator Version
| Conj VersionRange VersionRange
| Disj VersionRange VersionRange
| Any
| None
deriving (Eq)
-- | Smart constructor for conjunctions. Eagerly evaluates zeros and identities
conj :: VersionRange -> VersionRange -> VersionRange
conj Any b = b
conj a Any = a
conj None _ = None
conj _ None = None
conj a b = Conj a b
-- | Smart constructor for disjunctions. Eagerly evaluates zeros and identities
disj :: VersionRange -> VersionRange -> VersionRange
disj Any _ = Any
disj _ Any = Any
disj None b = b
disj a None = a
disj a b = Disj a b
exactly :: Version -> VersionRange
exactly = Anchor (Left EQ)
instance Show VersionRange where
show (Anchor (Left EQ) v) = '!' : '=' : show v
show (Anchor (Right EQ) v) = '=' : show v
show (Anchor (Left LT) v) = '>' : '=' : show v
show (Anchor (Right LT) v) = '<' : show v
show (Anchor (Left GT) v) = '<' : '=' : show v
show (Anchor (Right GT) v) = '>' : show v
show (Conj a b) = paren $ show a <> (' ' : show b)
show (Disj a b) = paren $ show a <> " || " <> show b
show Any = "*"
show None = "!"
instance Read VersionRange where
readsPrec _ s = case Atto.parse parseRange (T.pack s) of
Atto.Fail _ _ _ -> []
Atto.Partial _ -> []
Atto.Done i r -> [(r, T.unpack i)]
paren :: String -> String
paren = mappend "(" . flip mappend ")"
newtype AnyRange = AnyRange { unAnyRange :: VersionRange }
instance Semigroup AnyRange where
(<>) = AnyRange <<$>> disj `on` unAnyRange
instance Monoid AnyRange where
mempty = AnyRange None
newtype AllRange = AllRange { unAllRange :: VersionRange }
instance Semigroup AllRange where
(<>) = AllRange <<$>> conj `on` unAllRange
instance Monoid AllRange where
mempty = AllRange Any
-- | Predicate for deciding whether the 'Version' is in the 'VersionRange'
satisfies :: Version -> VersionRange -> Bool
satisfies v (Anchor op v') = either (\c x y -> compare x y /= c) (\c x y -> compare x y == c) op v v'
satisfies v (Conj a b ) = v `satisfies` a && v `satisfies` b
satisfies v (Disj a b ) = v `satisfies` a || v `satisfies` b
satisfies _ Any = True
satisfies _ None = False
(<||) :: Version -> VersionRange -> Bool
(<||) = satisfies
{-# INLINE (<||) #-}
(||>) :: VersionRange -> Version -> Bool
(||>) = flip satisfies
{-# INLINE (||>) #-}
(<<$>>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
(<<$>>) = fmap . fmap
{-# INLINE (<<$>>) #-}
parseOperator :: Atto.Parser Operator
parseOperator =
(Atto.char '=' $> Left EQ)
<|> (Atto.string ">=" $> Right GT)
<|> (Atto.string "<=" $> Right LT)
<|> (Atto.char '>' $> Left GT)
<|> (Atto.char '<' $> Left LT)
parseVersion :: Atto.Parser Version
parseVersion = do
major' <- Atto.decimal <* Atto.char '.'
minor' <- Atto.decimal <* Atto.char '.'
patch' <- Atto.decimal
quad' <- Atto.option 0 $ Atto.char '.' *> Atto.decimal
pure $ Version (major', minor', patch', quad')
-- >>> Atto.parseOnly parseRange "=2.3.4 1.2.3.4 - 2.3.4.5 (>3.0.0 || <3.4.5)"
-- Right (=2.3.4 ((>=1.2.3.4 <=2.3.4.5) (>3.0.0 || <3.4.5)))
parseRange :: Atto.Parser VersionRange
parseRange = s <|> (Atto.char '*' *> pure Any)
where
sub = Atto.char '(' *> Atto.skipSpace *> parseRange <* Atto.skipSpace <* Atto.char ')'
s =
unAnyRange
. foldMap AnyRange
<$> ((p <|> sub) `Atto.sepBy1` (Atto.skipSpace *> Atto.string "||" <* Atto.skipSpace))
p = unAllRange . foldMap AllRange <$> ((a <|> sub) `Atto.sepBy1` Atto.space)
a = liftA2 Anchor parseOperator parseVersion <|> caret <|> tilde <|> wildcard <|> hyphen
-- >>> Atto.parseOnly parseRange "^2.3.0.5"
-- Right (>=2.3.0.5 <3.0.0)
caret :: Atto.Parser VersionRange
caret = (Atto.char '^' *> parseVersion) <&> \case
v@(Version (0, 0, 0, _)) -> Anchor (Right EQ) v
v@(Version (0, 0, z, _)) -> rangeIE v (Version (0, 0, z + 1, 0))
v@(Version (0, y, _, _)) -> rangeIE v (Version (0, y + 1, 0, 0))
v@(Version (x, _, _, _)) -> rangeIE v (Version (x + 1, 0, 0, 0))
-- >>> Atto.parseOnly tilde "~1.2.3.4"
-- Right (>=1.2.3.4 <1.2.4)
tilde :: Atto.Parser VersionRange
tilde = (Atto.char '~' *> (Atto.decimal `Atto.sepBy1` Atto.char '.')) >>= \case
[x, y, z, q] -> pure $ rangeIE (Version (x, y, z, q)) (Version (x, y, z + 1, 0))
[x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y + 1, 0, 0))
[x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0))
[x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0))
o -> fail $ "Invalid number of version numbers: " <> show (length o)
range :: Bool -> Bool -> Version -> Version -> VersionRange
range inc0 inc1 v0 v1 =
let lo = if inc0 then Left LT else Right GT
hi = if inc1 then Left GT else Right LT
in Conj (Anchor lo v0) (Anchor hi v1)
rangeIE :: Version -> Version -> VersionRange
rangeIE = range True False
-- >>> Atto.parseOnly wildcard "1.2.3.x"
-- Right (>=1.2.3 <1.2.4)
wildcard :: Atto.Parser VersionRange
wildcard = (Atto.many1 (Atto.decimal <* Atto.char '.') <* Atto.char 'x') >>= \case
[x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y, z + 1, 0))
[x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0))
[x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0))
o -> fail $ "Invalid number of version numbers: " <> show (length o)
-- >>> Atto.parseOnly hyphen "0.1.2.3 - 1.2.3.4"
-- Right (>=0.1.2.3 <=1.2.3.4)
hyphen :: Atto.Parser VersionRange
hyphen = liftA2 (range True True) parseVersion (Atto.skipSpace *> Atto.char '-' *> Atto.skipSpace *> parseVersion)

View File

@@ -1,154 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Lib.Types.Semver where
import Startlude hiding ( break )
import qualified GHC.Read ( Read(..) )
import qualified GHC.Show ( Show(..) )
import Control.Monad.Fail
import Data.Aeson
import qualified Data.Attoparsec.ByteString.Char8
as AttoBS
import Data.Char ( isDigit )
import Data.String.Interpolate
import Data.Text
import Yesod.Core
import Database.Persist.Sql
------------------------------------------------------------------------------------------------------------------------
-- Semver AppVersion
------------------------------------------------------------------------------------------------------------------------
newtype AppVersion = AppVersion
{ unAppVersion :: (Word16, Word16, Word16, Word16) } deriving (Eq, Ord, Hashable)
instance Read AppVersion where
readsPrec _ s = case traverse (readMaybe . toS) $ splitOn "+" <=< splitOn "." $ (toS s) of
Just [major, minor, patch, build] -> [(AppVersion (major, minor, patch, build), "")]
Just [major, minor, patch] -> [(AppVersion (major, minor, patch, 0), "")]
_ -> []
instance PathPiece AppVersion where
fromPathPiece = readMaybe . toS
toPathPiece = show
instance Show AppVersion where
show (AppVersion (a, b, c, d)) | d == 0 = [i|#{a}.#{b}.#{c}|]
| otherwise = [i|#{a}.#{b}.#{c}+#{d}|]
instance IsString AppVersion where
fromString s = case traverse (readMaybe . toS) $ splitOn "+" <=< splitOn "." $ (toS s) of
Just [major, minor, patch, build] -> AppVersion (major, minor, patch, build)
Just [major, minor, patch] -> AppVersion (major, minor, patch, 0)
_ -> panic . toS $ "Invalid App Version: " <> s
instance ToJSON AppVersion where
toJSON = String . show
instance FromJSON AppVersion where
parseJSON = withText "app version" $ \t -> case traverse (decode . toS) $ splitOn "+" <=< splitOn "." $ t of
Just [a, b, c, d] -> pure $ AppVersion (a, b, c, d)
Just [a, b, c] -> pure $ AppVersion (a, b, c, 0)
_ -> fail "unknown versioning"
instance ToTypedContent AppVersion where
toTypedContent = toTypedContent . toJSON
instance ToContent AppVersion where
toContent = toContent . toJSON
instance FromJSONKey AppVersion where
fromJSONKey = FromJSONKeyTextParser $ \t -> case readMaybe (toS t) of
Nothing -> fail "invalid app version"
Just x -> pure x
instance PersistField AppVersion where
toPersistValue = toPersistValue @Text . show
fromPersistValue = note "invalid app version" . readMaybe <=< fromPersistValue
instance PersistFieldSql AppVersion where
sqlType _ = SqlString
------------------------------------------------------------------------------------------------------------------------
-- Semver AppVersionSpec
------------------------------------------------------------------------------------------------------------------------
data AppVersionSpec =
AppVersionAny
| AppVersionSpec SemverRequestModifier AppVersion
deriving Eq
instance Read AppVersionSpec where
readsPrec _ s = if s == "*"
then [(AppVersionAny, "")]
else case (readMaybe . toS $ svMod, readMaybe . toS $ version) of
(Just m, Just av) -> [(AppVersionSpec m av, "")]
_ -> []
where (svMod, version) = break isDigit . toS $ s
instance PathPiece AppVersionSpec where
fromPathPiece = readMaybe . toS
toPathPiece = show
instance Show AppVersionSpec where
show AppVersionAny = "*"
show (AppVersionSpec r b) = show r <> show b
instance ToJSON AppVersionSpec where
toJSON = String . show
instance FromJSON AppVersionSpec where
parseJSON = withText "app version spec" $ \t -> if t == "*"
then pure AppVersionAny
else do
let (svMod, version) = break isDigit t
baseVersion <- parseJSON . String $ version
requestModifier <- parseJSON . String $ svMod
pure $ AppVersionSpec requestModifier baseVersion
instance PersistField AppVersionSpec where
toPersistValue = PersistText . show
fromPersistValue (PersistText spec) = note ("Invalid Semver Requirement: " <> spec) . readMaybe $ spec
fromPersistValue other = Left $ "Persistent Type Mismatch. Expected 'PersistText _' got " <> show other
instance PersistFieldSql AppVersionSpec where
sqlType _ = SqlString
mostRecentVersion :: AppVersionSpec
mostRecentVersion = AppVersionSpec SVGreaterThanEq $ AppVersion (0, 0, 0, 0)
------------------------------------------------------------------------------------------------------------------------
-- Semver RequestModifier
------------------------------------------------------------------------------------------------------------------------
data SemverRequestModifier = SVEquals | SVLessThan | SVGreaterThan | SVGreatestWithMajor | SVGreatestWithMajorMinor | SVLessThanEq | SVGreaterThanEq deriving (Eq, Bounded, Enum)
instance Show SemverRequestModifier where
show SVEquals = "="
show SVLessThan = "<"
show SVGreaterThan = ">"
show SVGreatestWithMajor = "~"
show SVGreatestWithMajorMinor = "^"
show SVLessThanEq = "<="
show SVGreaterThanEq = ">="
instance FromJSON SemverRequestModifier where
parseJSON = withText "semver request modifier" $ \t -> case readMaybe . toS $ t of
Just m -> pure m
Nothing -> fail "invalid semver request modifier"
instance Read SemverRequestModifier where
readsPrec _ = \case
"" -> [(SVGreatestWithMajorMinor, "")]
"=" -> [(SVEquals, "")]
"<" -> [(SVLessThan, "")]
">" -> [(SVGreaterThan, "")]
"~" -> [(SVGreatestWithMajor, "")]
"^" -> [(SVGreatestWithMajorMinor, "")]
"<=" -> [(SVLessThanEq, "")]
">=" -> [(SVGreaterThanEq, "")]
_ -> []
semverParserBS :: AttoBS.Parser AppVersion
semverParserBS = do
major <- AttoBS.decimal <* AttoBS.char '.'
minor <- AttoBS.decimal <* AttoBS.char '.'
patch <- AttoBS.decimal
build <- AttoBS.option 0 $ AttoBS.char '+' *> AttoBS.decimal
pure $ AppVersion (major, minor, patch, build)

View File

@@ -11,7 +11,8 @@ module Model where
import Startlude
import Database.Persist.TH
import Lib.Types.Semver
import Lib.Types.Emver
import Orphans.Emver ( )
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
@@ -27,14 +28,14 @@ SApp
deriving Eq
deriving Show
Version
SVersion sql=version
createdAt UTCTime
updatedAt UTCTime Maybe
appId SAppId
number AppVersion
number Version
releaseNotes Text
osVersionRequired AppVersionSpec default='*'
osVersionRecommended AppVersionSpec default='*'
osVersionRequired VersionRange default='*'
osVersionRecommended VersionRange default='*'
UniqueBin appId number
deriving Eq
deriving Show
@@ -43,7 +44,7 @@ Version
Metric
createdAt UTCTime
appId SAppId
version VersionId
version SVersionId
deriving Eq
deriving Show
|]

35
src/Orphans/Emver.hs Normal file
View File

@@ -0,0 +1,35 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- | This module is here for the express purpose of keeping ecosystem dependencies separate from the core library.
-- The core library should in theory be only dependent on base, text, and attoparsec. These are reasonable dependencies.
-- aeson, persistent, and yesod are not. So we put those here as they will not be extracted into a separate library.
module Orphans.Emver where
import Startlude
import Data.Aeson
import qualified Data.Attoparsec.Text as Atto
import Lib.Types.Emver
import Database.Persist.Sql
import qualified Data.Text as T
import Control.Monad.Fail ( MonadFail(fail) )
instance FromJSON Version where
parseJSON = withText "Emver Version" $ either fail pure . Atto.parseOnly parseVersion
instance ToJSON Version where
toJSON = String . show
instance FromJSON VersionRange where
parseJSON = withText "Emver" $ either fail pure . Atto.parseOnly parseRange
instance ToJSON VersionRange where
toJSON = String . show
instance PersistField Version where
toPersistValue = PersistText . show
fromPersistValue = first T.pack . Atto.parseOnly parseVersion <=< fromPersistValue
instance PersistFieldSql Version where
sqlType _ = SqlString
instance PersistField VersionRange where
toPersistValue = PersistText . show
fromPersistValue = first T.pack . Atto.parseOnly parseRange <=< fromPersistValue
instance PersistFieldSql VersionRange where
sqlType _ = SqlString

View File

@@ -23,8 +23,9 @@ import Network.Wai.Handler.Warp ( HostPreference )
import System.FilePath ( (</>) )
import Yesod.Default.Config2 ( configSettingsYml )
import Lib.Types.Semver
import Lib.Types.Emver
import Lib.Types.AppIndex
import Orphans.Emver ( )
-- | Runtime settings to configure this application. These settings can be
-- loaded from various sources: defaults, environment variables, config files,
-- theoretically even a database.
@@ -44,8 +45,9 @@ data AppSettings = AppSettings
-- ^ Should all log messages be displayed?
, resourcesDir :: FilePath
, sslPath :: FilePath
, sslAuto :: Bool
, registryHostname :: Text
, registryVersion :: AppVersion
, registryVersion :: Version
, sslKeyLocation :: FilePath
, sslCsrLocation :: FilePath
, sslCertLocation :: FilePath
@@ -63,6 +65,7 @@ instance FromJSON AppSettings where
appShouldLogAll <- o .:? "should-log-all" .!= False
resourcesDir <- o .: "resources-path"
sslPath <- o .: "ssl-path"
sslAuto <- o .: "ssl-auto"
registryHostname <- o .: "registry-hostname"
torPort <- o .: "tor-port"
staticBinDir <- o .: "static-bin-dir"

View File

@@ -9,14 +9,16 @@ import Yesod.Core
import Foundation
import Lib.Registry
import Lib.Semver
import Lib.Types.Semver
import Lib.Types.Emver
import Data.Semigroup
getVersionFromQuery :: KnownSymbol a => FilePath -> Extension a -> Handler (Maybe AppVersion)
getVersionFromQuery :: KnownSymbol a => FilePath -> Extension a -> Handler (Maybe Version)
getVersionFromQuery rootDir ext = do
specString <- T.filter (not . isSpace) . fromMaybe "*" <$> lookupGetParam "spec"
spec <- case readMaybe specString of
Nothing -> sendResponseStatus status400 ("Invalid App Version Specification" :: Text)
Just t -> pure t
appVersions <- liftIO $ getAvailableAppVersions rootDir ext
pure $ version <$> getSpecifiedAppVersion spec appVersions
let satisfactory = filter ((<|| spec) . fst . unRegisteredAppVersion) appVersions
let best = getMax <$> foldMap (Just . Max . fst . unRegisteredAppVersion) satisfactory
pure best