packages feed

hydra-0.15.0: src/main/haskell/Hydra/Demos/Genpg/Rdf.hs

-- | RDF/SHACL output for the GenPG demo.
--
-- Provides an alternative output path (N-Triples + SHACL shapes) alongside the
-- existing GraphSON output. Uses the same CSV-to-property-graph pipeline; only
-- the final serialization differs.

module Hydra.Demos.GenPG.Rdf where

import Hydra.Kernel
import Hydra.Demos.GenPG.Examples.Sales.DatabaseSchema
import Hydra.Demos.GenPG.Examples.Sales.GraphSchema
import Hydra.Demos.GenPG.Examples.Sales.Mapping
import Hydra.Demos.GenPG.Examples.Health.DatabaseSchema
import Hydra.Demos.GenPG.Examples.Health.GraphSchema
import Hydra.Demos.GenPG.Examples.Health.Mapping
import Hydra.Demos.GenPG.Demo (transformTables)
import Hydra.Demos.Shacl.ShaclRdf (shapesGraphToNtriples)
import Hydra.Lib.Literals (showInt32)
import qualified Hydra.Pg.Rdf.Environment as PgRdfEnv
import qualified Hydra.Pg.Rdf.Mappings as PgRdfMappings

import qualified Hydra.Rdf.Syntax as Rdf
import qualified Hydra.Shacl.Model as Shacl
import qualified Hydra.Rdf.Serde as Serde
import qualified Hydra.Rdf.Utils as RdfUtils
import qualified Hydra.Pg.Model as Pg

import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as S
import System.IO (hFlush, stdout)


--------------------------------------------------------------------------------
-- Entry points

generateSalesRdf :: IO ()
generateSalesRdf = generateRdf
  "demos/genpg/data/sources/sales"
  salesTableSchemas
  salesGraph
  salesGraphSchema
  "demos/genpg/output/sales"

generateHealthRdf :: IO ()
generateHealthRdf = generateRdf
  "demos/genpg/data/sources/health"
  healthTableSchemas
  healthGraph
  healthGraphSchema
  "demos/genpg/output/health"

generateRdf :: FilePath -> [TableType] -> Pg.LazyGraph Term -> Pg.GraphSchema Type
  -> FilePath -> IO ()
generateRdf sourceRoot tableSchemas graphMapping graphSchema outputDir = do
  log $ "Reading CSV files from " ++ sourceRoot ++ "/"
  g <- transformTables sourceRoot tableSchemas graphMapping

  -- Generate SHACL shapes from the graph schema
  log "Generating SHACL shapes..."
  let shapes = graphSchemaToShapesGraph graphSchema
  let shapesNt = shapesGraphToNtriples shapes
  let shapesFile = outputDir ++ "-shapes.nt"
  writeFile shapesFile (ensureTrailingNewline shapesNt)
  log $ "  Wrote shapes to " ++ shapesFile

  -- Generate RDF data from the property graph
  log "Encoding property graph as RDF..."
  let env = defaultTermEnv
  let dataGraph = PgRdfMappings.encodeLazyGraph env g
  let nVertices = length (Pg.lazyGraphVertices g)
  let nEdges = length (Pg.lazyGraphEdges g)
  let dataNt = Serde.rdfGraphToNtriples dataGraph
  let dataFile = outputDir ++ "-data.nt"
  writeFile dataFile (ensureTrailingNewline dataNt)
  log $ "  Wrote " ++ show nVertices ++ " vertex and "
    ++ show nEdges ++ " edge descriptions to " ++ dataFile

  -- Generate intentionally non-conforming RDF data for negative validation
  log "Generating non-conforming RDF data..."
  let invalidNt = generateInvalidData
  let invalidFile = outputDir ++ "-invalid.nt"
  writeFile invalidFile (ensureTrailingNewline invalidNt)
  log $ "  Wrote to " ++ invalidFile

  log "Done."
  where
    log msg = putStrLn msg >> hFlush stdout
    ensureTrailingNewline s
      | null s = "\n"
      | last s == '\n' = s
      | otherwise = s ++ "\n"


