HaXml-1.25.14: src/tools/FpMLToHaskell.hs
-- 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