-- FpMLToHaskell module Main where -- This program is designed to convert a bunch of XML files containing XSD -- module decls into a bunch of Haskell modules containing data/newtype -- definitions which mirror the XSD. Once you have used this program -- to generate your type definitions, you should import Text.XML.HaXml.Schema -- (as well as the generated modules) wherever you intend to read and write -- XML files with your Haskell programs. import System.Exit import System.Environment import System.IO import Control.Monad import Control.Exception as E import System.Directory import Data.List import Data.Maybe (fromMaybe,mapMaybe) import Data.Function (on) import Text.XML.HaXml (version) import Text.XML.HaXml.Types import Text.XML.HaXml.Namespaces (resolveAllNames,qualify ,nullNamespace) import Text.XML.HaXml.Parse (xmlParse') import Text.XML.HaXml.Util (docContent) import Text.XML.HaXml.Posn (posInNewCxt) import Text.XML.HaXml.Schema.Parse import Text.XML.HaXml.Schema.XSDTypeModel (Schema) import Text.XML.HaXml.Schema.NameConversion import Text.XML.HaXml.Schema.Environment as Env import Text.XML.HaXml.Schema.TypeConversion as XsdToH import Text.XML.HaXml.Schema.PrettyHaskell import qualified Text.XML.HaXml.Schema.PrettyHsBoot as HsBoot import qualified Text.XML.HaXml.Schema.HaskellTypeModel as Haskell import Text.ParserCombinators.Poly import Text.PrettyPrint.HughesPJ (render) fst3 :: (a,b,c) -> a fst3 (a,_,_) = a -- sucked in from Text.XML.HaXml.Wrappers to avoid dependency on T.X.H.Html argDirsToFiles :: IO (FilePath,[(FilePath,FilePath)]) argDirsToFiles = do args <- getArgs when ("--version" `elem` args) $ do putStrLn $ "part of HaXml-"++version exitSuccess when ("--help" `elem` args) $ do putStrLn "Usage: FpMLToHaskell xsdDir haskellDir" putStrLn " -- The results go into haskelldir/Data/FpML/file0.hs etc" putStrLn "See http://haskell.org/HaXml" exitSuccess case args of [xsddir,hdir]-> do files <- fmap (filter (".xsd" `isSuffixOf`)) (getDirectoryContents xsddir) let newdirs = map (\file->hdir++"/"++dirOf (fpml file)) files mapM_ (\newdir -> do createDirectoryIfMissing True newdir) newdirs return (xsddir ,map (\f-> (f, hdir++"/"++reslash (fpml f)++".hs")) files) _ -> do prog <- getProgName putStrLn ("Usage: "++prog++" xsdDir haskellDir") exitFailure where reslash = map (\c-> case c of '.'->'/'; _->c) dirOf = intercalate "/" . init . wordsBy' '.' wordsBy' c s = let (a,b) = span (/=c) s in if null b then [a] else a: wordsBy' c (tail b) main ::IO () main = do (dir,files) <- argDirsToFiles deps <- forM files (\ (inf,_outf)-> do putStrLn $ "Reading "++inf thiscontent <- readFileUTF8 (dir++"/"++inf) let d@Document{} = resolveAllNames qualify . either (error . ("not XML:\n"++)) id . xmlParse' inf $ thiscontent case runParser schema [docContent (posInNewCxt inf Nothing) d] of (Left msg,_) -> do hPutStrLn stderr msg return ([], undefined) (Right v,[]) -> return (Env.gatherImports v, v) (Right v,_) -> do putStrLn "Parse incomplete!" putStrLn inf putStrLn "\n-----------------\n" putStrLn $ show v putStrLn "\n-----------------\n" return ([],v) ) let filedeps :: [[((FilePath,FilePath),([(FilePath,Maybe String)],Schema))]] filedeps = ordered (\ ((inf,_),_)-> inf) (\ (_,(ds,_))-> map fst ds) (\x-> lookupWith (fst.fst) x (zip files deps)) (zip files deps) -- a single supertype environment, closed over all modules supertypeEnv :: Environment supertypeEnv = foldr (flip (foldr (\ ((inf, _), (_, v)) -> mkEnvironment inf v))) emptyEnv filedeps adjust :: Environment -> Environment adjust env = env{ env_extendty = env_extendty supertypeEnv , env_substGrp = env_substGrp supertypeEnv , env_allTypes = env_allTypes supertypeEnv } -- each module's env includes only dependencies, apart from supertypes environs :: [(FilePath,(Environment,FilePath,Schema))] environs = flip concatMap filedeps $ \scc-> case scc of [((inf,outf),(ds,v))]-> [(inf, ( adjust $ mkEnvironment inf v (foldr combineEnv emptyEnv (flip map ds (\d-> fst3 $ fromMaybe (error "FME") $ lookup (fst d) environs) ) ) , outf , v ) )] cyclic -> let jointSchema :: Schema jointSchema = mconcat (map (snd.snd) cyclic) jointDeps :: [FilePath] jointDeps = concatMap (map fst.fst.snd) cyclic jointEnv :: Environment jointEnv = mkEnvironment "" jointSchema $ foldr combineEnv emptyEnv $ flip map (nub jointDeps \\ map (fst.fst) cyclic) (\d-> fst3 $ fromMaybe (error "FME") $ lookup d environs) in flip map cyclic $ \((inf,outf),(_,v))-> (inf,(adjust $ mkEnvironment inf v jointEnv ,outf ,v) ) forM_ environs (\ (inf,(env,outf,v))-> do o <- openFile outf WriteMode hb <- openFile (bootf outf) WriteMode hSetEncoding o utf8 hSetEncoding hb utf8 let decls = XsdToH.convert env (XsdToH.typeLift v) haskell = Haskell.mkModule inf v decls doc = ppModule fpmlNameConverter haskell docboot = HsBoot.ppModule fpmlNameConverter haskell putStrLn $ "Writing "++outf hPutStrLn o $ render doc putStrLn $ "Writing "++bootf outf hPutStrLn hb $ render docboot hFlush o hFlush hb ) -- | Munge filename for hs-boot. bootf :: FilePath -> FilePath bootf x = case reverse x of 's':'h':'.':f -> reverse f++".hs-boot" _ -> error "bad stuff made my cheese boots melt" -- | Calculate dependency ordering of modules, least dependent first. -- Cyclic groups may occur, suitably placed in the ordering. ordered :: (Eq a, Eq b) => (b->a) -> (b->[a]) -> (a->Maybe b) -> [b] -> [[b]] ordered name' deps env list = let cycles = cyclicDeps name' deps env list noncyclic = map (:[]) $ list \\ concat cycles workqueue = noncyclic++cycles in traverse' [] workqueue where traverse' acc [] = acc traverse' acc (w:wq) = if all (`elem` concatMap (map name') acc) (concatMap deps w \\ map name' w) then traverse' (acc++[w]) wq else traverse' acc (wq++[w]) -- | Find cyclic dependencies between modules. cyclicDeps :: Eq a => (b->a) -> (b->[a]) -> (a->Maybe b) -> [b] -> [[b]] cyclicDeps name' deps env = nubBy (setEq`on`map name') . (\cs-> foldl minimal cs cs) . concatMap (walk []) where -- walk :: [b] -> b -> [[b]] walk acc t = if name' t `elem` map name' acc then [acc] else concatMap (walk (t:acc)) (mapMaybe env $ deps t) minimal acc c = concatMap (prune c) acc prune c c' = [c' | not (map name' c `isProperSubsetOf` map name' c')] isSubsetOf a b = all (`elem`b) a setEq a b = a`isSubsetOf`b && b`isSubsetOf`a isProperSubsetOf a b = a`isSubsetOf`b && not (b`isSubsetOf`a) -- | A variation on the standard lookup function. lookupWith :: Eq a => (b->a) -> a -> [b] -> Maybe b lookupWith _ _ [] = Nothing lookupWith proj x (y:ys) | proj y == x = Just y | otherwise = lookupWith proj x ys -- | What is the targetNamespace of the unique top-level element? targetNamespace :: Element i -> String targetNamespace (Elem qn attrs _) = if qn /= xsdSchema then "ERROR! top element not an xsd:schema tag" else maybe "ERROR! no targetNamespace specified" show (lookup (N "targetNamespace") attrs) -- | The XSD Namespace. xsdSchema :: QName xsdSchema = QN (nullNamespace{nsURI="http://www.w3.org/2001/XMLSchema"}) "schema" -- | UTF8-clean readFile; avoids handle-leaks. readFileUTF8 :: FilePath -> IO String readFileUTF8 file = do h <- openFile file ReadMode (do hSetEncoding h utf8 hGetContents h) `E.onException` hClose h