├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config.nims
├── src
└── xmltools.nim
├── tests
└── test_all.nim
└── xmltools.nimble
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | bin
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: c
2 |
3 | env:
4 | # Nim versions to test against
5 | - CHOOSENIM_CHOOSE_VERSION=devel
6 | - CHOOSENIM_CHOOSE_VERSION=0.19.4
7 | - CHOOSENIM_CHOOSE_VERSION=0.19.0
8 |
9 | matrix:
10 | allow_failures:
11 | # devel branch is often broken
12 | - env: CHOOSENIM_CHOOSE_VERSION=devel
13 |
14 | install:
15 | - curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y
16 | - export PATH=~/.nimble/bin:$PATH
17 | - nimble update
18 | - nimble install -d -y
19 |
20 | before_script:
21 | - set -e
22 | - export PATH=~/.nimble/bin:$PATH
23 | - export CHOOSENIM_NO_ANALYTICS=1
24 | script:
25 | - nim test
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Anatoly Galiulin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xmltools [](https://github.com/yglukhov/nimble-tag)
2 |
3 | [](https://travis-ci.org/vegansk/xmltools)
4 |
5 | High level xml library for Nim.
6 |
7 | ## Examples ##
8 |
9 | ### Simple searches ###
10 |
11 | ```nim
12 | let xml = Node.fromStringE """
13 |
14 |
15 | 1
16 |
17 |
18 | 2
19 |
20 |
21 | 3
22 |
23 |
24 | """
25 |
26 | # Find all tags that's parent is
27 | let bTags = xml / "b"
28 | # Find all tags recursive starting from the root
29 | let cTags = xml // "c"
30 | ```
31 |
32 | ### Namespaces ###
33 |
34 | ```nim
35 | let xml = Node.fromStringE """
36 |
38 |
39 |
40 |
41 | 1
42 | 2
43 |
44 |
45 |
46 | """
47 |
48 | # Get namespaces declared in the root tag
49 | let nss = xml.namespaces
50 | # Check namespace presence by it's URL
51 | if nss.get("http://acme.com/api/v2").isDefined:
52 | # Get the namespace by it's URL
53 | let apiNs = nss.get("http://acme.com/api/v2").get
54 | # Get the value of tag using qualified name
55 | let sessionId = (xml // apiNs $: "session_id").text
56 | # Get the value of tag ignoring namespaces
57 | let issuerId = (xml // "*:issuer_id").text
58 | ```
59 |
60 | ### Get all of the error messages in the SOAP response as multiline string ###
61 |
62 | ```nim
63 | let xml = Node.fromStringE """
64 |
65 |
66 |
67 |
68 | SOAP-ENV:Client
69 | Validation error
70 |
71 | Schema validation error
72 |
73 | cvc-datatype-valid.1.2.1: 'ISSUER_ID_T' is not a valid value for 'integer'.
74 |
75 |
76 | cvc-type.3.1.3: The value 'ISSUER_ID_T' of element 'v2:issuer_id' is not valid.
77 |
78 |
79 |
80 |
81 |
82 | """
83 |
84 | let msgs = (xml // "*:Fault")
85 | .flatMap((e: Node) => e // "*:description" ++ e // "*:ValidationError")
86 | .map((n: Node) => n.text)
87 | .foldLeft("", (s, v: string) => s & (if s == "": "" else: "\L") & v)
88 | ```
89 |
90 | ### Xml to object parsing ###
91 |
92 | ```nim
93 | let xml = Node.fromStringE """
94 |
95 | 100
96 | Hello, world!
97 |
98 | """
99 | type Data = tuple[
100 | id: int,
101 | str: string,
102 | optStr: Option[string]
103 | ]
104 | let o: EitherS[Data] = tryS do -> auto:
105 | ((xml /! "id").asInt, (xml /! "str").asStr, (xml / "opt_str").asStrO)
106 | ```
107 |
--------------------------------------------------------------------------------
/config.nims:
--------------------------------------------------------------------------------
1 | srcdir = "src"
2 |
3 | proc buildBase(debug: bool, bin: string, src: string) =
4 | switch("out", (thisDir() & "/" & bin).toExe)
5 | --nimcache: build
6 | if not debug:
7 | --forceBuild
8 | --define: release
9 | --opt: size
10 | else:
11 | --define: debug
12 | --debuginfo
13 | --debugger: native
14 | --linedir: on
15 | --stacktrace: on
16 | --linetrace: on
17 | --verbosity: 1
18 |
19 | --NimblePath: src
20 | --NimblePath: srcdir
21 |
22 | setCommand "c", src
23 |
24 | proc test(name: string) =
25 | if not dirExists "bin":
26 | mkDir "bin"
27 | --run
28 | buildBase true, "bin/test_" & name, "tests/test_" & name
29 |
30 | task test, "Run all tests":
31 | test "all"
32 |
--------------------------------------------------------------------------------
/src/xmltools.nim:
--------------------------------------------------------------------------------
1 | import xmltree,
2 | xmlparser,
3 | streams,
4 | strutils,
5 | fp/either,
6 | fp/list,
7 | fp/option,
8 | fp/map,
9 | strtabs,
10 | sequtils,
11 | re,
12 | sugar,
13 | boost/parsers
14 |
15 | type
16 | Node* = distinct XmlNode
17 | NodeList* = List[Node]
18 | QNameImpl = tuple[ns: string, name: string]
19 | QName* = distinct QNameImpl
20 | Attr = (string, string)
21 | AttrValue* = Option[string]
22 | Attrs* = Map[string, string]
23 | Namespaces* = Map[string, string]
24 | NodeNotFoundError* = object of KeyError
25 | # We need this wrapper to generate normal error messages, and
26 | # module `re` doesn't have function `$` for Regex type
27 | XRegex* = ref object
28 | re: Regex
29 | p: string
30 |
31 | ####################################################################################################
32 | # Qualified name
33 |
34 | proc `$:`*(ns: string, name: string): QName = (ns: ns, name: name).QName
35 |
36 | converter toQName*(name: string): QName =
37 | let s = name.split(":")
38 | if s.len == 1:
39 | "" $: name
40 | else:
41 | s[0] $: s[1]
42 |
43 |
44 | proc ns*(q: QName): string = q.QNameImpl.ns
45 | proc name*(q: QName): string = q.QNameImpl.name
46 |
47 | proc nsDecl*(ns, url: string): Attr =
48 | ("xmlns" & (if ns == "": "" else: ":" & ns), url)
49 |
50 | proc `==`*(q1, q2: QName): bool {.borrow.}
51 |
52 | proc `$`*(qname: QName): string =
53 | if qname.ns == "":
54 | qname.name
55 | else:
56 | qname.ns & ":" & qname.name
57 |
58 | proc fromString(q: typedesc[QName], s: string): QName =
59 | let lst = s.split(":")
60 | if lst.len >= 2:
61 | lst[0] $: lst[1]
62 | else:
63 | s.toQName
64 |
65 | ####################################################################################################
66 | # Misc
67 |
68 | proc r*(s: string): XRegex =
69 | new(result)
70 | result.re = re(s)
71 | result.p = s
72 |
73 | proc `$`(r: XRegex): string = r.p
74 |
75 | proc name*(n: Node): QName =
76 | if n.XmlNode.kind != xnElement:
77 | QName.fromString("")
78 | else:
79 | QName.fromString(n.XmlNode.tag)
80 |
81 | proc nodeNotFoundMsg(n: string, name: string, deepSearch: bool): string =
82 | "Node $# doesn't have $# as it's $#" % [$n.name, name, if deepSearch: "descendant" else: "child"]
83 |
84 | proc toMap(s: StringTableRef): Map[string,string] =
85 | result = newMap[string,string]()
86 | for k,v in s:
87 | result = result + (k,v)
88 |
89 | ####################################################################################################
90 | # Node
91 |
92 | proc `$`*(n: Node): string = n.XmlNode.`$`
93 |
94 | proc fromStringE*(n = Node, s: string): Node =
95 | s.newStringStream.parseXml.Node
96 |
97 | proc fromString*(n: typedesc[Node], s: string): EitherS[Node] =
98 | tryS(() => Node.fromStringE(s))
99 |
100 | proc text*(n: Node): string = n.XmlNode.innerText
101 |
102 | proc child*(n: Node, qname: QName): Option[Node] = n.XmlNode.child($qname).some.notNil.map((v: XmlNode) => v.Node)
103 |
104 | proc attr*(n: Node, qname: QName): AttrValue =
105 | if n.XmlNode.kind == xnElement and n.XmlNode.attrsLen > 0:
106 | let name = $qname
107 | if n.XmlNode.attrs.hasKey(name): n.XmlNode.attrs[name].some.AttrValue else: string.none.AttrValue
108 | else:
109 | string.none.AttrValue
110 |
111 | proc `/`*(n: Node, regex: XRegex): NodeList =
112 | result = Nil[Node]()
113 | if n.XmlNode.kind == xnElement:
114 | for ch in n.XmlNode:
115 | if ch.kind == xnElement and ch.tag.match(regex.re):
116 | result = Cons(ch.Node, result)
117 | result = result.reverse
118 |
119 | proc `/`*(n: Node, qname: QName): NodeList =
120 | if n.XmlNode.kind != xnElement:
121 | result = Nil[Node]()
122 | elif qname.ns == "*":
123 | result = n / r("^(.+:)?" & qname.name & "$")
124 | else:
125 | result = Nil[Node]()
126 | let name = $qname
127 | for ch in n.XmlNode:
128 | if ch.kind == xnElement and ch.tag == name:
129 | result = Cons(ch.Node, result)
130 | result = result.reverse
131 |
132 | proc `//`*(n: Node, regex: XRegex): NodeList =
133 | if n.XmlNode.kind != xnElement:
134 | result = Nil[Node]()
135 | else:
136 | result = n / regex
137 | for ch in n.XmlNode:
138 | result = result ++ ch.Node // regex
139 |
140 | proc `//`*(n: Node, qname: QName): NodeList =
141 | if qname.ns == "*":
142 | result = n // r("^(.+:)?" & qname.name & "$")
143 | else:
144 | result = n.XmlNode.findAll($qname).asList.map((v: XmlNode) => v.Node)
145 |
146 | proc findAttrName(n: Node, regex: XRegex): Option[QName] =
147 | if n.XmlNode.kind == xnElement and n.XmlNode.attrsLen > 0:
148 | for k in n.XmlNode.attrs.keys:
149 | if k.match(regex.re):
150 | return QName.fromString(k).some
151 | QName.none
152 |
153 | proc `/@`*(n: Node, regex: XRegex): NodeList =
154 | result = Nil[Node]()
155 | if n.XmlNode.kind == xnElement:
156 | for ch in n.XmlNode:
157 | if ch.Node.findAttrName(regex).isDefined:
158 | result = Cons(ch.Node, result)
159 | result = result.reverse
160 |
161 | proc asStrO*(n: NodeList|Node|AttrValue): Option[string]
162 |
163 | proc `/@`*(n: Node, name: QName): NodeList =
164 | result = Nil[Node]()
165 | if n.XmlNode.kind != xnElement:
166 | return
167 | elif name.ns == "*":
168 | result = n /@ r("^(.+:)?" & name.name & "$")
169 | else:
170 | for ch in n.XmlNode:
171 | if ch.Node.attr(name).isDefined:
172 | result = Cons(ch.Node, result)
173 | result = result.reverse
174 |
175 | proc `//@`*(n: Node, regex: XRegex): NodeList =
176 | if n.XmlNode.kind != xnElement:
177 | result = Nil[Node]()
178 | else:
179 | result = n /@ regex
180 | for ch in n.XmlNode:
181 | result = result ++ ch.Node //@ regex
182 |
183 | proc `//@`*(n: Node, name: QName): NodeList =
184 | result = Nil[Node]()
185 | if n.XmlNode.kind != xnElement:
186 | return
187 | elif name.ns == "*":
188 | result = n //@ r("^(.+:)?" & name.name & "$")
189 | else:
190 | result = n /@ name
191 | for ch in n.XmlNode:
192 | result = result ++ ch.Node //@ name
193 |
194 | proc namespaces*(n:Node): Namespaces =
195 | let p = n.XmlNode.attrs.toMap
196 | p.filter((i: (string, string)) => QName.fromString(i.key).ns == "xmlns")
197 | .map((i: (string, string)) => (i.value, QName.fromString(i.key).name))
198 |
199 | ####################################################################################################
200 | # NodeList
201 |
202 | proc `$`*(lst: NodeList): string =
203 | lst.foldLeft("", (s: string, n: Node) => s & $n)
204 |
205 | proc `/`*(lst: NodeList, qname: QName): NodeList =
206 | lst.flatMap((n: Node) => n / qname)
207 |
208 | proc `//`*(lst: NodeList, qname: QName): NodeList =
209 | lst.flatMap((n: Node) => n // qname)
210 |
211 | proc text*(lst: NodeList): string =
212 | lst.foldLeft("", (s: string, n: Node) => s & n.text)
213 |
214 | ####################################################################################################
215 | # Data getters
216 |
217 | proc `/!`*(n: NodeList|Node, v: QName|XRegex): Node =
218 | let res = n / v
219 | if res.isEmpty:
220 | when n is Node:
221 | raise newException(NodeNotFoundError, nodeNotFoundMsg($n.name, $v, false))
222 | else:
223 | raise newException(NodeNotFoundError, nodeNotFoundMsg("", $v, false))
224 | res.head
225 |
226 | proc `//!`*(n: NodeList|Node, v: QName|XRegex): Node =
227 | let res = n // v
228 | if res.isEmpty:
229 | when n is Node:
230 | raise newException(NodeNotFoundError, nodeNotFoundMsg($n.name, $v, true))
231 | else:
232 | raise newException(NodeNotFoundError, nodeNotFoundMsg("", $v, true))
233 | res.head
234 |
235 | proc asStrO(n: NodeList|Node|AttrValue): Option[string] =
236 | when n is NodeList:
237 | n.headOption.map((n: Node) => n.text).notEmpty
238 | elif n is AttrValue:
239 | n
240 | else:
241 | n.text.some.notEmpty
242 |
243 | proc asStr*(n: NodeList|Node|AttrValue): string =
244 | n.asStrO.getOrElse("")
245 |
246 | proc asIntO*(n: NodeList|Node|AttrValue): Option[int] =
247 | n.asStrO.map((v: string) => v.strToInt)
248 |
249 | proc asInt*(n: Node|AttrValue): int =
250 | n.asStr.strToInt
251 |
252 | proc asInt64O*(n: NodeList|Node|AttrValue): Option[int64] =
253 | n.asStrO.map((v: string) => v.strToInt64)
254 |
255 | proc asInt64*(n: Node|AttrValue): int64 =
256 | n.asStr.strToInt64
257 |
258 | proc asUIntO*(n: NodeList|Node|AttrValue): Option[uint] =
259 | n.asStrO.map((v: string) => v.strToUInt)
260 |
261 | proc asUInt*(n: Node|AttrValue): uint =
262 | n.asStr.strToUInt
263 |
264 | proc asUInt64O*(n: NodeList|Node|AttrValue): Option[uint64] =
265 | n.asStrO.map((v: string) => v.strToUInt64)
266 |
267 | proc asUInt64*(n: Node|AttrValue): uint64 =
268 | n.asStr.strToUInt64
269 |
270 | #####################################################################################################
271 | # XmlBuilder
272 |
273 | type NodeBuilder* = () -> Node
274 |
275 | proc run*(b: NodeBuilder): Node = b()
276 |
277 | proc endn*(): List[NodeBuilder] = Nil[NodeBuilder]()
278 |
279 | proc attrs*(attrs: varargs[Attr]): Attrs =
280 | asMap(attrs)
281 |
282 | proc el*(qname: Qname, attrs: Attrs, children: List[NodeBuilder]): NodeBuilder =
283 | result = proc(): Node =
284 | var res = ($qname).newElement
285 | res.attrs = newStringTable()
286 | attrs.forEach((v: Attr) => (res.attrs[v.key] = v.value))
287 | children.forEach((nb: NodeBuilder) => res.add(nb().XmlNode))
288 | result = res.Node
289 | proc el*(qname: Qname, attrs: Attrs, children: varargs[NodeBuilder]): NodeBuilder = el(qname, attrs, asList(children))
290 | proc el*(qname: Qname, children: List[NodeBuilder]): NodeBuilder = el(qname, attrs(), children)
291 | proc el*(qname: Qname, children: varargs[NodeBuilder]): NodeBuilder = el(qname, attrs(), asList(children))
292 |
293 | proc textEl*(data: string): NodeBuilder =
294 | () => newText(data).Node
295 |
--------------------------------------------------------------------------------
/tests/test_all.nim:
--------------------------------------------------------------------------------
1 | import unittest,
2 | fp/either,
3 | fp/option,
4 | fp/map,
5 | xmltools,
6 | re,
7 | future
8 | import fp/list except `$`
9 |
10 | suite "xmltools":
11 | test "Conversion":
12 | let s = "1234"
13 | let x = Node.fromString(s)
14 | require: x.isRight
15 | check: $(x.get) == s
16 | let bx = Node.fromString("112233"""
27 | check: $(xml / "b") == "123"
28 |
29 | let xmlWithNs = Node.fromStringE """
30 |
31 | 11
32 | 22
33 | 33
34 |
35 | """
36 | check: $(xmlWithNs / "ns" $: "b") == "123"
37 |
38 | let xmlTree = Node.fromStringE """123 """
39 | let ns = ""
40 | check: $(xmlTree / ns $: "b" / ns $: "c") == "123"
41 | check: $(xmlTree // ns $: "c") == "123"
42 |
43 | test "Accessors":
44 | let xml = Node.fromStringE """112233"""
45 | check: xml.name == "a"
46 | check: (xml // "b").text == "123"
47 | check: xml.child("b").flatMap((v: Node) => v.attr("id")).getOrElse("") == "100"
48 |
49 | test "Namespaces":
50 | let xml = Node.fromStringE """
51 |
53 |
54 |
55 |
56 | 1
57 | 2
58 |
59 |
60 |
61 | """
62 | let nss = xml.namespaces
63 | require: nss.get("http://acme.com/api/v2").isDefined
64 | let apiNs = nss.get("http://acme.com/api/v2").get
65 | check: (xml // apiNs $: "session_id").text == "1"
66 | check: (xml // apiNs $: "issuer_id").text == "2"
67 |
68 | test "Advanced searches":
69 | let xml = Node.fromStringE """
70 |
71 |
72 |
73 |
74 | SOAP-ENV:Client
75 | Validation error
76 |
77 | Lily was here!
78 | cvc-datatype-valid.1.2.1: 'ISSUER_ID_T' is not a valid value for 'integer'.
79 | cvc-type.3.1.3: The value 'ISSUER_ID_T' of element 'v2:issuer_id' is not valid.
80 |
81 |
82 |
83 |
84 | """
85 | check: (xml // "*" $: "Fault").length == 1
86 | check: (xml // "*" $: "ValidationError").length == 2
87 | check: (xml // "*" $: "ValidationError").text == (xml // "*" $: "Fault" // "*" $: "ValidationError").text
88 | check: (xml /@ "xmlns" $: "spring-ws").length == 0
89 | check: (xml //@ "xmlns" $: "spring-ws").length == 2
90 | check: (xml /@ "*" $: "spring-ws").length == 0
91 | check: (xml //@ "*" $: "spring-ws").length == 2
92 | check: (xml /@ "xmlns:spring-ws").length == 0
93 | check: (xml //@ "xmlns:spring-ws").length == 2
94 |
95 | echo: (xml // "*:Fault")
96 | .flatMap((e: Node) => e // "*:description" ++ e // "*:ValidationError")
97 | .map((n: Node) => n.text)
98 | .foldLeft("", (s, v: string) => s & (if s == "": "" else: "\L") & v)
99 |
100 | test "Strong searches and data getters":
101 | let xml = Node.fromStringE """
102 |
103 | test
104 | 100500
105 |
106 | """
107 | expect(NodeNotFoundError): discard xml /! "test"
108 | expect(NodeNotFoundError): discard xml //! "test"
109 | check: (xml /! r"b" /! "v").asStrO == "test".some
110 | check: (xml //! "v").asStrO == "test".some # Only first v is used
111 | check: (xml //! "v").asStr == "test" # Only first v is used
112 | expect(ValueError): discard (xml //! "v").asInt # Only first v is used
113 | check: (xml // "c" /! "v").asIntO == 100500.some
114 | check: (xml // "c" /! "v").asInt == 100500
115 |
116 | test "Parse to object":
117 | let xml = Node.fromStringE """
118 |
119 | Hello, world!
120 |
121 | """
122 | type Data = tuple[
123 | id: int,
124 | str: string,
125 | optStr: Option[string]
126 | ]
127 | let o: EitherS[Data] = tryS do -> auto:
128 | Data((xml.attr("id").asInt, (xml /! "str").asStr, (xml / "opt_str").asStrO))
129 | check: o == (100, "Hello, world!", string.none).rightS
130 |
131 | test "XML builder":
132 | check: $(el"test".run) == ""
133 | check: $(el("ns" $: "test").run) == ""
134 | let xml = el("test", el("a", el("ns" $: "child")), el("b"), el("c"))
135 | echo xml()
136 | let xmla = el("test", attrs(("a", "b")), el("a"))
137 | echo xmla()
138 | check: $(el("test", textEl("data")).run) == "data"
139 |
--------------------------------------------------------------------------------
/xmltools.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "0.1.6"
4 | author = "Anatoly Galiulin "
5 | description = "High level xml library for Nim"
6 | license = "MIT"
7 |
8 | srcDir = "src"
9 |
10 | # Dependencies
11 |
12 | requires "nim >= 0.19.0", "nimfp >= 0.4.4", "nimboost >= 0.5.5"
13 |
--------------------------------------------------------------------------------