packages feed

poppy-codegen-1.0.0: src/Poppy/Codegen/Validate.hs

{-# LANGUAGE OverloadedStrings #-}

module Poppy.Codegen.Validate
  ( ValidationError (..),
    validateSchema,
  )
where

import Data.List (find, nub)
import Data.Maybe (catMaybes, isNothing)
import Data.Text (Text)
import Poppy.Codegen.IR

data ValidationError
  = DuplicateModelName Text
  | DuplicateEnumName Text
  | EnumHasNoVariants Text
  | DuplicateEnumVariant Text Text
  | UnknownEnumType Text Text Text
  | ModelMissingPrimaryKey Text
  | ModelMultiplePrimaryKeys Text
  | UnknownRelationModel Text Text Text
  | UnknownRelationField Text Text Text
  | DuplicateRelationName Text Text
  | RelationNameClashesWithField Text Text
  | UnknownUniqueModel Text
  | UnknownUniqueField Text Text
  | EmptyUniqueConstraint Text
  deriving (Show, Eq)

validateSchema :: Schema -> [ValidationError]
validateSchema schema =
  concat
    [ duplicateModelNames schema,
      duplicateEnumNames schema,
      concatMap validateEnum (schemaEnums schema),
      concatMap (validateModel schema) (schemaModels schema),
      concatMap (validateRelation schema) (concatMap modelRelations (schemaModels schema)),
      concatMap (validateUnique schema) (schemaUniques schema)
    ]

duplicateModelNames :: Schema -> [ValidationError]
duplicateModelNames schema =
  map DuplicateModelName (duplicates (map modelName (schemaModels schema)))

duplicateEnumNames :: Schema -> [ValidationError]
duplicateEnumNames schema =
  map DuplicateEnumName (duplicates (map enumName (schemaEnums schema)))

validateEnum :: EnumSpec -> [ValidationError]
validateEnum enumSpec =
  let emptyErr =
        [EnumHasNoVariants (enumName enumSpec) | null (enumVariants enumSpec)]
      dupVars =
        map
          (DuplicateEnumVariant (enumName enumSpec))
          (duplicates (map variantName (enumVariants enumSpec)))
   in emptyErr ++ dupVars

validateModel :: Schema -> Model -> [ValidationError]
validateModel schema model =
  let pks = filter fieldIsPrimaryKey (modelFields model)
      pkErrs = case pks of
        [] -> [ModelMissingPrimaryKey (modelName model)]
        [_] -> []
        _ -> [ModelMultiplePrimaryKeys (modelName model)]
      enumErrs = concatMap (validateFieldEnum schema (modelName model)) (modelFields model)
   in pkErrs ++ enumErrs ++ validateRelationNames model

validateRelationNames :: Model -> [ValidationError]
validateRelationNames model =
  map (DuplicateRelationName (modelName model)) (duplicates (map relName (modelRelations model)))
    ++ [ RelationNameClashesWithField (modelName model) name
         | rel <- modelRelations model,
           let name = relName rel,
           any ((== name) . fieldName) (modelFields model)
       ]

validateFieldEnum :: Schema -> Text -> FieldSpec -> [ValidationError]
validateFieldEnum schema modelName fieldSpec =
  case fieldType fieldSpec of
    TyEnum wanted ->
      ([UnknownEnumType modelName (fieldName fieldSpec) wanted | not (any ((== wanted) . enumName) (schemaEnums schema))])
    _ -> []

validateRelation :: Schema -> RelationSpec -> [ValidationError]
validateRelation schema rel =
  let fromOk = findModel schema (relFromModel rel)
      toOk = findModel schema (relToModel rel)
      modelErrs =
        catMaybes
          [ if isNothing fromOk
              then Just (UnknownRelationModel (relName rel) "from" (relFromModel rel))
              else Nothing,
            if isNothing toOk
              then Just (UnknownRelationModel (relName rel) "to" (relToModel rel))
              else Nothing
          ]
      fieldErrs = case (fromOk, toOk, relKind rel) of
        (Just fromModel, Just toModel, RelHasMany) ->
          requireField (relName rel) (modelName fromModel) (relLocalField rel) fromModel
            ++ requireField (relName rel) (modelName toModel) (relForeignField rel) toModel
        (Just fromModel, Just toModel, RelBelongsTo) ->
          requireField (relName rel) (modelName fromModel) (relForeignField rel) fromModel
            ++ requireField (relName rel) (modelName toModel) (relLocalField rel) toModel
        _ -> []
   in modelErrs ++ fieldErrs

findModel :: Schema -> Text -> Maybe Model
findModel schema name =
  find ((== name) . modelName) (schemaModels schema)

validateUnique :: Schema -> UniqueConstraint -> [ValidationError]
validateUnique schema UniqueConstraint {uniqueModel, uniqueFields} =
  case findModel schema uniqueModel of
    Nothing ->
      [UnknownUniqueModel uniqueModel]
    Just model
      | null uniqueFields ->
          [EmptyUniqueConstraint uniqueModel]
      | otherwise ->
          [ UnknownUniqueField uniqueModel wanted
            | wanted <- uniqueFields,
              not (any ((== wanted) . fieldName) (modelFields model))
          ]

requireField :: Text -> Text -> Text -> Model -> [ValidationError]
requireField relName modelName wantedField model =
  [UnknownRelationField relName modelName wantedField | not (any ((== wantedField) . fieldName) (modelFields model))]

duplicates :: (Eq a) => [a] -> [a]
duplicates xs = nub [x | x <- xs, length (filter (== x) xs) > 1]