xml-isogen
TemplateHaskell generators for XML-isomorphic data types, including instances for parsing and rendering. A convenient DSL to define those types.
Essentially it's a haskell DSL which allows its users to generate XML parsers and generators for haskell datatypes. See also xsd-isogen
Tutorial
Lets go through series of examples. First things first:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module Tutorial
where
import Prelude hiding ((^))
import Control.DeepSeq -- from deepseq
import Data.Text
import Data.THGen.XML -- from xml-isogen
import Text.XML.Writer -- from xml-conduit-writer
import Text.XML.DOM.Parser -- from dom-parser
Records
Lets say we want to parse and/or generate an XML file of the following form:
<?xml version="1.0" encoding="UTF-8"?>
<person>
<name>
John
</name>
<email>
john@example.com
</email>
</person>
Person has a name and an email, and email could be omitted. With xml-isogen it's
enough to write the following definition:
"Person" =:= record ParserAndGenerator
! "name" [t|Text|]
? "email" [t|Text|]
At this point you can load the module into ghci and check what was generated for us
so far:
*Tutorial> :browse
data XmlPerson
= XmlPerson {_xpName :: !Text, _xpEmail :: !(Maybe Text)}
xpEmail ::
lens-4.18.1:Control.Lens.Type.Lens' XmlPerson (Maybe Text)
xpName :: lens-4.18.1:Control.Lens.Type.Lens' XmlPerson Text
We have a data type XmlPerson with two fields and two lenses. Note that the
fields have a prefix built of an underscore and all upper case characters
and digits of the type name.
Lets take a closer look at XmlPerson:
*Tutorial> :i XmlPerson
data XmlPerson
= XmlPerson {_xpName :: !Text, _xpEmail :: !(Maybe Text)}
-- Defined at Tutorial.hs:13:1
instance Show XmlPerson -- Defined at Tutorial.hs:13:1
instance Eq XmlPerson -- Defined at Tutorial.hs:13:1
instance FromDom XmlPerson -- Defined at Tutorial.hs:13:1
instance ToXML XmlPerson -- Defined at Tutorial.hs:13:1
We have
FromDom
and
ToXML
instance generated for us. That's because
we instructed xml-isogen to generate them using the ParserAndGenerator noun. You can specify
also Parser or Generator if you want only one of them.
The _xpEmail field is optional; that's because we prefixed it with ? modifier.
Here is the list of possible modifiers that affect types:
| Modifier | Description | Generated Type |
|---|---|---|
! | required | a |
? | optional | Maybe a |
* | repeated | [a] |
+ | nonempty | NonEmpty a |
Supported types
Fields in a record may have any types as long as they are instances of Eq, Show,
NFData, FromDom (for the parser) and ToXml (for the generator). Remember
though that TemplateHaskell requires types to be available before they are used
in a splice.
You can omit field types altogether, in that case the type will be assumed to be a
capitalized field name with an Xml prefix.
It's your responsibility to make sure that type exists.
Example:
newtype XmlEmail = XmlEmail Text
deriving (Eq, Show, NFData, ToXML)
instance FromDom XmlEmail where
fromDom = XmlEmail <$> fromDom
"Example1" =:= record Parser
! "email" -- will have type XmlEmail
Enumerations
Often XML element can contain only limited number of possible values. Lets define
a type Status that can have only values Active, Pending or Deleted:
"Status" =:= enum ParserAndGenerator
& "Active"
& "Pending"
& "Deleted"
This definition will generate the following type for us:
data XmlStatus
= XmlStatusActive
| XmlStatusPending
| XmlStatusDeleted
| UnknownXmlStatus !String
It has all the necessary instances, so you can use it as a type for a field.
Append content
Sometimes the XML you are dealing with contains a mix of elements and immediate content. Something like the following:
<?xml version="1.0" encoding="UTF-8"?>
<example>
<field1>
I am
</field1>
totally
<field2>
weird
</field2>
</example>
You can model this with an "append content" modifier ^. It will instruct
xml-isogen to append content of the field as it is, without wrapping it
into an XML element. For our case it may look like this:
"Example2" =:= record ParserAndGenerator
! "field1" [t|Text|]
^ "mixed" [t|Text|]
! "field2" [t|Text|]
After parsing the XML above, we'll get the following:
XmlExample2 {_xe2Field1 = "I am", _xe2Mixed = "totally", _xe2Field2 = "weird"}
Attributes
xml-isogen also supports XML attributes using !% and ?% modifiers:
"Example3" =:= record ParserAndGenerator
! "field1" [t|Text|]
!% "attribute1" [t|Text|]
?% "attribute2" [t|Text|]
"Body" =:= record ParserAndGenerator
! "root" [t|XmlExample3|]
The following two types will be generated:
data XmlExample3
= XmlExample3 {_xe3Field1 :: !Text,
_xe3Attribute1 :: !Text,
_xe3Attribute2 :: !(Maybe Text)}
newtype XmlBody = XmlBody {_xbRoot :: XmlExample3}
The _xe3Attribute2 is optional because we used ?% modifier. Attributes will be
attached to parent XML element. Here is an example of the generated XML file:
<root attribute1="world">
<field1>
hello
</field1>
</root>
Note that attributes are attached to the parent XML element, that's why we needed
XmlBody type here.
And here is what you get after parsing the XML:
XmlExample3 {_xe3Field1 = "hello", _xe3Attribute1 = "world", _xe3Attribute2 = Nothing}
Namespaces
Often XSD schema requires XML elements to be qualified with a namespace. To instruct
xml-isogen to qualify fields, specify namespace is a curly brackets:
"Example4" =:= record ParserAndGenerator
! "field1" [t|Text|]
! "{http://example.com/1}field2" [t|Text|]
! "{http://example.com/2}field3" [t|Text|]
Here is the generated XML:
<field1>
hello
</field1>
<field2 xmlns="http://example.com/1">
world
</field2>
<field3 xmlns="http://example.com/2">
!
</field3>
Nillable types
Sometimes optional element in XML are encoded using nil="true" attribute instead of
omitting the element. (The nil attribute comes from http://www.w3.org/2001/XMLSchema-instance namespace). With xml-isogen you handle it using the Nillable type:
"Example5" =:= record ParserAndGenerator
! "field" [t|Nillable Text|]
If the field contains the value Nothing, like this
XmlExample5 { _xe5Field = Nillable Nothing}
then the following XML will be generated:
<field
xmlns:ns="http://www.w3.org/2001/XMLSchema-instance"
ns:nil="true"
/>
Development
To start working with xml-isogen using nix use:
nix-shell --packages cabal2nix --run "cabal2nix ." > default.nix
nix-shell
cabal v2-build