packages feed

porcupine-core-0.1.0.0: src/Data/Locations/Mappings.hs

{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE ExistentialQuantification  #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TupleSections              #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE ViewPatterns               #-}
{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC "-fno-warn-incomplete-uni-patterns" #-}

module Data.Locations.Mappings
  ( LocationMappings, LocationMappings_(..)
  , HasDefaultMappingRule(..)
  , LocShortcut(..), SerializableLocShortcut
  --, allLocsInMappings
  , mappingsFromLocTree
  , mappingRootOnly
  , insertMappings
  , propagateMappings
  , applyMappings
  ) where

import           Control.Arrow                      ((***))
import           Control.Lens
import           Data.Aeson
import qualified Data.HashMap.Strict                as HM
import           Data.List
import           Data.Locations.Accessors
import           Data.Locations.Loc
import           Data.Locations.LocationTree
import           Data.Locations.LogAndErrors
import           Data.Locations.SerializationMethod (FileExt)
import           Data.Maybe
import           Data.Representable
import qualified Data.Text                          as T


-- * The 'LocationMappings' type

newtype LocationMappings_ n = LocationMappings_
  (HM.HashMap LocationTreePath [n])
  deriving (Functor, Show)

-- | Describes how physical locations are mapped to an application's
-- LocationTree. This is the type that is written to the pipeline yaml config
-- file under the "locations" section.
type LocationMappings = LocationMappings_ SerializableLocShortcut

instance Monoid (LocationMappings_ n) where
  mempty = LocationMappings_ mempty

instance Semigroup (LocationMappings_ n) where
  (LocationMappings_ m) <> (LocationMappings_ m') = LocationMappings_ $
    HM.unionWith (++) m m'

instance (ToJSON n) => ToJSON (LocationMappings_ n) where
  toJSON (LocationMappings_ m) = Object $ HM.fromList $
    map (toTextRepr *** layersToJSON) $ HM.toList m
    where
      layersToJSON []     = Null
      layersToJSON [l]    = toJSON l
      layersToJSON layers = toJSON layers

instance FromJSON LocationMappings where
  parseJSON (Object m) = LocationMappings_ . HM.fromList <$>
    mapM (\(k, v) -> (,) <$> fromTextRepr k <*> parseJSONLayers v) (HM.toList m)
    where
      parseJSONLayers Null      = pure []
      parseJSONLayers j@Array{} = parseJSON j
      parseJSONLayers j         = (:[]) <$> parseJSON j
  parseJSON _ = mempty

-- -- | Lists all the physical paths that have been associated to some virtual
-- -- location
-- allLocsInMappings :: LocationMappings -> [LocWithVars]
-- allLocsInMappings (LocationMappings_ m) =
--   [ loc
--   | (_,layers) <- HM.toList m, FullySpecifiedLoc loc <- layers ]


-- * How to get pre-filled defaut mappings from an existing LocationTree

-- | Means that we can possibly derive a default @LocShortcut@ from @a@
class HasDefaultMappingRule a where
  getDefaultLocShortcut :: a -> Maybe (LocShortcut x)
    -- ^ Nothing means that the @a@ should not be mapped by default

-- | Pre-fills the mappings from the context of a 'LocationTree', with extra
-- metadata saying whether each node should be explicitely mapped or unmapped.
mappingsFromLocTree :: (HasDefaultMappingRule a) => LocationTree a -> LocationMappings
mappingsFromLocTree (LocationTree node subtree) | HM.null subtree =
  LocationMappings_ $
    HM.singleton (LTP [])
                 (case getDefaultLocShortcut node of
                    Just shortcuts -> [shortcuts]
                    Nothing        -> [])
mappingsFromLocTree (LocationTree _ sub) =
  LocationMappings_ (mconcat $ map f $ HM.toList sub)
  where
    f (ltpi, t) =
      HM.fromList $ map appendPath $ HM.toList m
      where
        appendPath (LTP path, maps) = (LTP $ ltpi : path, maps)
        LocationMappings_ m = mappingsFromLocTree t

-- | Creates a 'LocationMappings_' where the whole LocationTree is mapped to a
-- single folder
mappingRootOnly :: Loc -> LocationMappings
mappingRootOnly l = LocationMappings_ $
  HM.singleton (LTP [])
               [FullySpecifiedLoc $ toJSON $ locWithVarsFromLoc l]


-- * How to parse mappings to and from JSON

-- | A location with variables where some parts may have been eluded
data LocShortcut a
  = DeriveWholeLocFromTree FileExt
    -- ^ Means that this loc path and name should be inherited from locs up the
    -- virtual tree.
  | DeriveLocPrefixFromTree (PathWithExtension StringWithVars)
    -- ^ Means that this loc path should be inherited from locs up the resource
    -- tree. Its name should be a concatenation of the corresponding name in the
    -- tree and the PathWithExtension provided
  | FullySpecifiedLoc a
    -- ^ Means that this shortcut is a full location
  deriving (Show)

-- | A 'LocShorcut' where fully specified locs are aeson Values ready to be
-- parsed by some LocationAccessor. It is parsed from the mappings in the
-- configuration file.
type SerializableLocShortcut = LocShortcut Value

-- | A 'LocShortcut' where fully specified locs have been parsed, and resolved
-- to be tied to some specific LocationAccessor. It isn't serializable in JSON,
-- hence the separation between this and 'SerializableLocShortcut'.
type ResolvedLocShortcut m = LocShortcut (SomeLocWithVars m)

      -- The underscore sign here means "reuse inherited", depending on the
      -- position it can mean either file path or extension or both.
instance ToJSON SerializableLocShortcut where
  toJSON (DeriveWholeLocFromTree ext) = String $ case ext of
    "" -> "_"
    _  -> "_." <> ext
  toJSON (DeriveLocPrefixFromTree l) = String $ "_" <> toTextRepr l
  toJSON (FullySpecifiedLoc v) = v

instance FromJSON SerializableLocShortcut where
  parseJSON (String "_") = pure $ DeriveWholeLocFromTree ""
  parseJSON (String (T.uncons -> Just ('_', s))) = case parseLocStringAndExt $ T.unpack s of
    Left e  -> fail e
    Right r -> pure $ DeriveLocPrefixFromTree r
  parseJSON v = pure $ FullySpecifiedLoc v

-- * How to apply mappings to a LocationTree to get the physical locations bound
-- to each of its nodes

-- | Returns a new 'LocationTree', updated from the mappings. Paths in the
-- 'LocationMappings_' that don't correspond to anything in the 'LocationTree'
-- will just be ignored
insertMappings
  :: LocationMappings
  -> LocationTree a
  -> LocationTree (a, Maybe [SerializableLocShortcut])
insertMappings (LocationMappings_ m) tree = foldl' go initTree $ HM.toList m
  where
    initTree = fmap (,Nothing) tree
      -- By defaut, each node is set to "no mapping defined"...
    go t (path, layers) = t &
      inLocTree path . _Just . locTreeNodeTag . _2 .~ Just layers
      -- ...then we update the tree for each mapping present in the
      -- LocationMappings

-- | For each location in the tree, gives it a final list of physical locations,
-- as /layers/ (which can be empty)
propagateMappings :: forall m a b.
                     ([SomeLocWithVars m] -> a -> Bool -> b)
                  -> LocationTree (a, Maybe [ResolvedLocShortcut m])
                  -> LocationTree b
propagateMappings f tree = propagateMappings' [] tree
  where
    -- if a folder is explicitly set to null (ie if no layer exist for this
    -- folder), then we recursively unmap everything is contains, ignoring every
    -- submapping that might exist:
    propagateMappings' _ t@(LocationTree (_, Just []) _) = fmap unmap t
      where unmap (n, _) = f [] n True
    -- if a folder is mapped, we propagate the mapping downwards:
    propagateMappings' inheritedLayers (LocationTree (thisNode, mbTheseMappings) thisSub) =
      LocationTree thisNode' $ imap recur thisSub
      where
        theseLayers = applyInheritedLayersToShortcuts inheritedLayers mbTheseMappings
        thisNode' = f theseLayers thisNode (isJust mbTheseMappings)
        recur fname subtree = propagateMappings' sublayers subtree
          where
            addSubdir :: SomeLocWithVars m -> SomeLocWithVars m
            addSubdir (SomeGLoc l) = SomeGLoc $ addSubdirToLoc l $ T.unpack (_ltpiName fname)
            sublayers = fmap addSubdir theseLayers

-- | Given a list of loc layers inherited from further up the tree, fills in the
-- blanks in the loc shortcuts given for once node of the tree in order to get
-- the final loc layers mapped to this node.
applyInheritedLayersToShortcuts
  :: forall m.
     [SomeLocWithVars m] -- ^ Inherited layers
  -> Maybe [ResolvedLocShortcut m] -- ^ LocShortcuts mapped to the node
  -> [SomeLocWithVars m] -- ^ Final layers mapped to this node
applyInheritedLayersToShortcuts inheritedLayers Nothing = inheritedLayers
applyInheritedLayersToShortcuts inheritedLayers (Just shortcuts) =
  concatMap fillShortcut shortcuts
  where
    fillShortcut = \case
      FullySpecifiedLoc l -> [l]
      DeriveLocPrefixFromTree fp ->
        flip map inheritedLayers $ \(SomeGLoc l) ->
                                     SomeGLoc @m $ useLocAsPrefix l fp
      DeriveWholeLocFromTree ext ->
        flip map inheritedLayers $ \(SomeGLoc l) ->
                                     SomeGLoc @m $ overrideLocType l (T.unpack ext)

-- | In a context where we have LocationAccessors available, we parse the
-- locations in a 'SerializableLocShortcut' and obtain a 'ResolvedLocShortcut'
resolveLocShortcut :: (LogThrow m) => SerializableLocShortcut -> LocResolutionM m (ResolvedLocShortcut m)
resolveLocShortcut (DeriveWholeLocFromTree ext) = return $ DeriveWholeLocFromTree ext
resolveLocShortcut (DeriveLocPrefixFromTree path) = return $ DeriveLocPrefixFromTree path
resolveLocShortcut (FullySpecifiedLoc value) =
  withParsedLocsWithVars [value] $ \[resolvedLoc] ->
    return $ FullySpecifiedLoc $ SomeGLoc resolvedLoc

-- | Transform a tree to one where unmapped nodes have been changed to 'mempty'
-- and mapped nodes have been associated to their physical location. A function
-- is applied to ask each node to integrate its final mappings, with a Bool to
-- tell whether whether the mapping for a node was explicit (True) or not
-- (False), ie. if it was explicitely declared in the config file or if it was
-- derived from the mapping of a parent folder. @n'@ is often some file type or
-- metadata that's required in the mapping.
--
-- TODO: Maybe change the callback type to
-- @DerivedOrExplicit [SomeLocWithVars m] -> a -> b@ with
-- @data DerivedOrExplicit a = Derived a | Explicit a@
applyMappings :: (LogThrow m)
              => ([SomeLocWithVars m] -> a -> Bool -> b)
                                   -- ^ Add physical locations (if they exist)
                                   -- to a node
              -> LocationMappings  -- ^ Mappings to apply
              -> LocationTree a    -- ^ Original tree
              -> LocResolutionM m (LocationTree b) -- ^ Tree with physical locations
applyMappings f mappings loctree = do
  let treeWithShortcuts = insertMappings mappings loctree
      resolve (node, Nothing) = return (node, Nothing)
      resolve (node, Just shortcuts) =
        (\rs -> (node, Just rs)) <$> mapM resolveLocShortcut shortcuts
  treeWithResolvedShortcuts <- traverse resolve treeWithShortcuts
  return $ propagateMappings f treeWithResolvedShortcuts