packages feed

compaREST-0.1.0.0: src/Data/OpenApi/Compare/Validate/Server.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Data.OpenApi.Compare.Validate.Server
  ( Issue (..),
  )
where

import Control.Applicative
import Control.Arrow ((&&&))
import Control.Comonad
import Control.Monad
import Data.Attoparsec.Text
import Data.Either
import Data.Foldable
import Data.Function
import Data.Functor
import Data.HashMap.Strict.InsOrd as IOHM
import qualified Data.HashSet.InsOrd as IOHM
import qualified Data.HashSet.InsOrd as IOHS
import Data.Maybe
import Data.OpenApi
import Data.OpenApi.Compare.Behavior
import Data.OpenApi.Compare.Common
import Data.OpenApi.Compare.Paths
import Data.OpenApi.Compare.Subtree
import Data.OpenApi.Compare.Validate.MediaTypeObject
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Traversable
import Text.Pandoc.Builder
import Prelude as P

tracedParsedServerUrlParts ::
  Server ->
  Either (Issue 'ServerLevel) ProcessedServer
tracedParsedServerUrlParts s =
  let parsedUrl = parseServerUrl $ _serverUrl s
      lookupVar var = case IOHM.lookup var (_serverVariables s) of
        Nothing -> Left $ ServerVariableNotDefined var
        Just x -> Right x
   in (traverse @[] . traverse @ServerUrlPart) lookupVar parsedUrl

instance Behavable 'OperationLevel 'ServerLevel where
  data Behave 'OperationLevel 'ServerLevel
    = InServer Text
    deriving stock (Eq, Ord, Show)

  describeBehavior (InServer n) = "Server " <> code n

instance Subtree [Server] where
  type SubtreeLevel [Server] = 'OperationLevel
  type CheckEnv [Server] = '[]
  checkStructuralCompatibility _ pc =
    structuralEq $ fmap S.fromList . (fmap . fmap) reduceServer <$> pc
    where
      reducerServerVariable =
        fmap IOHM.toHashSet . _serverVariableEnum &&& _serverVariableDefault
      reduceServer =
        _serverUrl &&& fmap reducerServerVariable . IOHM.toHashMap . _serverVariables
  checkSemanticCompatibility env beh pcServer = do
    let (ProdCons (pErrs, pUrls) (cErrs, cUrls)) =
          pcServer
            <&> partitionEithers
              . fmap
                ( \(Traced t s) ->
                    let bhv = beh >>> step (InServer $ _serverUrl s)
                     in case tracedParsedServerUrlParts s of
                          Left e -> Left $ issueAt bhv e
                          Right u -> Right (bhv, Traced (t >>> step (ServerStep $ _serverUrl s)) u)
                )
              . sequence
    sequenceA_ pErrs
    sequenceA_ cErrs
    for_ pUrls $ \(bhv, pUrl) -> do
      let potentiallyCompatible = P.filter ((staticCompatible `on` extract) pUrl) $ fmap snd cUrls
      anyOfAt
        bhv
        ServerNotMatched
        [ checkCompatibility bhv env (ProdCons pUrl cUrl)
        | cUrl <- potentiallyCompatible
        ]
    pure ()

type ProcessedServer = [ServerUrlPart ServerVariable]

-- | Nothing means "open variable" – can have any value
unifyPart :: ServerUrlPart ServerVariable -> Maybe (IOHS.InsOrdHashSet Text)
unifyPart (ServerUrlVariable v) = _serverVariableEnum v
unifyPart (ServerUrlConstant c) = Just $ IOHS.singleton c

staticCompatiblePart :: ServerUrlPart x -> ServerUrlPart x -> Bool
staticCompatiblePart (ServerUrlConstant x) (ServerUrlConstant y) = x == y
staticCompatiblePart _ _ = True

staticCompatible :: [ServerUrlPart x] -> [ServerUrlPart x] -> Bool
staticCompatible a b = maybe False (all $ uncurry staticCompatiblePart) $ zipAll a b

data ServerUrlPart var
  = ServerUrlVariable var
  | ServerUrlConstant Text
  deriving stock (Eq, Show, Functor, Foldable, Traversable)

-- | This is super rough. Things like @{a|b}c@ will not match @ac@.
-- FIXME: #46
--
-- NOTE: syntax is defined vaguely in the spec.
parseServerUrl :: Text -> [ServerUrlPart Text]
-- There really is no way it can fail
parseServerUrl = fromRight undefined . parseOnly (serverUrlParser <* endOfInput)
  where
    serverUrlParser :: Parser [ServerUrlPart Text]
    serverUrlParser = many $ do
      variableUrlParser <|> do
        a <- anyChar
        aa <- takeTill (== '{')
        return (ServerUrlConstant $ T.cons a aa)

    variableUrlParser :: Parser (ServerUrlPart Text)
    variableUrlParser = do
      char '{'
      res <- takeTill (== '}')
      char '}'
      return $ ServerUrlVariable res

instance Steppable [Server] ProcessedServer where
  data Step [Server] ProcessedServer = ServerStep Text
    deriving stock (Eq, Ord, Show)

instance Issuable 'ServerLevel where
  data Issue 'ServerLevel
    = EnumValueNotConsumed Int Text
    | ConsumerNotOpen Int
    | ServerVariableNotDefined Text
    | ServerNotMatched
    deriving stock (Eq, Ord, Show)
  issueKind = \case
    ServerVariableNotDefined _ -> SchemaInvalid
    _ -> CertainIssue
  describeIssue Forward (EnumValueNotConsumed _ v) =
    para $ "Enum value " <> code v <> " has been removed."
  describeIssue Backward (EnumValueNotConsumed _ v) =
    para $ "Enum value " <> code v <> " has been added."
  describeIssue Forward (ConsumerNotOpen _) =
    para $ "A variable has been changed from being open to being closed."
  describeIssue Backward (ConsumerNotOpen _) =
    para $ "A variable has been changed from being closed to being open."
  describeIssue _ (ServerVariableNotDefined k) =
    para $ "Variable " <> code k <> " is not defined."
  describeIssue Forward ServerNotMatched = para $ "The server was removed."
  describeIssue Backward ServerNotMatched = para $ "The server was added."

instance Subtree ProcessedServer where
  type SubtreeLevel ProcessedServer = 'ServerLevel
  type CheckEnv ProcessedServer = '[]
  checkStructuralCompatibility _ pc =
    structuralEq $ (fmap . fmap . fmap . fmap) reducerServerVariable pc
    where
      reducerServerVariable =
        fmap IOHM.toHashSet . _serverVariableEnum &&& _serverVariableDefault
  checkSemanticCompatibility _ beh pc =
    -- traversing here is fine because we have already filtered for length
    for_ (zip [0 ..] $ zipProdCons . fmap (fmap unifyPart . extract) $ pc) $ \(i, pcPart) -> case pcPart of
      (Just x, Just y) -> for_ x $ \v -> unless (v `IOHS.member` y) (issueAt beh $ EnumValueNotConsumed i v)
      -- Consumer can consume anything
      (_, Nothing) -> pure ()
      -- Producer can produce anythings, but consumer has a finite enum ;(
      (Nothing, Just _) -> issueAt beh (ConsumerNotOpen i)
    where
      zipProdCons :: ProdCons [a] -> [(a, a)]
      zipProdCons (ProdCons x y) = zip x y