--------------------------------------------------------------------------------
-- Default environment for Term-valued property graphs

-- | A default namespace prefix for the demo
demoNs :: String
demoNs = "urn:hydra:genpg:"

-- | Default environment for encoding Term-valued property graphs as RDF.
defaultTermEnv :: PgRdfEnv.PgRdfEnvironment Term
defaultTermEnv = PgRdfEnv.PgRdfEnvironment {
  PgRdfEnv.pgRdfEnvironmentEncodeVertexId = termToIri "vertex:",
  PgRdfEnv.pgRdfEnvironmentEncodeVertexLabel = \(Pg.VertexLabel l) -> Rdf.Iri (demoNs ++ l),
  PgRdfEnv.pgRdfEnvironmentEncodeEdgeId = termToIri "edge:",
  PgRdfEnv.pgRdfEnvironmentEncodeEdgeLabel = \(Pg.EdgeLabel l) -> Rdf.Iri (demoNs ++ l),
  PgRdfEnv.pgRdfEnvironmentEncodePropertyKey = \(Pg.PropertyKey k) -> Rdf.Iri (demoNs ++ k),
  PgRdfEnv.pgRdfEnvironmentEncodePropertyValue = termToLiteral}

termToIri :: String -> Term -> Rdf.Iri
termToIri prefix term = case term of
  TermLiteral (LiteralString s) -> Rdf.Iri (demoNs ++ prefix ++ s)
  TermLiteral (LiteralInteger (IntegerValueInt32 i)) -> Rdf.Iri (demoNs ++ prefix ++ showInt32 i)
  _ -> error $ "Unsupported term type for IRI encoding: " ++ show term

termToLiteral :: Term -> Rdf.Literal
termToLiteral term = case term of
  TermLiteral lit -> RdfUtils.encodeLiteral lit
  _ -> error $ "Expected a literal term: " ++ show term


--------------------------------------------------------------------------------
-- Invalid data generation for negative validation

-- | Generate intentionally non-conforming RDF data for validation testing.
-- Creates instances that violate the SHACL shapes in specific ways:
--   1. An Employee missing required "firstName" and "lastName"
--   2. A Product with an integer "name" instead of string
--   3. An edge pointing to a non-existent vertex type
generateInvalidData :: String
generateInvalidData = Serde.rdfGraphToNtriples $ Rdf.Graph $ S.fromList $
  -- Employee missing required firstName and lastName
  [ triple "urn:hydra:genpg:vertex:invalid_emp" rdfType "urn:hydra:genpg:Employee"
  , tripleL "urn:hydra:genpg:vertex:invalid_emp" "urn:hydra:genpg:email" "bad@example.com"
  ] ++
  -- Product with integer name instead of string (wrong datatype)
  [ triple "urn:hydra:genpg:vertex:invalid_prod" rdfType "urn:hydra:genpg:Product"
  , tripleInt "urn:hydra:genpg:vertex:invalid_prod" "urn:hydra:genpg:name" 42
  ]
  where
    rdfType = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"

    triple :: String -> String -> String -> Rdf.Triple
    triple s p o = Rdf.Triple
      (Rdf.ResourceIri (Rdf.Iri s))
      (Rdf.Iri p)
      (Rdf.NodeIri (Rdf.Iri o))

    tripleL :: String -> String -> String -> Rdf.Triple
    tripleL s p v = Rdf.Triple
      (Rdf.ResourceIri (Rdf.Iri s))
      (Rdf.Iri p)
      (Rdf.NodeLiteral (Rdf.Literal v (Rdf.Iri "http://www.w3.org/2001/XMLSchema#string") Nothing))

    tripleInt :: String -> String -> Int -> Rdf.Triple
    tripleInt s p n = Rdf.Triple
      (Rdf.ResourceIri (Rdf.Iri s))
      (Rdf.Iri p)
      (Rdf.NodeLiteral (Rdf.Literal (show n) (Rdf.Iri "http://www.w3.org/2001/XMLSchema#integer") Nothing))


