-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The semver grammar and ordering (npm).

'SemverKey' wraps the [@versions@](https://hackage.haskell.org/package/versions) library's
'Data.Versions.SemVer', so parsing and precedence are the library's: semver 11 ordering, with
@+build@ metadata excluded from it. Among prerelease identifiers numeric ones rank below
alphanumeric ones, the opposite of the rule in "Ecluse.Core.Version.Token". A semver version
is stable iff it carries no prerelease.
-}
module Ecluse.Core.Version.Semver (
    SemverKey (..),
    parseSemver,
    isSemverStable,
) where

import Data.Char (isDigit)
import Data.Text qualified as T
import Data.Versions (SemVer (..))
import Data.Versions qualified as V

import Ecluse.Core.Version.Token (digitRuns, withinVersionLength)

-- | A parsed semver version, ordered by the @versions@ library's semver 11 precedence.
newtype SemverKey = SemverKey SemVer
    deriving stock (Int -> SemverKey -> ShowS
[SemverKey] -> ShowS
SemverKey -> String
(Int -> SemverKey -> ShowS)
-> (SemverKey -> String)
-> ([SemverKey] -> ShowS)
-> Show SemverKey
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> SemverKey -> ShowS
showsPrec :: Int -> SemverKey -> ShowS
$cshow :: SemverKey -> String
show :: SemverKey -> String
$cshowList :: [SemverKey] -> ShowS
showList :: [SemverKey] -> ShowS
Show)
    deriving newtype (SemverKey -> SemverKey -> Bool
(SemverKey -> SemverKey -> Bool)
-> (SemverKey -> SemverKey -> Bool) -> Eq SemverKey
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: SemverKey -> SemverKey -> Bool
== :: SemverKey -> SemverKey -> Bool
$c/= :: SemverKey -> SemverKey -> Bool
/= :: SemverKey -> SemverKey -> Bool
Eq, Eq SemverKey
Eq SemverKey =>
(SemverKey -> SemverKey -> Ordering)
-> (SemverKey -> SemverKey -> Bool)
-> (SemverKey -> SemverKey -> Bool)
-> (SemverKey -> SemverKey -> Bool)
-> (SemverKey -> SemverKey -> Bool)
-> (SemverKey -> SemverKey -> SemverKey)
-> (SemverKey -> SemverKey -> SemverKey)
-> Ord SemverKey
SemverKey -> SemverKey -> Bool
SemverKey -> SemverKey -> Ordering
SemverKey -> SemverKey -> SemverKey
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: SemverKey -> SemverKey -> Ordering
compare :: SemverKey -> SemverKey -> Ordering
$c< :: SemverKey -> SemverKey -> Bool
< :: SemverKey -> SemverKey -> Bool
$c<= :: SemverKey -> SemverKey -> Bool
<= :: SemverKey -> SemverKey -> Bool
$c> :: SemverKey -> SemverKey -> Bool
> :: SemverKey -> SemverKey -> Bool
$c>= :: SemverKey -> SemverKey -> Bool
>= :: SemverKey -> SemverKey -> Bool
$cmax :: SemverKey -> SemverKey -> SemverKey
max :: SemverKey -> SemverKey -> SemverKey
$cmin :: SemverKey -> SemverKey -> SemverKey
min :: SemverKey -> SemverKey -> SemverKey
Ord)

{- | Parse a semver version, or 'Nothing' so an ordering rule abstains rather than drops it.
The length and digit-run bounds refuse input that would overflow the @versions@ library.
-}
parseSemver :: Text -> Maybe SemverKey
parseSemver :: Text -> Maybe SemverKey
parseSemver Text
raw = do
    Bool -> Maybe ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Text -> Bool
withinVersionLength Text
raw)
    Bool -> Maybe ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Bool -> Bool
not (Text -> Bool
hasOverlongNumericRun Text
raw))
    -- Evaluated in full, so a retained key holds none of the parser's intermediate state.
    SemVer -> SemverKey
SemverKey (SemVer -> SemverKey) -> (SemVer -> SemVer) -> SemVer -> SemverKey
forall b c a. (b -> c) -> (a -> b) -> a -> c
. SemVer -> SemVer
forall a. NFData a => a -> a
force (SemVer -> SemverKey) -> Maybe SemVer -> Maybe SemverKey
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Either ParsingError SemVer -> Maybe SemVer
forall l r. Either l r -> Maybe r
rightToMaybe (Text -> Either ParsingError SemVer
V.semver Text
raw)

-- | Whether a semver version is stable: a final release with no prerelease component.
isSemverStable :: SemverKey -> Bool
isSemverStable :: SemverKey -> Bool
isSemverStable (SemverKey SemVer
sv) = Maybe Release -> Bool
forall a. Maybe a -> Bool
isNothing (SemVer -> Maybe Release
_svPreRel SemVer
sv)

{- The longest digit run guaranteed to fit the @versions@ library's fixed-width numeric
components: 18 digits is at most @10^18 - 1 < 2^63@, and a longer run might overflow silently. -}
maxNumericRun :: Int
maxNumericRun :: Int
maxNumericRun = Int
18

hasOverlongNumericRun :: Text -> Bool
hasOverlongNumericRun :: Text -> Bool
hasOverlongNumericRun = (Text -> Bool) -> [Text] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any Text -> Bool
overlongRun ([Text] -> Bool) -> (Text -> [Text]) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text]
digitRuns

overlongRun :: Text -> Bool
overlongRun :: Text -> Bool
overlongRun Text
run = (Char -> Bool) -> Text -> Bool
T.all Char -> Bool
isDigit Text
run Bool -> Bool -> Bool
&& Text -> Int -> Ordering
T.compareLength Text
run Int
maxNumericRun Ordering -> Ordering -> Bool
forall a. Eq a => a -> a -> Bool
== Ordering
GT