hydra-0.8.0: src/main/haskell/Hydra/Ext/Pg/Coder.hs
module Hydra.Ext.Pg.Coder (
elementCoder,
) where
import Hydra.Kernel
import Hydra.Pg.Mapping
import Hydra.Ext.Pg.TermsToElements
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.Map as M
import qualified Data.Maybe as Y
type ElementAdapter s t v = Adapter s s (Type) (PG.ElementTypeTree t) (Term) (PG.ElementTree v)
type PropertyAdapter s t v = Adapter s s (FieldType) (PG.PropertyType t) (Field) (PG.Property v)
type IdAdapter s t v = (Name, Adapter s s (Type) t (Term) v)
data AdjacentEdgeAdapter s a t v = AdjacentEdgeAdapter {
adjacentEdgeAdapterDirection :: PG.Direction,
adjacentEdgeAdapterField :: FieldType,
adjacentEdgeAdapterLabel :: PG.EdgeLabel,
adjacentEdgeAdapterAdapter :: ElementAdapter s t v}
data ProjectionSpec a = ProjectionSpec {
projectionSpecField :: FieldType,
projectionSpecValues :: ValueSpec,
projectionSpecAlias :: Maybe String}
-- TODO: deprecate "kind"
key_kind = Name "kind"
check :: Bool -> Flow s () -> Flow s ()
check b err = if b then pure () else err
checkRecordName expected actual = check (actual == expected) $
unexpected ("record of type " ++ unName expected) ("record of type " ++ unName actual)
edgeCoder :: PG.Direction -> Schema s t v
-> Type
-> t
-> Name
-> PG.EdgeLabel -> PG.VertexLabel -> PG.VertexLabel
-> Maybe (IdAdapter s t v) -> Maybe (IdAdapter s t v) -> Maybe (IdAdapter s t v) -> [PropertyAdapter s t v]
-> ElementAdapter s t v
edgeCoder dir schema source eidType tname label outLabel inLabel mIdAdapter outAdapter inAdapter propAdapters
= Adapter lossy source (elementTypeTreeEdge et []) coder
where
et = PG.EdgeType label eidType outLabel inLabel $ propertyTypes propAdapters
coder = Coder encode decode
where
encode term = case stripTerm term of
TermOptional (Just ot) -> encode ot
TermRecord (Record tname' fields) -> do
checkRecordName tname tname'
let fieldsm = fieldMap fields
id <- case mIdAdapter of
Nothing -> pure $ schemaDefaultEdgeId schema
Just ad -> selectEdgeId fieldsm ad
props <- encodeProperties fieldsm propAdapters
outId <- getVertexId PG.DirectionOut fieldsm outAdapter
inId <- getVertexId PG.DirectionIn fieldsm inAdapter
return $ elementTreeEdge (PG.Edge label id outId inId props) []
_ -> unexpected "record (1)" $ show term
decode el = noDecoding "edge"
getVertexId dir1 fieldsm adapter = if dir1 == dir
then pure $ schemaDefaultVertexId schema
else case adapter of
Nothing -> fail $ "no adapter for " ++ show dir1 ++ " with " ++ show dir
Just ad -> selectVertexId fieldsm ad
elementCoder :: (Show t, Show v) => Y.Maybe (PG.Direction, PG.VertexLabel)
-> Schema s t v
-> Type
-> t -> t
-> Flow s (ElementAdapter s t v)
elementCoder mparent schema source vidType eidType = case stripType source of
TypeOptional ot -> elementCoder mparent schema ot vidType eidType
TypeRecord (RowType name fields) -> withTrace ("adapter for " ++ unName name) $ do
mOutSpec <- findProjectionSpec name outVertexKey outVertexLabelKey fields
mInSpec <- findProjectionSpec name inVertexKey inVertexLabelKey fields
kind <- case getTypeAnnotation key_kind source of
Nothing -> pure $ if hasVertexAdapters mOutSpec mInSpec
then PG.ElementKindEdge
else PG.ElementKindVertex
Just kindTerm -> do
s <- Expect.string kindTerm
case s of
"vertex" -> return PG.ElementKindVertex
"edge" -> if Y.isNothing mOutSpec || Y.isNothing mInSpec
then fail $ "Record type marked as an edge type, but missing 'out' and/or 'in' fields: " ++ unName name
else return PG.ElementKindEdge
propSpecs <- findPropertySpecs kind fields
propAdapters <- CM.mapM (propertyAdapter schema) propSpecs
case kind of
PG.ElementKindVertex -> do
label <- PG.VertexLabel <$> findLabelString name vertexLabelKey
idAdapter <- vertexIdAdapter name vertexIdKey fields
outEdgeAdapters <- edgeAdapters label PG.DirectionOut fields
inEdgeAdapters <- edgeAdapters label PG.DirectionIn fields
return $ vertexCoder schema source vidType name label idAdapter propAdapters (outEdgeAdapters ++ inEdgeAdapters)
PG.ElementKindEdge -> do
label <- PG.EdgeLabel <$> findLabelString name edgeLabelKey
idAdapter <- edgeIdAdapter name edgeIdKey fields
outAdapter <- Y.maybe (pure Nothing) (\s -> Just <$> projectionAdapter vidType (schemaVertexIds schema) s "out") mOutSpec
inAdapter <- Y.maybe (pure Nothing) (\s -> Just <$> projectionAdapter vidType (schemaVertexIds schema) s "in") mInSpec
outLabel <- case mOutSpec of
Nothing -> pure parentLabel
Just spec -> Y.maybe (fail "no out-vertex label") (pure . PG.VertexLabel) $ projectionSpecAlias spec
inLabel <- case mInSpec of
Nothing -> pure parentLabel
Just spec -> Y.maybe (fail "no in-vertex label") (pure . PG.VertexLabel) $ projectionSpecAlias spec
return $ edgeCoder dir schema source eidType name label outLabel inLabel idAdapter outAdapter inAdapter propAdapters
_ -> unexpected "record type" $ show source
where
dir = Y.maybe PG.DirectionBoth fst mparent
parentLabel = Y.maybe (PG.VertexLabel "NOLABEL") snd mparent
vertexIdAdapter name idKey fields = do
idSpec <- Y.fromJust <$> findId True name idKey fields
idAdapter <- projectionAdapter vidType (schemaVertexIds schema) idSpec "id"
return idAdapter
edgeIdAdapter name idKey fields = do
mIdSpec <- findId False name idKey fields
case mIdSpec of
Nothing -> pure Nothing
Just idSpec -> Just <$> projectionAdapter eidType (schemaEdgeIds schema) idSpec "id"
hasVertexAdapters mOutSpec mInSpec = case dir of
PG.DirectionOut -> Y.isJust mInSpec
PG.DirectionIn -> Y.isJust mOutSpec
PG.DirectionBoth -> Y.isJust mOutSpec && Y.isJust mInSpec
vertexLabelKey = Name $ annotationSchemaVertexLabel $ schemaAnnotations schema
edgeLabelKey = Name $ annotationSchemaEdgeLabel $ schemaAnnotations schema
vertexIdKey = Name $ annotationSchemaVertexId $ schemaAnnotations schema
edgeIdKey = Name $ annotationSchemaEdgeId $ schemaAnnotations schema
propertyKeyKey = Name $ annotationSchemaPropertyKey $ schemaAnnotations schema
propertyValueKey = Name $ annotationSchemaPropertyValue $ schemaAnnotations schema
outVertexKey = Name $ annotationSchemaOutVertex $ schemaAnnotations schema
outVertexLabelKey = Name $ annotationSchemaOutVertexLabel $ schemaAnnotations schema
inVertexKey = Name $ annotationSchemaInVertex $ schemaAnnotations schema
inVertexLabelKey = Name $ annotationSchemaInVertexLabel $ schemaAnnotations schema
outEdgeLabelKey = Name $ annotationSchemaOutEdgeLabel $ schemaAnnotations schema
inEdgeLabelKey = Name $ annotationSchemaInEdgeLabel $ schemaAnnotations schema
ignoreKey = Name $ annotationSchemaIgnore $ schemaAnnotations schema
findLabelString tname labelKey = case getTypeAnnotation labelKey source of
Nothing -> pure $ unName tname
Just labelTerm -> Expect.string labelTerm
findId required tname idKey fields = withTrace "find id field" $ do
mid <- findField tname idKey fields
case mid of
Nothing -> if required
then fail $ "no " ++ unName idKey ++ " field"
else pure Nothing
Just mi -> do
spec <- case getTypeAnnotation idKey (fieldTypeType mi) of
Nothing -> pure ValueSpecValue
Just t -> decodeValueSpec t
return $ Just $ ProjectionSpec mi spec Nothing
findProjectionSpec tname key aliasKey fields = withTrace ("find " ++ show key ++ " projection") $ do
mfield <- findField tname key fields
case mfield of
Nothing -> pure Nothing
Just field -> do
spec <- decodeValueSpec $ Y.fromJust $ getTypeAnnotation key $ fieldTypeType field
alias <- case getTypeAnnotation aliasKey $ fieldTypeType field of
Nothing -> pure Nothing
Just t -> Just <$> Expect.string t
return $ Just $ ProjectionSpec field spec alias
findField tname key fields = withTrace ("find " ++ show key ++ " field") $ do
let matches = L.filter (\f -> Y.isJust $ getTypeAnnotation key $ fieldTypeType f) fields
if L.length matches > 1
then fail $ "Multiple fields marked as '" ++ unName key ++ "' in record type " ++ unName tname ++ ": "
++ (L.intercalate ", " (unName . fieldTypeName <$> matches))
else return $ if L.null matches then Nothing else Just $ L.head matches
findPropertySpecs kind fields = CM.mapM toSpec $ L.filter isPropField fields
where
isPropField field = not (hasSpecialAnnotation || hasSpecialFieldName)
where
hasSpecialAnnotation = L.foldl (\b k -> b || hasAnnotation k) False (ignoreKey:specialKeys)
hasSpecialFieldName = L.foldl (\b n -> b || hasName n) False specialKeys
specialKeys = case kind of
PG.ElementKindVertex -> [vertexIdKey, outEdgeLabelKey, inEdgeLabelKey]
PG.ElementKindEdge -> [edgeIdKey, outVertexKey, inVertexKey]
hasAnnotation key = Y.isJust $ getTypeAnnotation key $ fieldTypeType field
hasName fname = fieldTypeName field == fname
toSpec field = do
alias <- case (getTypeAnnotation propertyKeyKey $ fieldTypeType field) of
Nothing -> pure Nothing
Just a -> Just <$> Expect.string a
values <- case (getTypeAnnotation propertyValueKey $ fieldTypeType field) of
Nothing -> pure ValueSpecValue
Just sp -> decodeValueSpec sp
return $ ProjectionSpec field values alias
edgeAdapters vlabel dir fields = Y.catMaybes <$> CM.mapM toSpec fields
where
toSpec field = case getTypeAnnotation key (fieldTypeType field) of
Nothing -> pure Nothing
Just a -> do
label <- PG.EdgeLabel <$> Expect.string a
elad <- elementCoder (Just (dir, vlabel)) schema (fieldTypeType field) vidType eidType
return $ Just $ AdjacentEdgeAdapter dir field label elad
key = case dir of
PG.DirectionOut -> outEdgeLabelKey
PG.DirectionIn -> inEdgeLabelKey
elementTreeEdge :: PG.Edge v -> [PG.ElementTree v] -> PG.ElementTree v
elementTreeEdge edge = PG.ElementTree (PG.ElementEdge edge)
elementTreeVertex :: PG.Vertex v -> [PG.ElementTree v] -> PG.ElementTree v
elementTreeVertex vertex = PG.ElementTree (PG.ElementVertex vertex)
elementTypeTreeEdge :: PG.EdgeType t -> [PG.ElementTypeTree t] -> PG.ElementTypeTree t
elementTypeTreeEdge etype = PG.ElementTypeTree (PG.ElementTypeEdge etype)
elementTypeTreeVertex :: PG.VertexType t -> [PG.ElementTypeTree t] -> PG.ElementTypeTree t
elementTypeTreeVertex vtype = PG.ElementTypeTree (PG.ElementTypeVertex vtype)
encodeProperties :: M.Map Name (Term) -> [PropertyAdapter s t v] -> Flow s (M.Map PG.PropertyKey v)
encodeProperties fields adapters = do
props <- Y.catMaybes <$> CM.mapM (encodeProperty fields) adapters
return $ M.fromList $ fmap (\(PG.Property key val) -> (key, val)) props
encodeProperty :: M.Map Name (Term) -> PropertyAdapter s t v -> Flow s (Maybe (PG.Property v))
encodeProperty fields adapter = do
case M.lookup fname fields of
Nothing -> case ftyp of
TypeOptional _ -> pure Nothing
_ -> fail $ "expected field not found in record: " ++ unName fname
Just value -> case ftyp of
TypeOptional _ -> case fullyStripTerm value of
TermOptional ov -> case ov of
Nothing -> pure Nothing
Just v -> Just <$> encodeValue v
_ -> unexpected "optional term" $ show value
_ -> Just <$> encodeValue value
where
fname = fieldTypeName $ adapterSource adapter
ftyp = stripType (fieldTypeType $ adapterSource adapter)
encodeValue v = coderEncode (adapterCoder adapter) (Field fname v)
-- TODO; infer lossiness
lossy = True
noDecoding :: String -> Flow s x
noDecoding cat = fail $ cat ++ " decoding is not yet supported"
projectionAdapter :: t
-> Coder s s (Term) v
-> ProjectionSpec a
-> String
-> Flow s (IdAdapter s t v)
projectionAdapter idtype coder spec key = do
traversal <- parseValueSpec $ projectionSpecValues spec
let field = projectionSpecField spec
let encode = \typ -> traverseToSingleTerm (key ++ "-projection") traversal typ >>= coderEncode coder
return (fieldTypeName field, Adapter lossy (fieldTypeType field) idtype $ Coder encode decode)
where
decode _ = noDecoding $ "edge '" ++ key ++ "'"
propertyAdapter :: Schema s t v -> ProjectionSpec a -> Flow s (PropertyAdapter s t v)
propertyAdapter schema (ProjectionSpec tfield values alias) = do
let key = PG.PropertyKey $ case alias of
Nothing -> unName $ fieldTypeName tfield
Just k -> k
pt <- coderEncode (schemaPropertyTypes schema) $ fieldTypeType tfield
traversal <- parseValueSpec values
let coder = Coder encode decode
where
encode dfield = withTrace ("encode property field " ++ show (unName $ fieldTypeName tfield)) $ do
if fieldName dfield /= fieldTypeName tfield
then unexpected ("field '" ++ unName (fieldTypeName tfield) ++ "'") $ show dfield
else do
result <- traverseToSingleTerm "property traversal" traversal $ fieldTerm dfield
value <- coderEncode (schemaPropertyValues schema) result
return $ PG.Property key value
decode _ = noDecoding "property"
return $ Adapter lossy tfield (PG.PropertyType key pt True) coder
propertyTypes propAdapters = toPropertyType <$> propAdapters
where
toPropertyType a = PG.PropertyType (PG.propertyTypeKey $ adapterTarget a) (PG.propertyTypeValue $ adapterTarget a) True
selectEdgeId fields (fname, ad) = case M.lookup fname fields of
Nothing -> fail $ "no " ++ unName fname ++ " in record"
Just t -> coderEncode (adapterCoder ad) t
selectVertexId :: M.Map Name (Term) -> IdAdapter s t v -> Flow s v
selectVertexId fields (fname, ad) = case M.lookup fname fields of
Nothing -> fail $ "no " ++ unName fname ++ " in record"
Just t -> coderEncode (adapterCoder ad) t
traverseToSingleTerm :: String -> (Term -> Flow s [Term]) -> Term -> Flow s (Term)
traverseToSingleTerm desc traversal term = do
terms <- traversal term
case terms of
[] -> fail $ desc ++ " did not resolve to a term"
[t] -> pure t
_ -> fail $ desc ++ " resolved to multiple terms"
vertexCoder :: (Show t, Show v)
=> Schema s t v
-> Type
-> t
-> Name
-> PG.VertexLabel -> IdAdapter s t v -> [PropertyAdapter s t v]
-> [AdjacentEdgeAdapter s a t v]
-> ElementAdapter s t v
vertexCoder schema source vidType tname label idAdapter propAdapters edgeAdapters = Adapter lossy source target coder
where
target = elementTypeTreeVertex vtype depTypes
vtype = PG.VertexType label vidType $ propertyTypes propAdapters
depTypes = adapterTarget . adjacentEdgeAdapterAdapter <$> edgeAdapters
coder = Coder encode decode
where
encode term = case stripTerm term of
TermOptional (Just ot) -> encode ot
TermRecord (Record tname' fields) -> do
checkRecordName tname tname'
let fieldsm = fieldMap fields
vid <- selectVertexId fieldsm idAdapter
props <- encodeProperties (fieldMap fields) propAdapters
deps <- Y.catMaybes <$> CM.mapM (findDeps vid fieldsm) edgeAdapters
return $ elementTreeVertex (PG.Vertex label vid props) deps
_ -> unexpected "record (2)" $ show term
where
findDeps vid fieldsm (AdjacentEdgeAdapter dir field label ad) = do
case M.lookup (fieldTypeName field) fieldsm of
Nothing -> pure Nothing
Just fterm -> Just <$> (coderEncode (adapterCoder ad) fterm >>= fixTree)
where
fixTree tree = case PG.elementTreeSelf tree of
PG.ElementEdge e -> pure $ tree {PG.elementTreeSelf = PG.ElementEdge $ fixEdge e}
_ -> unexpected "edge tree" $ show tree
fixEdge e = case dir of
PG.DirectionOut -> e {PG.edgeOut = vid}
PG.DirectionIn -> e {PG.edgeIn = vid}
decode el = noDecoding "vertex"