--------------------------------------------------------------------------------
-- Schema-to-SHACL conversion (raw Haskell, mirrors the promoted DSL logic)

-- | Convert a GraphSchema Type to a SHACL ShapesGraph.
graphSchemaToShapesGraph :: Pg.GraphSchema Type -> Shacl.ShapesGraph
graphSchemaToShapesGraph schema =
  Shacl.ShapesGraph $ S.fromList $ map makeVertexShape $ M.elems (Pg.graphSchemaVertices schema)
  where
    allEdgeTypes = M.elems (Pg.graphSchemaEdges schema)

    makeVertexShape :: Pg.VertexType Type -> Shacl.Definition Shacl.Shape
    makeVertexShape vt =
      let labelIri = vertexLabelToIri (Pg.vertexTypeLabel vt)
          vlabel = Pg.vertexTypeLabel vt
          propConstraints = map (\pt -> makePropertyConstraint (scopedPropertyIri vlabel $ Pg.propertyTypeKey pt) (makePropertyShape pt)) (Pg.vertexTypeProperties vt)
          edgeConstraints = concatMap (makeEdgeConstraint (Pg.vertexTypeLabel vt)) allEdgeTypes
          allConstraints = S.fromList (propConstraints ++ edgeConstraints)
          common = emptyCommon {
            Shacl.commonPropertiesConstraints = allConstraints,
            Shacl.commonPropertiesTargetClass = S.singleton (Rdf.RdfsClass ())}
      in Shacl.Definition {
        Shacl.definitionIri = labelIri,
        Shacl.definitionTarget = Shacl.ShapeNode $ Shacl.NodeShape common}

    makePropertyShape :: Pg.PropertyType Type -> Shacl.PropertyShape
    makePropertyShape pt =
      let keyIri = propertyKeyToIri (Pg.propertyTypeKey pt)
          dtIri = typeToXsdIri (Pg.propertyTypeValue pt)
          constraints = S.singleton (Shacl.CommonConstraintDatatype dtIri)
          propConstraints = if Pg.propertyTypeRequired pt
            then S.singleton (Shacl.PropertyShapeConstraintMinCount 1)
            else S.empty
      in Shacl.PropertyShape {
        Shacl.propertyShapeCommon = emptyCommon { Shacl.commonPropertiesConstraints = constraints },
        Shacl.propertyShapeConstraints = propConstraints,
        Shacl.propertyShapeDefaultValue = Nothing,
        Shacl.propertyShapeDescription = emptyLangStrings,
        Shacl.propertyShapeName = emptyLangStrings,
        Shacl.propertyShapeOrder = Nothing,
        Shacl.propertyShapePath = keyIri}

    makePropertyConstraint :: Rdf.Iri -> Shacl.PropertyShape -> Shacl.CommonConstraint
    makePropertyConstraint propIri ps = Shacl.CommonConstraintProperty $
      S.singleton (Shacl.ReferenceDefinition $ Shacl.Definition propIri ps)

    makeEdgeConstraint :: Pg.VertexLabel -> Pg.EdgeType Type -> [Shacl.CommonConstraint]
    makeEdgeConstraint vLabel et
      | Pg.edgeTypeOut et == vLabel =
          let edgeIri = edgeLabelToIri (Pg.edgeTypeLabel et)
              inIri = vertexLabelToIri (Pg.edgeTypeIn et)
              classConstraint = Shacl.CommonConstraintClass $ S.singleton (Rdf.RdfsClass ())
              nodeConstraint = Shacl.CommonConstraintNode $
                S.singleton (Shacl.ReferenceNamed inIri)
              common = emptyCommon { Shacl.commonPropertiesConstraints = S.fromList [classConstraint, nodeConstraint] }
              ps = Shacl.PropertyShape {
                Shacl.propertyShapeCommon = common,
                Shacl.propertyShapeConstraints = S.empty,
                Shacl.propertyShapeDefaultValue = Nothing,
                Shacl.propertyShapeDescription = emptyLangStrings,
                Shacl.propertyShapeName = emptyLangStrings,
                Shacl.propertyShapeOrder = Nothing,
                Shacl.propertyShapePath = edgeIri}
          in [makePropertyConstraint (scopedEdgeIri vLabel $ Pg.edgeTypeLabel et) ps]
      | otherwise = []

    emptyCommon :: Shacl.CommonProperties
    emptyCommon = Shacl.CommonProperties {
      Shacl.commonPropertiesConstraints = S.empty,
      Shacl.commonPropertiesDeactivated = Nothing,
      Shacl.commonPropertiesMessage = emptyLangStrings,
      Shacl.commonPropertiesSeverity = Shacl.SeverityViolation,
      Shacl.commonPropertiesTargetClass = S.empty,
      Shacl.commonPropertiesTargetNode = S.empty,
      Shacl.commonPropertiesTargetObjectsOf = S.empty,
      Shacl.commonPropertiesTargetSubjectsOf = S.empty}

    emptyLangStrings :: Rdf.LangStrings
    emptyLangStrings = Rdf.LangStrings M.empty

    vertexLabelToIri :: Pg.VertexLabel -> Rdf.Iri
    vertexLabelToIri (Pg.VertexLabel l) = Rdf.Iri (demoNs ++ l)

    edgeLabelToIri :: Pg.EdgeLabel -> Rdf.Iri
    edgeLabelToIri (Pg.EdgeLabel l) = Rdf.Iri (demoNs ++ l)

    propertyKeyToIri :: Pg.PropertyKey -> Rdf.Iri
    propertyKeyToIri (Pg.PropertyKey k) = Rdf.Iri (demoNs ++ k)

    scopedPropertyIri :: Pg.VertexLabel -> Pg.PropertyKey -> Rdf.Iri
    scopedPropertyIri (Pg.VertexLabel vl) (Pg.PropertyKey k) = Rdf.Iri (demoNs ++ vl ++ "#" ++ k)

    scopedEdgeIri :: Pg.VertexLabel -> Pg.EdgeLabel -> Rdf.Iri
    scopedEdgeIri (Pg.VertexLabel vl) (Pg.EdgeLabel el) = Rdf.Iri (demoNs ++ vl ++ "#" ++ el)

    -- | Map a Hydra Type to an XSD datatype IRI
    typeToXsdIri :: Type -> Rdf.Iri
    typeToXsdIri typ = case typ of
      TypeLiteral lt -> literalTypeToXsd lt
      _ -> Rdf.Iri "http://www.w3.org/2001/XMLSchema#string"  -- fallback

    literalTypeToXsd :: LiteralType -> Rdf.Iri
    literalTypeToXsd lt = Rdf.Iri $ "http://www.w3.org/2001/XMLSchema#" ++ case lt of
      LiteralTypeBinary -> "base64Binary"
      LiteralTypeBoolean -> "boolean"
      LiteralTypeFloat ft -> case ft of
        FloatTypeBigfloat -> "decimal"
        FloatTypeFloat32 -> "float"
        FloatTypeFloat64 -> "double"
      LiteralTypeInteger it -> case it of
        IntegerTypeBigint -> "integer"
        IntegerTypeInt8 -> "byte"
        IntegerTypeInt16 -> "short"
        IntegerTypeInt32 -> "int"
        IntegerTypeInt64 -> "long"
        IntegerTypeUint8 -> "unsignedByte"
        IntegerTypeUint16 -> "unsignedShort"
        IntegerTypeUint32 -> "unsignedInt"
        IntegerTypeUint64 -> "unsignedLong"
      LiteralTypeString -> "string"