{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
module Data.Medea.Analysis
( AnalysisError (..),
ArrayType (..),
CompiledSchema (..),
TypeNode (..),
compileSchemata,
)
where
import Algebra.Graph.Acyclic.AdjacencyMap (toAcyclic)
import qualified Algebra.Graph.AdjacencyMap as Cyclic
import Control.Applicative ((<|>))
import Control.Monad (foldM, when)
import Control.Monad.Except (MonadError (..))
import Data.Coerce (coerce)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NEList
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (isJust, isNothing, mapMaybe)
import Data.Medea.JSONType (JSONType (..))
import Data.Medea.Parser.Primitive
( Identifier,
MedeaString (..),
Natural,
PrimTypeIdentifier (..),
ReservedIdentifier (..),
identFromReserved,
isReserved,
isStartIdent,
tryPrimType,
typeOf,
)
import Data.Medea.Parser.Spec.Array
( elementType,
maxLength,
minLength,
tupleSpec,
)
import Data.Medea.Parser.Spec.Object
( additionalAllowed,
additionalSchema,
properties,
)
import Data.Medea.Parser.Spec.Property (propName, propOptional, propSchema)
import qualified Data.Medea.Parser.Spec.Schema as Schema
import qualified Data.Medea.Parser.Spec.Schemata as Schemata
import qualified Data.Medea.Parser.Spec.String as String
import qualified Data.Medea.Parser.Spec.Type as Type
import qualified Data.Set as S
import Data.Set.NonEmpty (NESet)
import qualified Data.Set.NonEmpty as NESet
import Data.Text (Text)
import Data.Vector (Vector)
import qualified Data.Vector as V
import Prelude
data AnalysisError
= DuplicateSchemaName !Identifier
| NoStartSchema
| DanglingTypeReference !Identifier !Identifier
| TypeRelationIsCyclic
| ReservedDefined !Identifier
| DefinedButNotUsed !Identifier
| MinMoreThanMax !Identifier
| DanglingTypeRefProp !Identifier !Identifier
| DanglingTypeRefList !Identifier !Identifier
| DanglingTypeRefTuple !Identifier !Identifier
| DuplicatePropName !Identifier !MedeaString
| PropertyWithoutObject !Identifier
| ListWithoutArray !Identifier
| TupleWithoutArray !Identifier
| StringValsWithoutString !Identifier
deriving stock (Eq, Show)
data TypeNode
= AnyNode
| PrimitiveNode !JSONType
| CustomNode !Identifier
deriving stock (Eq, Ord, Show)
data CompiledSchema = CompiledSchema
{ schemaNode :: !TypeNode,
typesAs :: {-# UNPACK #-} !(NESet TypeNode),
minArrayLen :: !(Maybe Natural),
maxArrayLen :: !(Maybe Natural),
arrayTypes :: !(Maybe ArrayType),
props :: !(HashMap Text (TypeNode, Bool)),
additionalProps :: !Bool,
additionalPropSchema :: !TypeNode,
stringVals :: {-# UNPACK #-} !(Vector Text)
}
deriving stock (Eq, Show)
data ArrayType
= ListType !TypeNode
| TupleType {-# UNPACK #-} !(Vector TypeNode)
deriving stock (Eq, Show)
checkAcyclic ::
(MonadError AnalysisError m) =>
Map Identifier CompiledSchema ->
m ()
checkAcyclic m =
when (isNothing . toAcyclic . getTypesAsGraph $ m) $
throwError TypeRelationIsCyclic
compileSchemata ::
(MonadError AnalysisError m) =>
Schemata.Specification ->
m (Map Identifier CompiledSchema)
compileSchemata (Schemata.Specification v) = do
m <- foldM go M.empty v
checkStartSchema m
checkDanglingReferences getTypeRefs DanglingTypeReference m
checkDanglingReferences getPropertyTypeRefs DanglingTypeRefProp m
checkDanglingReferences getListTypeRefs DanglingTypeRefList m
checkDanglingReferences getTupleTypeRefs DanglingTypeRefTuple m
checkUnusedSchemata m
checkAcyclic m
pure m
where
go acc spec = M.alterF (checkedInsert spec) (Schema.name spec) acc
checkedInsert spec = \case
Nothing -> Just <$> compileSchema spec
Just _ -> throwError . DuplicateSchemaName $ ident
where
ident = Schema.name spec
compileSchema ::
(MonadError AnalysisError m) =>
Schema.Specification ->
m CompiledSchema
compileSchema scm = do
when (isReserved schemaName && (not . isStartIdent) schemaName)
$ throwError . ReservedDefined
$ schemaName
let minListLen = minLength arraySpec
maxListLen = maxLength arraySpec
when (isJust minListLen && isJust maxListLen && minListLen > maxListLen)
$ throwError
$ MinMoreThanMax schemaName
propMap <- foldM go HM.empty (maybe V.empty properties objSpec)
let arrType = getArrayTypes (elementType arraySpec) (tupleSpec arraySpec)
tupleLen = getTupleTypeLen arrType
hasPropSpec = isJust objSpec
compiledScm =
CompiledSchema
{ schemaNode = identToNode . Just $ schemaName,
typesAs = NESet.fromList . defaultToAny . V.toList . fmap (identToNode . Just) $ types,
minArrayLen = minListLen <|> tupleLen,
maxArrayLen = maxListLen <|> tupleLen,
arrayTypes = arrType,
props = propMap,
additionalProps = maybe True additionalAllowed objSpec,
additionalPropSchema = identToNode $ objSpec >>= additionalSchema,
stringVals = String.toReducedSpec stringValsSpec
}
when (shouldNotHavePropertySpec compiledScm hasPropSpec)
$ throwError . PropertyWithoutObject
$ schemaName
when (shouldNotHaveListSpec compiledScm)
$ throwError . ListWithoutArray
$ schemaName
when (shouldNotHaveTupleSpec compiledScm)
$ throwError . TupleWithoutArray
$ schemaName
when (shouldNotHaveStringSpec compiledScm)
$ throwError . StringValsWithoutString
$ schemaName
pure compiledScm
where
Schema.Specification schemaName (Type.Specification types) stringValsSpec arraySpec objSpec =
scm
go acc prop = HM.alterF (checkedInsert prop) (coerce $ propName prop) acc
checkedInsert prop = \case
Nothing -> pure . Just $ (identToNode (propSchema prop), propOptional prop)
Just _ -> throwError $ DuplicatePropName schemaName (propName prop)
defaultToAny :: [TypeNode] -> NEList.NonEmpty TypeNode
defaultToAny xs = case NEList.nonEmpty xs of
Nothing -> (NEList.:|) AnyNode []
Just xs' -> xs'
checkStartSchema ::
(MonadError AnalysisError m) =>
Map Identifier CompiledSchema ->
m ()
checkStartSchema m = case M.lookup (identFromReserved RStart) m of
Nothing -> throwError NoStartSchema
Just _ -> pure ()
checkDanglingReferences ::
(MonadError AnalysisError m) =>
(CompiledSchema -> [TypeNode]) ->
(Identifier -> Identifier -> AnalysisError) ->
Map Identifier CompiledSchema ->
m ()
checkDanglingReferences getRefs err m = mapM_ go . M.toList $ m
where
go (schemaName, scm) = case getDanglingRefs scm of
danglingRef : _ -> throwError $ err danglingRef schemaName
[] -> pure ()
getDanglingRefs = filter isUndefined . mapMaybe fromCustomNode . getRefs
isUndefined ident = isNothing . M.lookup ident $ m
fromCustomNode (CustomNode ident) = Just ident
fromCustomNode _ = Nothing
checkUnusedSchemata ::
(MonadError AnalysisError m) =>
Map Identifier CompiledSchema ->
m ()
checkUnusedSchemata m = mapM_ checkUnused . M.keys $ m
where
checkUnused ident
| S.member (CustomNode ident) allReferences = pure ()
| isStartIdent ident = pure ()
| otherwise = throwError $ DefinedButNotUsed ident
allReferences = S.unions . fmap getReferences . M.elems $ m
getReferences scm =
S.fromList $
getTypeRefs scm ++ getPropertyTypeRefs scm ++ getListTypeRefs scm ++ getTupleTypeRefs scm
identToNode :: Maybe Identifier -> TypeNode
identToNode ident = case ident of
Nothing -> AnyNode
Just t -> maybe (CustomNode t) (PrimitiveNode . typeOf) $ tryPrimType t
getTypeRefs :: CompiledSchema -> [TypeNode]
getTypeRefs = NEList.toList . NESet.toList . typesAs
getPropertyTypeRefs :: CompiledSchema -> [TypeNode]
getPropertyTypeRefs scm = (fmap fst . HM.elems . props $ scm) ++ [additionalPropSchema scm]
getListTypeRefs :: CompiledSchema -> [TypeNode]
getListTypeRefs scm = case arrayTypes scm of
Just (ListType typeNode) -> [typeNode]
_ -> []
getTupleTypeRefs :: CompiledSchema -> [TypeNode]
getTupleTypeRefs scm = case arrayTypes scm of
Just (TupleType typeNodes) -> V.toList typeNodes
_ -> []
getArrayTypes :: Maybe Identifier -> Maybe [Identifier] -> Maybe ArrayType
getArrayTypes Nothing Nothing = Nothing
getArrayTypes (Just ident) _ = Just . ListType . identToNode . Just $ ident
getArrayTypes _ (Just idents) =
Just . TupleType . V.fromList $ identToNode . Just <$> idents
getTupleTypeLen :: Maybe ArrayType -> Maybe Natural
getTupleTypeLen (Just (TupleType types)) = Just . fromIntegral . V.length $ types
getTupleTypeLen _ = Nothing
getTypesAsGraph :: Map Identifier CompiledSchema -> Cyclic.AdjacencyMap TypeNode
getTypesAsGraph = Cyclic.edges . concatMap intoTypesAsEdges . M.elems
intoTypesAsEdges :: CompiledSchema -> [(TypeNode, TypeNode)]
intoTypesAsEdges scm = fmap (schemaNode scm,) . NEList.toList . NESet.toList . typesAs $ scm
arrayNode :: TypeNode
arrayNode = PrimitiveNode JSONArray
objectNode :: TypeNode
objectNode = PrimitiveNode JSONObject
stringNode :: TypeNode
stringNode = PrimitiveNode JSONString
hasListSpec :: CompiledSchema -> Bool
hasListSpec scm = case arrayTypes scm of
Just (ListType _) -> True
Just (TupleType _) -> False
_ -> isJust $ minArrayLen scm <|> maxArrayLen scm
hasTupleSpec :: CompiledSchema -> Bool
hasTupleSpec scm = case arrayTypes scm of
Just (TupleType _) -> True
_ -> False
hasStringSpec :: CompiledSchema -> Bool
hasStringSpec = not . V.null . stringVals
shouldNotHavePropertySpec :: CompiledSchema -> Bool -> Bool
shouldNotHavePropertySpec scm hasPropSpec = hasPropSpec && (not . NESet.member objectNode . typesAs $ scm)
shouldNotHaveListSpec :: CompiledSchema -> Bool
shouldNotHaveListSpec scm = hasListSpec scm && (not . NESet.member arrayNode . typesAs $ scm)
shouldNotHaveTupleSpec :: CompiledSchema -> Bool
shouldNotHaveTupleSpec scm = hasTupleSpec scm && (not . NESet.member arrayNode . typesAs $ scm)
shouldNotHaveStringSpec :: CompiledSchema -> Bool
shouldNotHaveStringSpec scm = hasStringSpec scm && (not . NESet.member stringNode . typesAs $ scm)