packages feed

rdf4h-5.2.0: src/Data/RDF/Vocabulary/Generator/VocabularyGenerator.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.RDF.Vocabulary.Generator.VocabularyGenerator
  ( genVocabulary,
  )
where

-- import Control.Monad (join)
import Data.Char (isLower)
import Data.List (nub, sortBy)
import qualified Data.Map as M
import Data.Maybe (maybeToList)
import Data.RDF
  ( AdjHashMap,
    LValue (..),
    Node (..),
    PrefixMappings (PrefixMappings),
    RDF,
    Rdf,
    TurtleParser (TurtleParser),
    objectOf,
    parseFile,
    prefixMappings,
    query,
    subjectOf,
    triplesOf,
  )
import Data.Text (Text)
import qualified Data.Text as T
import Language.Haskell.TH

-- | Generates 'Node' values for concepts and properties, and
-- 'Namespace' values, for a given schema in the Haskell module in
-- which 'genVocabulary' is used.
--
-- Concepts in the schema are prepended with "_", the names of
-- properties are unchanged.
--
-- For example:
--
-- >>> $(genVocabulary "resources/shacl.ttl")
--
-- creates many 'Node' values including
--
-- @
--     _SPARQLConstraint  :: Node
--     annotationProperty :: Node
-- @
--
-- This is used to auto-generate all modules in Data.RDF.Vocabulary.* at
-- compile time with Template Haskell.
genVocabulary ::
  -- | the filepath of the file containing the schema in RDF Turtle format.
  String ->
  Q [Dec]
genVocabulary file = runIO (loadGraph file) >>= vocabulary

loadGraph :: String -> IO (RDF AdjHashMap)
loadGraph file =
  parseFile (TurtleParser Nothing Nothing) file >>= \case
    Left err -> error $ show err
    Right rdfGraph -> return rdfGraph

vocabulary :: (Rdf a) => RDF a -> Q [Dec]
vocabulary graph =
  let nameDecls = do
        subject <- nub $ subjectOf <$> triplesOf graph
        iri <- maybeToList $ toIRI subject
        name <- maybeToList $ iriToName iri
        let comment =
              combineComments
                . traverse (nodeToComment . objectOf)
                $ query graph (Just subject) (Just rdfsCommentNode) Nothing
        return (name, declareIRI name iri comment)
      (PrefixMappings prefixMappings') = prefixMappings graph
      namespaceDecls = do
        (prefix, iri) <- M.toList prefixMappings'
        let name = mkName . T.unpack . escape $ prefix <> "NS"
        return $ declarePrefix name prefix iri
      iriDecls = fmap snd . sortBy (\x y -> fst y `compare` fst x) $ nameDecls
      irisDecl = declareIRIs $ fst <$> nameDecls
   in sequence $ irisDecl : namespaceDecls <> iriDecls

toIRI :: Node -> Maybe Text
toIRI (UNode iri) = Just iri
toIRI _ = Nothing

packFun :: Exp
packFun = VarE $ mkName "Data.Text.pack"

unodeFun :: Exp
unodeFun = VarE $ mkName "Data.RDF.Types.unode"

mkPrefixedNSFun :: Exp
mkPrefixedNSFun = VarE $ mkName "Data.RDF.Namespace.mkPrefixedNS"

nodeToComment :: Node -> Maybe Text
nodeToComment (UNode uri) = Just $ "See \\<<" <> uri <> ">\\>."
nodeToComment (BNode _) = Nothing
nodeToComment (BNodeGen _) = Nothing
nodeToComment (LNode (PlainL l)) = Just l
nodeToComment (LNode (PlainLL l _)) = Just l
nodeToComment (LNode (TypedL l _)) = Just l

combineComments :: Maybe [Text] -> Maybe Text
combineComments = (combineComments' =<<)
  where
    combineComments' [] = Nothing
    combineComments' comments = Just . T.intercalate "\n" $ comments

rdfsCommentNode :: Node
rdfsCommentNode = UNode "http://www.w3.org/2000/01/rdf-schema#comment"

declareIRI :: Name -> Text -> Maybe Text -> Q Dec
declareIRI name iri comment =
  let iriLiteral = LitE . StringL $ T.unpack iri
      unodeLiteral = AppE unodeFun $ AppE packFun iriLiteral
   in funD_doc
        name
        [return $ Clause [] (NormalB unodeLiteral) []]
        (T.unpack <$> comment)
        [Nothing]

declareIRIs :: [Name] -> Q Dec
declareIRIs names =
  let iriList = ListE (VarE <$> names)
   in funD_doc
        (mkName "iris")
        [return $ Clause [] (NormalB iriList) []]
        (Just "All IRIs in this vocabulary.")
        [Nothing]

-- namespace = mkPrefixedNS "ogit" "http://www.purl.org/ogit/"
declarePrefix :: Name -> Text -> Text -> Q Dec
declarePrefix name prefix iri =
  let prefixLiteral = AppE packFun . LitE . StringL . T.unpack $ prefix
      iriLiteral = AppE packFun . LitE . StringL . T.unpack $ iri
      namespace = AppE (AppE mkPrefixedNSFun prefixLiteral) iriLiteral
   in funD_doc
        name
        [return $ Clause [] (NormalB namespace) []]
        (Just $ "Namespace prefix for \\<<" <> T.unpack iri <> ">\\>.")
        [Nothing]

iriToName :: Text -> Maybe Name
iriToName iri = mkName . T.unpack . escape <$> (lastMay . filter (not . T.null) . T.split (`elem` separators)) iri
  where
    separators = ['/', '#']
    lastMay :: [a] -> Maybe a
    lastMay [] = Nothing
    lastMay xs = Just (last xs)

escape :: Text -> Text
escape name = escapeKeywords $ T.map escapeOperators name
  where
    escapeOperators c | c `elem` operators = escapeChar
    escapeOperators c = c
    escapeKeywords name' | not (isLower $ T.head name') = escapeChar `T.cons` name'
    escapeKeywords name' | name' `elem` keywords = escapeChar `T.cons` name'
    escapeKeywords name' = name'
    operators = ['!', '#', '$', '%', '&', '*', '+', '.', '/', '<', '=', '>', '?', '@', '\\', '^', '|', '-', '~']
    keywords =
      [ "as",
        "case",
        "of",
        "class",
        "data",
        "data family",
        "data instance",
        "default",
        "deriving",
        "deriving instance",
        "do",
        "forall",
        "foreign",
        "hiding",
        "if",
        "then",
        "else",
        "import",
        "infix",
        "infixl",
        "infixr",
        "instance",
        "let",
        "in",
        "mdo",
        "module",
        "newtype",
        "proc",
        "qualified",
        "rec",
        "type",
        "type family",
        "type instance",
        "where"
      ]
    escapeChar = '_'