packages feed

hydra-0.8.0: src/main/haskell/Hydra/Ext/Pg/TermsToElements.hs

module Hydra.Ext.Pg.TermsToElements (
  decodeValueSpec,
  parseValueSpec,
  termToElementsAdapter,
) where

import Hydra.Kernel
import Hydra.Pg.Mapping
import qualified Hydra.Pg.Model as PG
import qualified Hydra.Dsl.Expect as Expect
import qualified Hydra.Dsl.Terms as Terms

import qualified Control.Monad as CM
import qualified Data.List as L
import qualified Data.List.Split as LS
import qualified Data.Map as M
import qualified Data.Maybe as Y


type PgAdapter s v = Adapter s s Type [PG.Label] Term [PG.Element v]

key_elements = Name "elements"

termToElementsAdapter :: Schema s t v -> Type -> Flow s (PgAdapter s v)
termToElementsAdapter schema typ = do
    case getTypeAnnotation key_elements typ of
      Nothing -> pure trivialAdapter
      Just term -> do
        specs <- Expect.list decodeElementSpec term >>= CM.mapM (parseElementSpec schema)
        let labels = L.nub (fst <$> specs)
        let encoders = snd <$> specs
        let encode term = L.concat <$> CM.mapM (\e -> e term) encoders
        return $ Adapter lossy typ labels $ Coder encode (\els -> noDecoding "element")
  where
    trivialAdapter = Adapter False typ [] $ Coder (\term -> pure []) (\el -> fail "no corresponding element type")

-- TODO; infer lossiness
lossy = False

noDecoding :: String -> Flow s x
noDecoding cat = fail $ cat ++ " decoding is not yet supported"

parseEdgeIdPattern :: Schema s t v -> ValueSpec -> Flow s (Term -> Flow s [v])
parseEdgeIdPattern schema spec = do
  fun <- parseValueSpec spec
  return $ \term -> fun term >>= CM.mapM (coderEncode $ schemaEdgeIds schema)

parseEdgeSpec :: Schema s t v -> EdgeSpec -> Flow s (PG.Label, Term -> Flow s [PG.Element v])
parseEdgeSpec schema (EdgeSpec label id outV inV props) = do
  getId <- parseEdgeIdPattern schema id
  getOut <- parseVertexIdPattern schema outV
  getIn <- parseVertexIdPattern schema inV
  getProps <- CM.mapM (parsePropertySpec schema) props
  let encode term = withTrace "encode as edge" $ do
        tid <- requireUnique "edge id" getId term
        tout <- requireUnique "vertex id" getOut term
        tin <- requireUnique "edge id" getIn term
        tprops <- M.fromList <$> CM.mapM (\g -> requireUnique "property key" g term) getProps
        return [PG.ElementEdge $ PG.Edge label tid tout tin tprops]
  return (PG.LabelEdge label, encode)

parseElementSpec :: Schema s t v -> ElementSpec -> Flow s (PG.Label, Term -> Flow s [PG.Element v])
parseElementSpec schema spec = case spec of
  ElementSpecVertex vspec -> parseVertexSpec schema vspec
  ElementSpecEdge espec -> parseEdgeSpec schema espec

