packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/BindingSpec/Private/Version.hs

{-# LANGUAGE TemplateHaskellQuotes #-}

-- | Binding specification version management
--
-- This /private/ module may only be used by "HsBindgen.BindingSpec" and
-- sub-modules.
--
-- Intended for unqualified import.
module HsBindgen.BindingSpec.Private.Version (
    -- * HsBindgenVersion
    HsBindgenVersion
  , prettyForTraceHsBindgenVersion
    -- * BindingSpecVersion
  , BindingSpecVersion
  , constBindingSpecVersion
  , parseBindingSpecVersion
    -- * Compatibility
  , BindingSpecCompatibility(..)
  , isCompatBindingSpecVersions
    -- * AVersion
  , AVersion(..)
  , mkAVersion
  , getAVersion
  ) where

import Data.Aeson ((.:), (.=))
import Data.Aeson.Types qualified as Aeson
import Data.Char qualified as Char
import Data.List qualified as List
import Data.Text qualified as Text
import Data.Version qualified
import Language.Haskell.TH.Syntax qualified as THS
import Text.Read (readMaybe)
import Text.SimplePrettyPrint qualified as PP

import HsBindgen.Errors (failCode)
import HsBindgen.Imports
import HsBindgen.Util.Tracer (PrettyForTrace (prettyForTrace))

import Paths_hs_bindgen qualified as Package

{-------------------------------------------------------------------------------
  HsBindgenVersion
-------------------------------------------------------------------------------}

-- | @hs-bindgen@ version
type HsBindgenVersion = Data.Version.Version

prettyForTraceHsBindgenVersion :: HsBindgenVersion -> PP.CtxDoc
prettyForTraceHsBindgenVersion = PP.string . Data.Version.showVersion

{-------------------------------------------------------------------------------
  BindingSpecVersion
-------------------------------------------------------------------------------}

-- | Binding specification version
--
-- Binding specification versions are in @MAJOR.MINOR@ format where
-- @MAJOR >= 1@ and @MINOR >= 0@.
data BindingSpecVersion = UnsafeBindingSpecVersion Int Int
  deriving stock (Eq, Ord, THS.Lift)

instance Aeson.FromJSON BindingSpecVersion where
  parseJSON = Aeson.withText "BindingSpecVersion" $ \t ->
    case parseBindingSpecVersion t of
      Right v -> return v
      Left  e -> Aeson.parseFail $
        "Invalid binding specification version: " ++ e ++ ": " ++ show t

instance Aeson.ToJSON BindingSpecVersion where
  toJSON = Aeson.String . Text.pack . show

instance PrettyForTrace BindingSpecVersion where
  prettyForTrace = PP.string . show

instance Show BindingSpecVersion where
  show (UnsafeBindingSpecVersion x y) = show x ++ '.' : show y

-- | Construct a 'BindingSpecVersion'
mkBindingSpecVersion ::
     Int  -- ^ Major version (@>= 1@)
  -> Int  -- ^ Minor version (@>= 0@)
  -> Either String BindingSpecVersion
mkBindingSpecVersion major minor
    | major < 1 = Left "major version less than 1"
    | minor < 0 = Left "minor version less than 0"
    | otherwise = Right (UnsafeBindingSpecVersion major minor)

-- | Construct a 'BindingSpecVersion', validating it a compile-time
constBindingSpecVersion ::
     (MonadFail m, THS.Quote m)
  => Int  -- ^ Major version
  -> Int  -- ^ Minor version
  -> THS.Code m BindingSpecVersion
constBindingSpecVersion major minor = case mkBindingSpecVersion major minor of
    Right v -> [|| v ||]
    Left  e -> failCode $ "Invalid binding specification version: " ++ e

-- | Parse a 'BindingSpecVersion'
parseBindingSpecVersion :: Text -> Either String BindingSpecVersion
parseBindingSpecVersion t =
    case List.uncons <$> span Char.isDigit (Text.unpack t) of
      (majorS, Just ('.', minorS)) ->
        case (readMaybe majorS, readMaybe minorS) of
          (Just major, Just minor) -> mkBindingSpecVersion major minor
          _otherwise               -> Left "not in MAJOR.MINOR format"
      _otherwise -> Left "not in MAJOR.MINOR format"

{-------------------------------------------------------------------------------
  Compatibility
-------------------------------------------------------------------------------}

-- | Binding specification compatibility strictness
data BindingSpecCompatibility =
    -- | Do not allow newer minor versions
    BindingSpecStrict
  | -- | Allow newer minor versions
    BindingSpecAllowNewer
  deriving stock (Eq, Show)

instance Default BindingSpecCompatibility where
  def = BindingSpecStrict

-- | Check 'BindingSpecVersion' compatibility
isCompatBindingSpecVersions ::
     BindingSpecCompatibility
  -> BindingSpecVersion  -- ^ Version of binding specification being read
  -> BindingSpecVersion  -- ^ Version of binding specification module
  -> Bool
isCompatBindingSpecVersions
  BindingSpecStrict
  (UnsafeBindingSpecVersion majorL minorL)
  (UnsafeBindingSpecVersion majorR minorR) =
    majorL == majorR && minorL <= minorR
isCompatBindingSpecVersions
  BindingSpecAllowNewer
  (UnsafeBindingSpecVersion majorL _)
  (UnsafeBindingSpecVersion majorR _) =
    majorL == majorR

{-------------------------------------------------------------------------------
  AVersion
-------------------------------------------------------------------------------}

-- | JSON/YAML version information
data AVersion = AVersion {
      bindgen     :: HsBindgenVersion
    , bindingSpec :: BindingSpecVersion
    }
  deriving stock Show

instance Aeson.FromJSON AVersion where
  parseJSON = Aeson.withObject "AVersion" $ \o -> do
    aVersionHsBindgen            <- o .: "hs_bindgen"
    aVersionBindingSpecification <- o .: "binding_specification"
    return AVersion{
        bindgen     = aVersionHsBindgen
      , bindingSpec = aVersionBindingSpecification
      }

instance Aeson.ToJSON AVersion where
  toJSON version = Aeson.object [
      "hs_bindgen"            .= version.bindgen
    , "binding_specification" .= version.bindingSpec
    ]

-- | Construct an t'AVersion' with the current versions
mkAVersion :: BindingSpecVersion -> AVersion
mkAVersion aVersionBindingSpecification = AVersion{
      bindgen = Package.version
    , bindingSpec = aVersionBindingSpecification
    }

-- | Internal type used to parse just the version information
newtype ABindingSpecVersion = ABindingSpecVersion {
      aVersion :: AVersion
    }
  deriving stock Show

instance Aeson.FromJSON ABindingSpecVersion where
  parseJSON = Aeson.withObject "file" $ \o -> do
    aVersion <- o .: "version"
    return $ ABindingSpecVersion aVersion

instance Aeson.ToJSON ABindingSpecVersion where
  toJSON (ABindingSpecVersion aVersion) = Aeson.object [
      "version" .= aVersion
    ]

-- | Parse just the version information
getAVersion :: Aeson.Value -> Either String AVersion
getAVersion value = case Aeson.fromJSON value of
    Aeson.Success (ABindingSpecVersion aVersion) -> Right aVersion
    Aeson.Error   err                            -> Left  err