update spec doc, notably FQDN requirement
[pdns-pipe-nmc.git] / NmcTransform.hs
1 module NmcTransform ( seedNmcDom
2                     , descendNmcDom
3                     ) where
4
5 import Prelude hiding (lookup)
6 import Data.ByteString.Lazy (ByteString)
7 import Data.Text.Lazy (splitOn, pack, unpack)
8 import Data.Map.Lazy (empty, lookup, delete, size, singleton
9                      , foldrWithKey, insert, insertWith)
10 import Control.Monad (foldM)
11 import Data.Aeson (decode)
12
13 import NmcDom
14
15 -- | Perform query and return error string or parsed domain object
16 queryNmcDom ::
17   (String -> IO (Either String ByteString)) -- ^ query operation action
18   -> String                                 -- ^ key
19   -> IO (Either String NmcDom)              -- ^ error string or domain
20 queryNmcDom queryOp key = do
21   l <- queryOp key
22   case l of
23     Left estr -> return $ Left estr
24     Right str -> case decode str :: Maybe NmcDom of
25       Nothing  -> return $ Left $ "Unparseable value: " ++ (show str)
26       Just dom -> return $ Right dom
27
28 -- | Try to fetch "delegate" or "import" object and merge them into the
29 --   base domain. Original "import" element is removed, but newly
30 --   merged data may contain new "import" or "delegate", so the objects
31 --   that are about to be merged are processed recursively until there
32 --   are no more "import" and "deletage" attributes (or the depth gauge
33 --   reaches zero).
34 mergeIncl ::
35   (String -> IO (Either String ByteString)) -- ^ query operation action
36   -> Int                                    -- ^ recursion counter
37   -> NmcDom                                 -- ^ base domain
38   -> IO (Either String NmcDom)              -- ^ result with merged import
39 mergeIncl queryOp depth base = do
40   let
41     mbase = (expandSrv . splitSubdoms . mergeSelf) base
42     base' = mbase {domDelegate = Nothing, domImport = Nothing}
43   -- print base
44   if depth <= 0 then return $ Left "Nesting of imports is too deep"
45   else case ((domDelegate mbase), (domImport mbase)) of
46     (Nothing,  Nothing  ) -> return $ Right base'
47     (Nothing,  Just keys) -> foldM mergeIncl1 (Right base') keys
48     (Just key, _        ) -> mergeIncl1 (Right emptyNmcDom) key
49   where
50     mergeIncl1 (Left  err) _   = return $ Left err -- can never happen
51     mergeIncl1 (Right acc) key = do
52       sub <- queryNmcDom queryOp key
53       case sub of
54         Left  err  -> return $ Left err
55         Right sub' -> mergeIncl queryOp (depth - 1) $ sub' `mergeNmcDom` acc
56
57 -- | If there is an element in the map with key "", merge the contents
58 --   and remove this element. Do this recursively.
59 mergeSelf :: NmcDom -> NmcDom
60 mergeSelf base =
61   let
62     map   = domMap base
63     base' = base {domMap = removeSelf map}
64     removeSelf Nothing    = Nothing
65     removeSelf (Just map) = if size map' == 0 then Nothing else Just map'
66       where map' = delete "" map
67   in
68     case map of
69       Nothing   -> base'
70       Just map' ->
71         case lookup "" map' of
72           Nothing  -> base'
73           Just sub -> (mergeSelf sub) `mergeNmcDom` base'
74         -- recursion depth limited by the size of the record
75
76 -- | replace Service with Srv down in the Map
77 expandSrv :: NmcDom -> NmcDom
78 expandSrv base =
79   let
80     base' = base { domService = Nothing }
81   in
82     case domService base of
83       Nothing -> base'
84       Just sl -> foldr addSrvMx base' sl
85         where
86           addSrvMx sr acc = sub1 `mergeNmcDom` acc
87             where
88               sub1 = emptyNmcDom { domMap = Just (singleton proto sub2)
89                                  , domMx = maybemx}
90               sub2 = emptyNmcDom { domMap = Just (singleton srvid sub3) }
91               sub3 = emptyNmcDom { domSrv = Just [srvStr] }
92               proto = "_" ++ (srvProto sr)
93               srvid = "_" ++ (srvName sr)
94               srvStr =  (show (srvPrio sr)) ++ "\t"
95                      ++ (show (srvWeight sr)) ++ " "
96                      ++ (show (srvPort sr)) ++ " "
97                      ++ (srvHost sr)
98               maybemx =
99                 if srvName sr == "smtp"
100                    && srvProto sr == "tcp"
101                    && srvPort sr == 25
102                 then Just [(show (srvPrio sr)) ++ "\t" ++ (srvHost sr)]
103                 else Nothing
104
105 -- | Convert map elements of the form "subN...sub2.sub1.dom.bit"
106 --   into nested map and merge it
107 splitSubdoms :: NmcDom -> NmcDom
108 splitSubdoms base =
109   let
110     base' = base { domMap = Nothing }
111   in
112     case domMap base of
113       Nothing -> base'
114       Just sdmap -> (emptyNmcDom { domMap = Just sdmap' }) `mergeNmcDom` base'
115         where
116           sdmap' = foldrWithKey stow empty sdmap
117           stow fqdn sdom acc = insertWith mergeNmcDom fqdn' sdom' acc
118             where
119               (fqdn', sdom') =
120                 nest (map unpack (splitOn (pack ".") (pack fqdn)), sdom)
121               nest ([], v)   = (fqdn, v) -- can split result be empty?
122               nest ([k], v)  = (k, v)
123               nest (k:ks, v) =
124                 nest (ks, emptyNmcDom { domMap = Just (singleton k v) })
125  
126 -- | Presence of some elements require removal of some others
127 normalizeDom :: NmcDom -> NmcDom
128 normalizeDom dom = foldr id dom [ translateNormalizer
129                                 , nsNormalizer
130                                 ]
131   where
132     nsNormalizer dom = case domNs dom of
133       Nothing  -> dom
134       Just ns  -> emptyNmcDom { domNs = domNs dom, domEmail = domEmail dom }
135     translateNormalizer dom = case domTranslate dom of
136       Nothing  -> dom
137       Just tr  -> dom { domMap = Nothing }
138
139 -- | Merge imports and Selfs and follow the maps tree to get dom
140 descendNmcDom ::
141   (String -> IO (Either String ByteString)) -- ^ query operation action
142   -> [String]                               -- ^ subdomain chain
143   -> NmcDom                                 -- ^ base domain
144   -> IO (Either String NmcDom)              -- ^ fully processed result
145 descendNmcDom queryOp subdom base = do
146   base' <- mergeIncl queryOp 10 base
147   case subdom of
148     []   -> return $ fmap normalizeDom base'
149     d:ds ->
150       case base' of
151         Left err     -> return base'
152         Right base'' ->
153           case domMap base'' of
154             Nothing  -> return $ Right emptyNmcDom
155             Just map ->
156               case lookup d map of
157                 Nothing  -> return $ Right emptyNmcDom
158                 Just sub -> descendNmcDom queryOp ds sub
159
160 -- | Initial NmcDom populated with "import" only, suitable for "descend"
161 seedNmcDom ::
162   String        -- ^ domain key (without namespace prefix)
163   -> NmcDom     -- ^ resulting seed domain
164 seedNmcDom dn = emptyNmcDom { domImport = Just (["d/" ++ dn])}