parsePattern :: String -> Flow s (Term -> Flow s [Term])
parsePattern pat = withTrace "parse path pattern" $ do
    (lits, paths) <- parsePattern [] [] "" pat
    return $ traverse lits paths
  where
    parsePattern lits paths cur s = case s of
      [] -> pure (L.reverse (nextLit:lits), L.reverse paths)
      ('$':'{':rest) -> parsePath (nextLit:lits) paths "" rest
      (c:rest) -> parsePattern lits paths (c:cur) rest
      where
        nextLit = L.reverse cur
        parsePath lits paths cur s = case s of
          [] -> fail $ "Unfinished path expression: " ++ pat
          ('}':rest) -> parsePattern lits (path:paths) "" rest
            where
              path = LS.splitOn "/" $ L.reverse cur
          (c:rest) -> parsePath lits paths (c:cur) rest
    traverse lits paths term = withTrace ("traverse pattern: " ++ pat) $ recurse [""] True lits paths
      where
        recurse values lp lits paths = if L.null values
            then pure []
            else if lp
            -- Try to apply a literal
            then case lits of
              -- All done. The last segment is always a literal.
              [] -> return $ Terms.string <$> values
              -- Append the literal and continue traversing.
              (l:rest) -> recurse (append l) False rest paths
            -- Try to apply a path
            else case paths of
              -- No more paths; continue with literals
              [] -> recurse values True lits []
              -- Apply the next path
              (path:rest) -> do
                  strings <- evalPath path term >>= CM.mapM toString
                  recurse (appendAll strings) True lits rest
          where
            append s = fmap (\v -> v ++ s) values
            appendAll strings = L.concat (append <$> strings)
    evalPath path term = case path of
        [] -> pure [term]
        (step:rest) -> do
          results <- evalStep step term
          L.concat <$> (CM.mapM (evalPath rest) results)
      where
        evalStep step term = if L.null step
          then pure [term]
          else case stripTerm term of
              TermList terms -> L.concat <$> CM.mapM (evalStep step) terms
              TermOptional mt -> case mt of
                Nothing -> pure []
                Just term' -> evalStep step term'
              TermRecord (Record _ fields) -> case M.lookup (Name step) (fieldMap fields) of
                Nothing -> fail $ "No such field " ++ step ++ " in record: " ++ show term
                Just term' -> pure [term']
              TermUnion (Injection _ field) -> if unName (fieldName field) == step
                then evalStep step $ fieldTerm field
                else pure [] -- Note: not checking the step against the union type; assuming it is correct but that it references a field unused by the injection
              TermWrap (WrappedTerm _ term') -> evalStep step term'
              _ -> fail $ "Can't traverse through term for step " ++ show step ++ ": " ++ show term

    -- TODO: replace this with a more standard function
    toString term = case stripTerm term of
      TermLiteral lit -> pure $ case lit of
        LiteralBinary b -> b
        LiteralBoolean b -> show b
        LiteralInteger i -> case i of
          IntegerValueBigint v -> show v
          IntegerValueInt8 v -> show v
          IntegerValueInt16 v -> show v
          IntegerValueInt32 v -> show v
          IntegerValueInt64 v -> show v
          IntegerValueUint8 v -> show v
          IntegerValueUint16 v -> show v
          IntegerValueUint32 v -> show v
          IntegerValueUint64 v -> show v
        LiteralFloat f -> case f of
          FloatValueBigfloat v -> show v
          FloatValueFloat32 v -> show v
          FloatValueFloat64 v -> show v
        LiteralString s -> s
      TermOptional mt -> case mt of
        Nothing -> pure "nothing"
        Just t -> toString t
      _ -> pure $ show term

parsePropertySpec :: Schema s t v -> PropertySpec -> Flow s (Term -> Flow s [(PG.PropertyKey, v)])
parsePropertySpec schema (PropertySpec key value) = withTrace "parse property spec" $ do
  fun <- parseValueSpec value
  return $ \term -> withTrace ("encode property " ++ PG.unPropertyKey key) $ do
    results <- fun term
    values <- CM.mapM (coderEncode $ schemaPropertyValues schema) results
    return $ fmap (\v -> (key, v)) values

parseValueSpec :: ValueSpec -> Flow s (Term -> Flow s [Term])
parseValueSpec spec = case spec of
  ValueSpecValue -> pure $ \term -> pure [term]
  ValueSpecPattern pat -> parsePattern pat

parseVertexIdPattern :: Schema s t v -> ValueSpec -> Flow s (Term -> Flow s [v])
parseVertexIdPattern schema spec = do
  fun <- parseValueSpec spec
  return $ \term -> fun term >>= CM.mapM (coderEncode $ schemaVertexIds schema)

parseVertexSpec :: Schema s t v -> VertexSpec -> Flow s (PG.Label, Term -> Flow s [PG.Element v])
parseVertexSpec schema (VertexSpec label id props) = do
  getId <- parseVertexIdPattern schema id
  getProps <- CM.mapM (parsePropertySpec schema) props
  let encode term = withTrace "encode as vertex" $ do
        tid <- requireUnique "vertex id" getId term
        tprops <- M.fromList <$> CM.mapM (\g -> requireUnique "property key" g term) getProps
        return [PG.ElementVertex $ PG.Vertex label tid tprops]
  return (PG.LabelVertex label, encode)

requireUnique :: String -> (Term -> Flow s [x]) -> Term -> Flow s x
requireUnique context fun term = do
  results <- fun term
  case results of
    [] -> fail $ "No value found: " ++ context
    [value] -> pure value
    _ -> fail $ "Multiple values found: " ++ context


-- Element spec decoding. TODO: this should code should really be generated rather than hand-written.

decodeEdgeLabel :: Term -> Flow s PG.EdgeLabel
decodeEdgeLabel t = PG.EdgeLabel <$> Expect.string t

decodeEdgeSpec :: Term -> Flow s EdgeSpec
decodeEdgeSpec term = withTrace "decode edge spec" $ matchRecord (\fields -> EdgeSpec
  <$> readField fields _EdgeSpec_label decodeEdgeLabel
  <*> readField fields _EdgeSpec_id decodeValueSpec
  <*> readField fields _EdgeSpec_out decodeValueSpec
  <*> readField fields _EdgeSpec_in decodeValueSpec
  <*> readField fields _EdgeSpec_properties (Expect.list decodePropertySpec)) term

decodeElementSpec :: Term -> Flow s ElementSpec
decodeElementSpec term = withTrace "decode element spec" $ matchInjection [
  (_ElementSpec_vertex, \t -> ElementSpecVertex <$> decodeVertexSpec t),
  (_ElementSpec_edge, \t -> ElementSpecEdge <$> decodeEdgeSpec t)] term

decodePropertyKey :: Term -> Flow s PG.PropertyKey
decodePropertyKey t = PG.PropertyKey <$> Expect.string t

decodePropertySpec :: Term -> Flow s PropertySpec
decodePropertySpec term = withTrace "decode property spec" $ matchRecord (\fields -> PropertySpec
  <$> readField fields _PropertySpec_key decodePropertyKey
  <*> readField fields _PropertySpec_value decodeValueSpec) term

decodeValueSpec :: Term -> Flow s ValueSpec
decodeValueSpec term = withTrace "decode value spec" $ case stripTerm term of
  -- Allow an abbreviated specification consisting of only the pattern string
  TermLiteral (LiteralString s) -> pure $ ValueSpecPattern s
  _ -> matchInjection [
    (_ValueSpec_value, \t -> pure ValueSpecValue),
    (_ValueSpec_pattern, \t -> ValueSpecPattern <$> Expect.string t)] term

decodeVertexLabel :: Term -> Flow s PG.VertexLabel
decodeVertexLabel t = PG.VertexLabel <$> Expect.string t

decodeVertexSpec :: Term -> Flow s VertexSpec
decodeVertexSpec term = withTrace "decode vertex spec" $ matchRecord (\fields -> VertexSpec
  <$> readField fields _VertexSpec_label decodeVertexLabel
  <*> readField fields _VertexSpec_id decodeValueSpec
  <*> readField fields _VertexSpec_properties (Expect.list decodePropertySpec)) term


-- General-purpose code for decoding

matchInjection :: [(Name, Term -> Flow s x)] -> Term -> Flow s x
matchInjection cases encoded = do
  mp <- Expect.map (\k -> Name <$> Expect.string k) pure encoded
  f <- case M.toList mp of
    [] -> fail "empty injection"
    [(k, v)] -> pure $ Field k v
    _ -> fail $ "invalid injection: " ++ show mp
  case snd <$> (L.filter (\c -> fst c == fieldName f) cases) of
    [] -> fail $ "unexpected field: " ++ unName (fieldName f)
    [fun] -> fun (fieldTerm f)
    _ -> fail "duplicate field name in cases"

matchRecord :: (M.Map Name Term -> Flow s x) -> Term -> Flow s x
matchRecord cons term = Expect.map (\k -> Name <$> Expect.string k) pure term >>= cons

readField fields fname fun = case M.lookup fname fields of
  Nothing -> fail $ "no such field: " ++ unName fname
  Just t -> fun t