hydra-0.15.0: src/main/haskell/Hydra/Demos/ValidatePg/Demo.hs
-- | Haskell driver for the PG validation translingual demo.
--
-- Reads a schema JSON file and graph JSON files (produced by GenerateData using
-- hydra.encode.pg.model), validates each graph against the schema using
-- Hydra.Validate.Pg, and prints the results.
--
-- Usage: runhaskell ValidateDemo.hs <data-directory>
module Hydra.Demos.ValidatePg.Demo where
import qualified Hydra.Core as Core
import qualified Hydra.Pg.Model as Pg
import qualified Hydra.Validate.Pg as Validation
import qualified Hydra.Error.Pg as Err
import qualified Hydra.Json.Model as Json
import qualified Data.Aeson as A
import qualified Data.Aeson.KeyMap as AKM
import qualified Data.Aeson.Key as AK
import qualified Data.ByteString.Lazy as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Map as M
import qualified Data.Scientific as SC
import qualified Data.Text as T
import qualified Data.Vector as V
import System.CPUTime (getCPUTime)
import System.Directory (doesFileExist)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO (hFlush, stdout, hPutStrLn, stderr)
main :: IO ()
main = do
args <- getArgs
case args of
[dataDir] -> runDemo dataDir
_ -> do
hPutStrLn stderr "Usage: ValidateDemo <data-directory>"
exitFailure
runDemo :: FilePath -> IO ()
runDemo dataDir = do
-- Load data (I/O, not timed)
schemaJson <- loadJsonFile (dataDir </> "schema.json")
let schema = decodeGraphSchema schemaJson
graphData <- mapM (loadGraph dataDir) graphNames
let loaded = [(n, g) | (n, Just g) <- zip graphNames graphData]
-- Validate (timed: Hydra computation only)
startTime <- getCPUTime
mapM_ (uncurry (printResult schema)) loaded
endTime <- getCPUTime
let elapsedPs = endTime - startTime
elapsedMs = fromIntegral elapsedPs / 1e9 :: Double
hPutStrLn stderr $ "HYDRA_TIME_MS=" ++ show elapsedMs
loadGraph :: FilePath -> String -> IO (Maybe (Pg.Graph Core.Literal))
loadGraph dataDir name = do
let path = dataDir </> name ++ ".json"
exists <- doesFileExist path
if exists
then do
graphJson <- loadJsonFile path
return (Just (decodeGraph graphJson))
else return Nothing
printResult :: Pg.GraphSchema Core.LiteralType -> String -> Pg.Graph Core.Literal -> IO ()
printResult schema name graph = do
let result = Validation.validateGraph checkLiteral schema graph
case result of
Nothing -> log $ "Graph \"" ++ name ++ "\": VALID"
Just err -> log $ "Graph \"" ++ name ++ "\": INVALID - " ++ show err
where
log msg = putStrLn msg >> hFlush stdout
graphNames :: [String]
graphNames =
[ "valid_social_network"
, "missing_required_property"
, "wrong_id_type"
, "unknown_edge_endpoint"
, "unexpected_vertex_label"
, "unexpected_edge_label"
, "property_value_type_mismatch"
, "unexpected_property_key"
, "wrong_in_vertex_label"
, "wrong_out_vertex_label"
, "missing_required_edge_property"
]
-- | Check that a literal value matches a literal type.
checkLiteral :: Core.LiteralType -> Core.Literal -> Maybe Err.InvalidValueError
checkLiteral lt lv = if showLiteralType lt == literalFamily lv
then Nothing
else Just $ Err.InvalidValueError {
Err.invalidValueErrorExpectedType = showLiteralType lt,
Err.invalidValueErrorValue = showLiteral lv }
showLiteralType :: Core.LiteralType -> String
showLiteralType lt = case lt of
Core.LiteralTypeBinary -> "binary"
Core.LiteralTypeBoolean -> "boolean"
Core.LiteralTypeString -> "string"
Core.LiteralTypeFloat ft -> "float:" ++ showFloatType ft
Core.LiteralTypeInteger it -> "integer:" ++ showIntegerType it
showFloatType :: Core.FloatType -> String
showFloatType ft = case ft of
Core.FloatTypeBigfloat -> "bigfloat"
Core.FloatTypeFloat32 -> "float32"
Core.FloatTypeFloat64 -> "float64"
showIntegerType :: Core.IntegerType -> String
showIntegerType it = case it of
Core.IntegerTypeBigint -> "bigint"
Core.IntegerTypeInt8 -> "int8"
Core.IntegerTypeInt16 -> "int16"
Core.IntegerTypeInt32 -> "int32"
Core.IntegerTypeInt64 -> "int64"
Core.IntegerTypeUint8 -> "uint8"
Core.IntegerTypeUint16 -> "uint16"
Core.IntegerTypeUint32 -> "uint32"
Core.IntegerTypeUint64 -> "uint64"
showLiteral :: Core.Literal -> String
showLiteral l = case l of
Core.LiteralBinary _ -> "binary:..."
Core.LiteralBoolean b -> "boolean:" ++ show b
Core.LiteralString s -> "string:\"" ++ s ++ "\""
Core.LiteralFloat fv -> showFloatValue fv
Core.LiteralInteger iv -> showIntegerValue iv
showFloatValue :: Core.FloatValue -> String
showFloatValue fv = case fv of
Core.FloatValueBigfloat d -> "float:bigfloat:" ++ show d
Core.FloatValueFloat32 f -> "float:float32:" ++ show f
Core.FloatValueFloat64 d -> "float:float64:" ++ show d
showIntegerValue :: Core.IntegerValue -> String
showIntegerValue iv = case iv of
Core.IntegerValueBigint n -> "integer:bigint:" ++ show n
Core.IntegerValueInt8 n -> "integer:int8:" ++ show n
Core.IntegerValueInt16 n -> "integer:int16:" ++ show n
Core.IntegerValueInt32 n -> "integer:int32:" ++ show n
Core.IntegerValueInt64 n -> "integer:int64:" ++ show n
Core.IntegerValueUint8 n -> "integer:uint8:" ++ show n
Core.IntegerValueUint16 n -> "integer:uint16:" ++ show n
Core.IntegerValueUint32 n -> "integer:uint32:" ++ show n
Core.IntegerValueUint64 n -> "integer:uint64:" ++ show n
literalFamily :: Core.Literal -> String
literalFamily l = case l of
Core.LiteralBinary _ -> "binary"
Core.LiteralBoolean _ -> "boolean"
Core.LiteralString _ -> "string"
Core.LiteralFloat fv -> "float:" ++ floatFamily fv
Core.LiteralInteger iv -> "integer:" ++ integerFamily iv
where
floatFamily fv = case fv of
Core.FloatValueBigfloat _ -> "bigfloat"
Core.FloatValueFloat32 _ -> "float32"
Core.FloatValueFloat64 _ -> "float64"
integerFamily iv = case iv of
Core.IntegerValueBigint _ -> "bigint"
Core.IntegerValueInt8 _ -> "int8"
Core.IntegerValueInt16 _ -> "int16"
Core.IntegerValueInt32 _ -> "int32"
Core.IntegerValueInt64 _ -> "int64"
Core.IntegerValueUint8 _ -> "uint8"
Core.IntegerValueUint16 _ -> "uint16"
Core.IntegerValueUint32 _ -> "uint32"
Core.IntegerValueUint64 _ -> "uint64"
-- ============================================================================
-- JSON parsing (Aeson → Hydra JSON)
-- ============================================================================
loadJsonFile :: FilePath -> IO Json.Value
loadJsonFile fp = do
content <- BS.readFile fp
case A.eitherDecode content of
Left err -> fail $ "JSON parse error in " ++ fp ++ ": " ++ err
Right v -> return (aesonToHydra v)
aesonToHydra :: A.Value -> Json.Value
aesonToHydra v = case v of
A.Object km -> Json.ValueObject $ M.fromList [(AK.toString k, aesonToHydra v') | (k, v') <- AKM.toList km]
A.Array a -> Json.ValueArray [aesonToHydra x | x <- V.toList a]
A.String t -> Json.ValueString (T.unpack t)
A.Number s -> Json.ValueNumber s
A.Bool b -> Json.ValueBoolean b
A.Null -> Json.ValueNull
-- ============================================================================
-- JSON → PG model decoders
-- ============================================================================
decodeGraphSchema :: Json.Value -> Pg.GraphSchema Core.LiteralType
decodeGraphSchema json = Pg.GraphSchema
(decodeMap (requireField obj "vertices") (Pg.VertexLabel . expectString) decodeVertexType)
(decodeMap (requireField obj "edges") (Pg.EdgeLabel . expectString) decodeEdgeType)
where obj = expectObject json
decodeGraph :: Json.Value -> Pg.Graph Core.Literal
decodeGraph json = Pg.Graph
(decodeMap (requireField obj "vertices") decodeLiteral decodeVertex)
(decodeMap (requireField obj "edges") decodeLiteral decodeEdge)
where obj = expectObject json
decodeVertexType :: Json.Value -> Pg.VertexType Core.LiteralType
decodeVertexType json = Pg.VertexType
(Pg.VertexLabel $ expectString $ requireField obj "label")
(decodeLiteralType $ requireField obj "id")
(decodeList (requireField obj "properties") decodePropertyType)
where obj = expectObject json
decodeEdgeType :: Json.Value -> Pg.EdgeType Core.LiteralType
decodeEdgeType json = Pg.EdgeType
(Pg.EdgeLabel $ expectString $ requireField obj "label")
(decodeLiteralType $ requireField obj "id")
(Pg.VertexLabel $ expectString $ requireField obj "out")
(Pg.VertexLabel $ expectString $ requireField obj "in")
(decodeList (requireField obj "properties") decodePropertyType)
where obj = expectObject json
decodePropertyType :: Json.Value -> Pg.PropertyType Core.LiteralType
decodePropertyType json = Pg.PropertyType
(Pg.PropertyKey $ expectString $ requireField obj "key")
(decodeLiteralType $ requireField obj "value")
(expectBoolean $ requireField obj "required")
where obj = expectObject json
decodeVertex :: Json.Value -> Pg.Vertex Core.Literal
decodeVertex json = Pg.Vertex
(Pg.VertexLabel $ expectString $ requireField obj "label")
(decodeLiteral $ requireField obj "id")
(decodeMap (requireField obj "properties") (Pg.PropertyKey . expectString) decodeLiteral)
where obj = expectObject json
decodeEdge :: Json.Value -> Pg.Edge Core.Literal
decodeEdge json = Pg.Edge
(Pg.EdgeLabel $ expectString $ requireField obj "label")
(decodeLiteral $ requireField obj "id")
(decodeLiteral $ requireField obj "out")
(decodeLiteral $ requireField obj "in")
(decodeMap (requireField obj "properties") (Pg.PropertyKey . expectString) decodeLiteral)
where obj = expectObject json
decodeLiteralType :: Json.Value -> Core.LiteralType
decodeLiteralType json
| hasField obj "binary" = Core.LiteralTypeBinary
| hasField obj "boolean" = Core.LiteralTypeBoolean
| hasField obj "string" = Core.LiteralTypeString
| hasField obj "float" = Core.LiteralTypeFloat $ decodeFloatType (requireField obj "float")
| hasField obj "integer" = Core.LiteralTypeInteger $ decodeIntegerType (requireField obj "integer")
| otherwise = error $ "Unknown literal type"
where obj = expectObject json
decodeFloatType :: Json.Value -> Core.FloatType
decodeFloatType json
| hasField obj "bigfloat" = Core.FloatTypeBigfloat
| hasField obj "float32" = Core.FloatTypeFloat32
| hasField obj "float64" = Core.FloatTypeFloat64
| otherwise = error "Unknown float type"
where obj = expectObject json
decodeIntegerType :: Json.Value -> Core.IntegerType
decodeIntegerType json
| hasField obj "bigint" = Core.IntegerTypeBigint
| hasField obj "int8" = Core.IntegerTypeInt8
| hasField obj "int16" = Core.IntegerTypeInt16
| hasField obj "int32" = Core.IntegerTypeInt32
| hasField obj "int64" = Core.IntegerTypeInt64
| hasField obj "uint8" = Core.IntegerTypeUint8
| hasField obj "uint16" = Core.IntegerTypeUint16
| hasField obj "uint32" = Core.IntegerTypeUint32
| hasField obj "uint64" = Core.IntegerTypeUint64
| otherwise = error "Unknown integer type"
where obj = expectObject json
decodeLiteral :: Json.Value -> Core.Literal
decodeLiteral json
| hasField obj "binary" = Core.LiteralBinary $ BS8.pack $ expectString (requireField obj "binary")
| hasField obj "boolean" = Core.LiteralBoolean $ expectBoolean (requireField obj "boolean")
| hasField obj "string" = Core.LiteralString $ expectString (requireField obj "string")
| hasField obj "float" = Core.LiteralFloat $ decodeFloatValue (requireField obj "float")
| hasField obj "integer" = Core.LiteralInteger $ decodeIntegerValue (requireField obj "integer")
| otherwise = error "Unknown literal"
where obj = expectObject json
decodeFloatValue :: Json.Value -> Core.FloatValue
decodeFloatValue json
| hasField obj "bigfloat" = Core.FloatValueBigfloat $ expectNumber (requireField obj "bigfloat")
| hasField obj "float32" = Core.FloatValueFloat32 $ realToFrac $ expectNumber (requireField obj "float32")
| hasField obj "float64" = Core.FloatValueFloat64 $ expectNumber (requireField obj "float64")
| otherwise = error "Unknown float value"
where obj = expectObject json
decodeIntegerValue :: Json.Value -> Core.IntegerValue
decodeIntegerValue json
| hasField obj "bigint" = Core.IntegerValueBigint $ round $ expectNumber (requireField obj "bigint")
| hasField obj "int8" = Core.IntegerValueInt8 $ round $ expectNumber (requireField obj "int8")
| hasField obj "int16" = Core.IntegerValueInt16 $ round $ expectNumber (requireField obj "int16")
| hasField obj "int32" = Core.IntegerValueInt32 $ round $ expectNumber (requireField obj "int32")
| hasField obj "int64" = Core.IntegerValueInt64 $ round $ expectNumber (requireField obj "int64")
| hasField obj "uint8" = Core.IntegerValueUint8 $ round $ expectNumber (requireField obj "uint8")
| hasField obj "uint16" = Core.IntegerValueUint16 $ round $ expectNumber (requireField obj "uint16")
| hasField obj "uint32" = Core.IntegerValueUint32 $ round $ expectNumber (requireField obj "uint32")
| hasField obj "uint64" = Core.IntegerValueUint64 $ round $ expectNumber (requireField obj "uint64")
| otherwise = error "Unknown integer value"
where obj = expectObject json
-- ============================================================================
-- JSON helpers
-- ============================================================================
decodeMap :: Ord k => Json.Value -> (Json.Value -> k) -> (Json.Value -> v) -> M.Map k v
decodeMap json decodeKey decodeValue = M.fromList $ fmap decodePair entries
where
entries = expectArray json
decodePair entry = (decodeKey (requireField obj "@key"), decodeValue (requireField obj "@value"))
where obj = expectObject entry
decodeList :: Json.Value -> (Json.Value -> a) -> [a]
decodeList json decode = fmap decode (expectArray json)
expectObject :: Json.Value -> M.Map String Json.Value
expectObject (Json.ValueObject m) = m
expectObject _ = error "Expected JSON object"
expectArray :: Json.Value -> [Json.Value]
expectArray (Json.ValueArray a) = a
expectArray _ = error "Expected JSON array"
expectString :: Json.Value -> String
expectString (Json.ValueString s) = s
expectString _ = error "Expected JSON string"
expectBoolean :: Json.Value -> Bool
expectBoolean (Json.ValueBoolean b) = b
expectBoolean _ = error "Expected JSON boolean"
expectNumber :: Json.Value -> Double
expectNumber (Json.ValueNumber n) = SC.toRealFloat n
expectNumber _ = error "Expected JSON number"
requireField :: M.Map String Json.Value -> String -> Json.Value
requireField obj name = case M.lookup name obj of
Just v -> v
Nothing -> error $ "Missing required field: " ++ name
hasField :: M.Map String Json.Value -> String -> Bool
hasField obj name = M.member name obj