git.iko.soy/typeable/xml-isogen
on nillable-stuff help sign in
282 lines 7.2 kB history raw
1# xml-isogen
3[![Build Status](https://travis-ci.org/typeable/xml-isogen.svg?branch=master)](https://travis-ci.org/typeable/xml-isogen)
5TemplateHaskell generators for XML-isomorphic data types, including
6instances for parsing and rendering. A convenient DSL to define those
7types.
9Essentially it's a haskell DSL which allows its users to generate XML parsers and generators for haskell datatypes.
10See also [xsd-isogen](https://github.com/typeable/xsd-isogen)
12## Tutorial
14Lets go through series of examples. First things first:
16```haskell
17{-# LANGUAGE TemplateHaskell #-}
18{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE DeriveGeneric #-}
21module Tutorial
22where
24import Prelude hiding ((^))
25import Control.DeepSeq -- from deepseq
26import Data.Text
27import Data.THGen.XML -- from xml-isogen
28import Text.XML.Writer -- from xml-conduit-writer
29import Text.XML.DOM.Parser -- from dom-parser
30```
32### Records
34Lets say we want to parse and/or generate an XML file of the following form:
36```XML
37<?xml version="1.0" encoding="UTF-8"?>
38<person>
39 <name>
40 John
41 </name>
42 <email>
43 john@example.com
44 </email>
45</person>
46```
48Person has a name and an email, and email could be omitted. With `xml-isogen` it's
49enough to write the following definition:
51```haskell
52"Person" =:= record ParserAndGenerator
53 ! "name" [t|Text|]
54 ? "email" [t|Text|]
55```
57At this point you can load the module into `ghci` and check what was generated for us
58so far:
60```
61*Tutorial> :browse
62data XmlPerson
63 = XmlPerson {_xpName :: !Text, _xpEmail :: !(Maybe Text)}
64xpEmail ::
65 lens-4.18.1:Control.Lens.Type.Lens' XmlPerson (Maybe Text)
66xpName :: lens-4.18.1:Control.Lens.Type.Lens' XmlPerson Text
67```
69We have a data type `XmlPerson` with two fields and two lenses. Note that the
70fields have a prefix built of an underscore and all upper case characters
71and digits of the type name.
74Lets take a closer look at `XmlPerson`:
76```
77*Tutorial> :i XmlPerson
78data XmlPerson
79 = XmlPerson {_xpName :: !Text, _xpEmail :: !(Maybe Text)}
80 -- Defined at Tutorial.hs:13:1
81instance Show XmlPerson -- Defined at Tutorial.hs:13:1
82instance Eq XmlPerson -- Defined at Tutorial.hs:13:1
83instance FromDom XmlPerson -- Defined at Tutorial.hs:13:1
84instance ToXML XmlPerson -- Defined at Tutorial.hs:13:1
86```
88We have
89[FromDom](https://hackage.haskell.org/package/dom-parser-3.1.0/docs/Text-XML-DOM-Parser-FromDom.html#t:FromDom)
90and
91[ToXML](https://hackage.haskell.org/package/xml-conduit-writer-0.1.1.2/docs/Text-XML-Writer.html#t:ToXML)
92instance generated for us. That's because
93we instructed `xml-isogen` to generate them using the `ParserAndGenerator` noun. You can specify
94also `Parser` or `Generator` if you want only one of them.
96The `_xpEmail` field is optional; that's because we prefixed it with `?` modifier.
97Here is the list of possible modifiers that affect types:
99Modifier | Description | Generated Type
100--- | --- | ---
101`!` | required | `a`
102`?` | optional | `Maybe a`
103`*` | repeated | `[a]`
104`+` | nonempty | `NonEmpty a`
106### Supported types
108Fields in a record may have any types as long as they are instances of `Eq`, `Show`,
109`NFData`, `FromDom` (for the parser) and `ToXml` (for the generator). Remember
110though that TemplateHaskell requires types to be available before they are used
111in a splice.
113You can omit field types altogether, in that case the type will be assumed to be a
114capitalized field name with an `Xml` prefix.
115It's your responsibility to make sure that type exists.
116Example:
118```haskell
119newtype XmlEmail = XmlEmail Text
120 deriving (Eq, Show, NFData, ToXML)
122instance FromDom XmlEmail where
123 fromDom = XmlEmail <$> fromDom
125"Example1" =:= record Parser
126 ! "email" -- will have type XmlEmail
127```
129### Enumerations
131Often XML element can contain only limited number of possible values. Lets define
132a type `Status` that can have only values `Active`, `Pending` or `Deleted`:
134```haskell
135"Status" =:= enum ParserAndGenerator
136 & "Active"
137 & "Pending"
138 & "Deleted"
139```
141This definition will generate the following type for us:
143```haskell
144data XmlStatus
145 = XmlStatusActive
146 | XmlStatusPending
147 | XmlStatusDeleted
148 | UnknownXmlStatus !String
149```
151It has all the necessary instances, so you can use it as a type for a field.
153### Append content
155Sometimes the XML you are dealing with contains a mix of elements and immediate
156content. Something like the following:
158```XML
159<?xml version="1.0" encoding="UTF-8"?>
160<example>
161 <field1>
162 I am
163 </field1>
164 totally
165 <field2>
166 weird
167 </field2>
168</example>
169```
171You can model this with an "append content" modifier `^`. It will instruct
172`xml-isogen` to append content of the field as it is, without wrapping it
173into an XML element. For our case it may look like this:
175```haskell
176"Example2" =:= record ParserAndGenerator
177 ! "field1" [t|Text|]
178 ^ "mixed" [t|Text|]
179 ! "field2" [t|Text|]
180```
182After parsing the XML above, we'll get the following:
184```haskell
185XmlExample2 {_xe2Field1 = "I am", _xe2Mixed = "totally", _xe2Field2 = "weird"}
186```
188### Attributes
190`xml-isogen` also supports XML attributes using `!%` and `?%` modifiers:
192```haskell
193"Example3" =:= record ParserAndGenerator
194 ! "field1" [t|Text|]
195 !% "attribute1" [t|Text|]
196 ?% "attribute2" [t|Text|]
198"Body" =:= record ParserAndGenerator
199 ! "root" [t|XmlExample3|]
200```
202The following two types will be generated:
204```haskell
205data XmlExample3
206 = XmlExample3 {_xe3Field1 :: !Text,
207 _xe3Attribute1 :: !Text,
208 _xe3Attribute2 :: !(Maybe Text)}
210newtype XmlBody = XmlBody {_xbRoot :: XmlExample3}
211```
213The `_xe3Attribute2` is optional because we used `?%` modifier. Attributes will be
214attached to parent XML element. Here is an example of the generated XML file:
216```XML
217<root attribute1="world">
218 <field1>
219 hello
220 </field1>
221</root>
222```
224Note that attributes are attached to the parent XML element, that's why we needed
225`XmlBody` type here.
227And here is what you get after parsing the XML:
229```haskell
230XmlExample3 {_xe3Field1 = "hello", _xe3Attribute1 = "world", _xe3Attribute2 = Nothing}
231```
233### Namespaces
235Often XSD schema requires XML elements to be qualified with a namespace. To instruct
236`xml-isogen` to qualify fields, specify namespace is a curly brackets:
238```haskell
239"Example4" =:= record ParserAndGenerator
240 ! "field1" [t|Text|]
241 ! "{http://example.com/1}field2" [t|Text|]
242 ! "{http://example.com/2}field3" [t|Text|]
243```
245Here is the generated XML:
247```XML
248<field1>
249 hello
250</field1>
251<field2 xmlns="http://example.com/1">
252 world
253</field2>
254<field3 xmlns="http://example.com/2">
255 !
256</field3>
257```
259### Nillable types
261Sometimes optional element in XML are encoded using `nil="true"` attribute instead of
262omitting 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:
264```haskell
265"Example5" =:= record ParserAndGenerator
266 ! "field" [t|Nillable Text|]
267```
269If the field contains the value `Nothing`, like this
271```haskell
272XmlExample5 { _xe5Field = Nillable Nothing}
273```
275then the following XML will be generated:
277```XML
278<field
279 xmlns:ns="http://www.w3.org/2001/XMLSchema-instance"
280 ns:nil="true"
281/>
282```