@zio.dev/zio-blocks 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,743 @@
1
+ ---
2
+ id: xml
3
+ title: "XML"
4
+ ---
5
+
6
+ Zero-dependency XML codec for ZIO Blocks Schema with cross-platform support.
7
+
8
+ ## Overview
9
+
10
+ The schema-xml module provides automatic XML codec derivation for any type with a `Schema`. It includes a complete XML AST, fluent navigation API, and support for XML-specific features like attributes and namespaces.
11
+
12
+ Key features:
13
+
14
+ - **Zero Dependencies**: No external XML libraries required
15
+ - **Cross-Platform**: Full support for JVM and Scala.js
16
+ - **Schema-Based**: Automatic codec derivation from Schema definitions
17
+ - **XML AST**: Complete representation of XML documents
18
+ - **Fluent API**: Navigation and transformation with XmlSelection
19
+ - **Attributes**: First-class support for XML attributes via annotations
20
+ - **Namespaces**: XML namespace support with prefix handling
21
+
22
+ ## Installation
23
+
24
+ ```scala
25
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-xml" % "0.0.14"
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ ### Deriving Codecs
31
+
32
+ ```scala
33
+ import zio.blocks.schema._
34
+ import zio.blocks.schema.xml._
35
+
36
+ case class Person(name: String, age: Int)
37
+
38
+ object Person {
39
+ implicit val schema: Schema[Person] = Schema.derived
40
+ }
41
+
42
+ // Derive XML codec using the unified format API
43
+ val codec = Schema[Person].derive(XmlFormat)
44
+ ```
45
+
46
+ ### Encoding to XML
47
+
48
+ ```scala
49
+ import zio.blocks.schema._
50
+ import zio.blocks.schema.xml._
51
+
52
+ case class Person(name: String, age: Int)
53
+ object Person {
54
+ implicit val schema: Schema[Person] = Schema.derived
55
+ }
56
+
57
+ val codec = Schema[Person].derive(XmlFormat)
58
+ val person = Person("Alice", 30)
59
+
60
+ // Encode to XML bytes
61
+ val bytes: Array[Byte] = codec.encode(person)
62
+
63
+ // Encode to XML string
64
+ val xmlString: String = codec.encodeToString(person)
65
+ // <Person><name>Alice</name><age>30</age></Person>
66
+
67
+ // Encode to pretty-printed XML
68
+ val prettyXml = codec.encodeToString(person, WriterConfig.pretty)
69
+ // <Person>
70
+ // <name>Alice</name>
71
+ // <age>30</age>
72
+ // </Person>
73
+ ```
74
+
75
+ ### Decoding from XML
76
+
77
+ ```scala
78
+ import zio.blocks.schema._
79
+ import zio.blocks.schema.xml._
80
+
81
+ case class Person(name: String, age: Int)
82
+ object Person {
83
+ implicit val schema: Schema[Person] = Schema.derived
84
+ }
85
+
86
+ val codec = Schema[Person].derive(XmlFormat)
87
+
88
+ // Decode from XML string
89
+ val xml = "<Person><name>Alice</name><age>30</age></Person>"
90
+ val result: Either[SchemaError, Person] = codec.decode(xml)
91
+ // Right(Person("Alice", 30))
92
+
93
+ // Decode from bytes
94
+ val bytes = xml.getBytes("UTF-8")
95
+ val fromBytes: Either[SchemaError, Person] = codec.decode(bytes)
96
+ ```
97
+
98
+ ## XML AST
99
+
100
+ The `Xml` ADT represents all valid XML node types:
101
+
102
+ ```
103
+ Xml
104
+ ├── Xml.Element (element with name, attributes, children)
105
+ ├── Xml.Text (character data)
106
+ ├── Xml.CData (unparsed character data)
107
+ ├── Xml.Comment (XML comment)
108
+ └── Xml.ProcessingInstruction (processing instruction)
109
+ ```
110
+
111
+ ### Creating XML Nodes
112
+
113
+ ```scala
114
+ import zio.blocks.schema.xml._
115
+ import zio.blocks.chunk.Chunk
116
+
117
+ // Create elements
118
+ val simple = Xml.Element("person")
119
+ val withChildren = Xml.Element("person", Xml.Text("Alice"))
120
+
121
+ // Create with XmlName (for namespaces)
122
+ val namespaced = Xml.Element(
123
+ XmlName("person", "http://example.com"),
124
+ Chunk.empty,
125
+ Chunk.empty
126
+ )
127
+
128
+ // Create text nodes
129
+ val text = Xml.Text("Hello, World!")
130
+ val cdata = Xml.CData("<script>...</script>")
131
+
132
+ // Create comments and processing instructions
133
+ val comment = Xml.Comment("This is a comment")
134
+ val pi = Xml.ProcessingInstruction("xml-stylesheet", "href=\"style.css\"")
135
+ ```
136
+
137
+ ### XmlName
138
+
139
+ `XmlName` represents an element or attribute name with optional namespace:
140
+
141
+ ```scala
142
+ import zio.blocks.schema.xml.XmlName
143
+
144
+ // Local name only
145
+ val simple = XmlName("person")
146
+ simple.localName // "person"
147
+ simple.namespace // ""
148
+ simple.prefix // ""
149
+
150
+ // With namespace URI
151
+ val ns = XmlName("person", "http://example.com/ns")
152
+
153
+ // With prefix (for prefixed elements like atom:feed)
154
+ val prefixed = XmlName("feed", Some("atom"), None)
155
+ prefixed.localName // "feed"
156
+ prefixed.prefix.contains("atom") // true
157
+ ```
158
+
159
+ ## XmlBuilder
160
+
161
+ Construct XML documents programmatically with a fluent API:
162
+
163
+ ```scala
164
+ import zio.blocks.schema.xml._
165
+
166
+ // Build an element with attributes and children
167
+ val doc = XmlBuilder.element("person")
168
+ .attr("id", "123")
169
+ .attr("status", "active")
170
+ .child(XmlBuilder.element("name").text("Alice").build)
171
+ .child(XmlBuilder.element("age").text("30").build)
172
+ .build
173
+
174
+ // Result:
175
+ // <person id="123" status="active">
176
+ // <name>Alice</name>
177
+ // <age>30</age>
178
+ // </person>
179
+
180
+ // Create other node types
181
+ val textNode = XmlBuilder.text("content")
182
+ val cdataNode = XmlBuilder.cdata("<![CDATA[raw content]]>")
183
+ val commentNode = XmlBuilder.comment("comment text")
184
+ ```
185
+
186
+ ## Configuration
187
+
188
+ ### WriterConfig
189
+
190
+ Controls XML output formatting:
191
+
192
+ ```scala
193
+ import zio.blocks.schema.xml.WriterConfig
194
+
195
+ // Compact output (default)
196
+ val compact = WriterConfig.default
197
+ // <Person><name>Alice</name></Person>
198
+
199
+ // Pretty-printed with 2-space indentation
200
+ val pretty = WriterConfig.pretty
201
+ // <Person>
202
+ // <name>Alice</name>
203
+ // </Person>
204
+
205
+ // With XML declaration
206
+ val withDecl = WriterConfig.withDeclaration
207
+ // <?xml version="1.0" encoding="UTF-8"?>
208
+ // <Person><name>Alice</name></Person>
209
+
210
+ // Custom configuration
211
+ val custom = WriterConfig(
212
+ indentStep = 4,
213
+ includeDeclaration = true,
214
+ encoding = "UTF-8"
215
+ )
216
+ ```
217
+
218
+ | Option | Default | Description |
219
+ |--------|---------|-------------|
220
+ | `indentStep` | `0` | Spaces per indentation level (0 = compact) |
221
+ | `includeDeclaration` | `false` | Include XML declaration |
222
+ | `encoding` | `"UTF-8"` | Character encoding in declaration |
223
+
224
+ ### ReaderConfig
225
+
226
+ Controls XML parsing behavior and security limits:
227
+
228
+ ```scala
229
+ import zio.blocks.schema.xml.ReaderConfig
230
+
231
+ // Default configuration
232
+ val default = ReaderConfig.default
233
+
234
+ // Custom limits
235
+ val custom = ReaderConfig(
236
+ maxDepth = 100,
237
+ maxAttributes = 50,
238
+ maxTextLength = 1000000,
239
+ preserveWhitespace = true
240
+ )
241
+ ```
242
+
243
+ | Option | Default | Description |
244
+ |--------|---------|-------------|
245
+ | `maxDepth` | `1000` | Maximum element nesting depth |
246
+ | `maxAttributes` | `1000` | Maximum attributes per element |
247
+ | `maxTextLength` | `10000000` | Maximum text content length |
248
+ | `preserveWhitespace` | `false` | Preserve whitespace in text nodes |
249
+
250
+ ## Attributes
251
+
252
+ Encode case class fields as XML attributes using the `@xmlAttribute` annotation:
253
+
254
+ ```scala
255
+ import zio.blocks.schema._
256
+ import zio.blocks.schema.xml._
257
+
258
+ case class Person(
259
+ @xmlAttribute() id: String,
260
+ @xmlAttribute("status") active: String,
261
+ name: String,
262
+ age: Int
263
+ )
264
+
265
+ object Person {
266
+ implicit val schema: Schema[Person] = Schema.derived
267
+ }
268
+
269
+ val codec = Schema[Person].derive(XmlFormat)
270
+ val person = Person("123", "active", "Alice", 30)
271
+ val xml = codec.encodeToString(person)
272
+ // <Person id="123" status="active">
273
+ // <name>Alice</name>
274
+ // <age>30</age>
275
+ // </Person>
276
+ ```
277
+
278
+ The `@xmlAttribute` annotation accepts an optional custom name:
279
+
280
+ - `@xmlAttribute()` - Uses the field name as the attribute name
281
+ - `@xmlAttribute("customName")` - Uses the provided name as the attribute name
282
+
283
+ ## Namespaces
284
+
285
+ Support for XML namespaces with the `@xmlNamespace` annotation:
286
+
287
+ ```scala
288
+ import zio.blocks.schema._
289
+ import zio.blocks.schema.xml._
290
+
291
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
292
+ case class Feed(
293
+ title: String,
294
+ updated: String
295
+ )
296
+
297
+ object Feed {
298
+ implicit val schema: Schema[Feed] = Schema.derived
299
+ }
300
+
301
+ val codec = Schema[Feed].derive(XmlFormat)
302
+ val feed = Feed("My Blog", "2024-01-01T00:00:00Z")
303
+ val xml = codec.encodeToString(feed)
304
+ // <atom:Feed xmlns:atom="http://www.w3.org/2005/Atom">
305
+ // <title>My Blog</title>
306
+ // <updated>2024-01-01T00:00:00Z</updated>
307
+ // </atom:Feed>
308
+ ```
309
+
310
+ Without a prefix (default namespace):
311
+
312
+ ```scala
313
+ import zio.blocks.schema._
314
+ import zio.blocks.schema.xml._
315
+
316
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom")
317
+ case class Feed(title: String)
318
+
319
+ object Feed {
320
+ implicit val schema: Schema[Feed] = Schema.derived
321
+ }
322
+
323
+ val codec = Schema[Feed].derive(XmlFormat)
324
+ val feed = Feed("My Blog")
325
+ val xml = codec.encodeToString(feed)
326
+ // <Feed xmlns="http://www.w3.org/2005/Atom">
327
+ // <title>My Blog</title>
328
+ // </Feed>
329
+ ```
330
+
331
+ ## XmlSelection
332
+
333
+ `XmlSelection` provides a fluent API for navigating and querying XML structures:
334
+
335
+ ### Navigation
336
+
337
+ ```scala
338
+ import zio.blocks.schema.xml._
339
+
340
+ val xml = XmlReader.read("""
341
+ <library>
342
+ <books>
343
+ <book id="1">
344
+ <title>Functional Programming</title>
345
+ <author>Alice</author>
346
+ </book>
347
+ <book id="2">
348
+ <title>Advanced Scala</title>
349
+ <author>Bob</author>
350
+ </book>
351
+ </books>
352
+ </library>
353
+ """).toOption.get
354
+
355
+ // Navigate to child elements
356
+ val books = xml.select.get("library").get("books")
357
+
358
+ // Navigate by index
359
+ val firstBook = books.get("book")(0)
360
+
361
+ // Extract text content
362
+ val title: Either[XmlError, String] = firstBook.get("title").text
363
+ // Right("Functional Programming")
364
+
365
+ // Navigate descendants (recursive search)
366
+ val allTitles = xml.select.descendant("title")
367
+ ```
368
+
369
+ ### Filtering
370
+
371
+ ```scala
372
+ import zio.blocks.schema.xml._
373
+
374
+ val selection: XmlSelection = ???
375
+
376
+ // Filter by type
377
+ val elements = selection.elements
378
+ val texts = selection.texts
379
+ val comments = selection.comments
380
+
381
+ // Custom filtering
382
+ val filtered = selection.filter(xml => xml.is(XmlType.Element))
383
+ ```
384
+
385
+ ### Terminal Operations
386
+
387
+ ```scala
388
+ import zio.blocks.schema.xml._
389
+
390
+ val selection: XmlSelection = ???
391
+
392
+ // Get single value (fails if not exactly one)
393
+ val one: Either[XmlError, Xml] = selection.one
394
+
395
+ // Get any value (first of many)
396
+ val any: Either[XmlError, Xml] = selection.any
397
+
398
+ // Get all values as a single XML element
399
+ val all: Either[XmlError, Xml] = selection.all
400
+
401
+ // Convert to chunk
402
+ val chunk = selection.toChunk
403
+
404
+ // Extract text content
405
+ val text: Either[XmlError, String] = selection.text
406
+ val allText: String = selection.textContent
407
+ ```
408
+
409
+ ### Combinators
410
+
411
+ ```scala
412
+ import zio.blocks.schema.xml._
413
+
414
+ val selection1: XmlSelection = ???
415
+ val selection2: XmlSelection = ???
416
+
417
+ // Map over selections
418
+ val mapped = selection1.map(xml => xml)
419
+
420
+ // FlatMap for chaining
421
+ val nested = selection1.flatMap(xml => XmlSelection.succeed(xml))
422
+
423
+ // Combine selections
424
+ val combined = selection1 ++ selection2
425
+
426
+ // Alternative on failure
427
+ val withFallback = selection1.orElse(selection2)
428
+ ```
429
+
430
+ ## XmlPatch
431
+
432
+ `XmlPatch` provides composable XML modification operations:
433
+
434
+ ### Creating Patches
435
+
436
+ ```scala
437
+ import zio.blocks.schema._
438
+ import zio.blocks.schema.xml._
439
+
440
+ val path = p".library.books.book"
441
+
442
+ // Add content
443
+ val addPatch = XmlPatch.add(
444
+ path,
445
+ XmlBuilder.element("book").attr("id", "3").build,
446
+ XmlPatch.Position.AppendChild
447
+ )
448
+
449
+ // Remove element
450
+ val removePatch = XmlPatch.remove(path)
451
+
452
+ // Replace element
453
+ val replacePatch = XmlPatch.replace(
454
+ path,
455
+ XmlBuilder.element("book").attr("id", "999").build
456
+ )
457
+
458
+ // Set attribute
459
+ val attrPatch = XmlPatch.setAttribute(path, "featured", "true")
460
+
461
+ // Remove attribute
462
+ val removeAttrPatch = XmlPatch.removeAttribute(path, "id")
463
+ ```
464
+
465
+ ### Position Options
466
+
467
+ ```scala
468
+ import zio.blocks.schema.xml.XmlPatch.Position
469
+
470
+ Position.Before // Insert before the target element
471
+ Position.After // Insert after the target element
472
+ Position.PrependChild // Insert as first child of target
473
+ Position.AppendChild // Insert as last child of target
474
+ ```
475
+
476
+ ### Applying Patches
477
+
478
+ ```scala
479
+ import zio.blocks.schema._
480
+ import zio.blocks.schema.xml._
481
+
482
+ val xml: Xml = ???
483
+ val patch = XmlPatch.setAttribute(p".person", "active", "true")
484
+
485
+ // Apply the patch
486
+ val result: Either[XmlError, Xml] = patch(xml)
487
+ ```
488
+
489
+ ### Composing Patches
490
+
491
+ ```scala
492
+ import zio.blocks.schema._
493
+ import zio.blocks.schema.xml._
494
+
495
+ val patch1 = XmlPatch.setAttribute(p".person", "id", "123")
496
+ val patch2 = XmlPatch.add(
497
+ p".person",
498
+ XmlBuilder.element("email").text("alice@example.com").build,
499
+ XmlPatch.Position.AppendChild
500
+ )
501
+
502
+ // Compose patches - applies patch1, then patch2
503
+ val combined = patch1 ++ patch2
504
+ ```
505
+
506
+ ## Extension Syntax
507
+
508
+ When a `Schema` is in scope, use convenient extension methods:
509
+
510
+ ```scala
511
+ import zio.blocks.schema._
512
+ import zio.blocks.schema.xml._
513
+ import zio.blocks.schema.xml.syntax._
514
+
515
+ case class Person(name: String, age: Int)
516
+ object Person {
517
+ implicit val schema: Schema[Person] = Schema.derived
518
+ }
519
+
520
+ val person = Person("Alice", 30)
521
+
522
+ // Encode to XML AST
523
+ val xml: Xml = person.toXml
524
+
525
+ // Encode to XML string
526
+ val xmlString: String = person.toXmlString
527
+ // <Person><name>Alice</name><age>30</age></Person>
528
+
529
+ // Encode to bytes
530
+ val bytes: Array[Byte] = person.toXmlBytes
531
+
532
+ // Decode from XML string
533
+ val parsed: Either[SchemaError, Person] =
534
+ "<Person><name>Bob</name><age>25</age></Person>".fromXml[Person]
535
+
536
+ // Decode from bytes
537
+ val fromBytes: Either[SchemaError, Person] = bytes.fromXml[Person]
538
+ ```
539
+
540
+ ## Printing XML
541
+
542
+ ### Basic Printing
543
+
544
+ ```scala
545
+ import zio.blocks.schema.xml._
546
+
547
+ val xml = Xml.Element("person", Xml.Element("name", Xml.Text("Alice")))
548
+
549
+ // Compact output
550
+ val compact: String = xml.print
551
+ // <person><name>Alice</name></person>
552
+
553
+ // Pretty-printed output
554
+ val pretty: String = xml.printPretty
555
+ // <person>
556
+ // <name>Alice</name>
557
+ // </person>
558
+
559
+ // Custom configuration
560
+ val custom: String = xml.print(WriterConfig(indentStep = 4))
561
+ ```
562
+
563
+ ## Type Testing and Access
564
+
565
+ ```scala
566
+ import zio.blocks.schema.xml._
567
+
568
+ val xml: Xml = Xml.Element("person")
569
+
570
+ // Type testing
571
+ xml.is(XmlType.Element) // true
572
+ xml.is(XmlType.Text) // false
573
+
574
+ // Type narrowing (returns Option)
575
+ val elem: Option[Xml.Element] = xml.as(XmlType.Element)
576
+
577
+ // Value extraction (returns Option)
578
+ val (name, attrs, children) = xml.unwrap(XmlType.Element).get
579
+ ```
580
+
581
+ ## Supported Types
582
+
583
+ All standard ZIO Blocks Schema types are supported:
584
+
585
+ **Numeric Types**:
586
+ - `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `Char`
587
+ - `BigInt`, `BigDecimal`
588
+
589
+ **Text Types**:
590
+ - `String`
591
+
592
+ **Special Types**:
593
+ - `Unit`, `UUID`, `Currency`
594
+
595
+ **Java Time Types**:
596
+ - `Instant`, `LocalDate`, `LocalTime`, `LocalDateTime`
597
+ - `OffsetTime`, `OffsetDateTime`, `ZonedDateTime`
598
+ - `Duration`, `Period`
599
+ - `Year`, `YearMonth`, `MonthDay`
600
+ - `DayOfWeek`, `Month`
601
+ - `ZoneId`, `ZoneOffset`
602
+
603
+ **Composite Types**:
604
+ - Records (case classes)
605
+ - Variants (sealed traits)
606
+ - Sequences (`List`, `Vector`, `Set`, etc.)
607
+ - Maps (`Map[K, V]`)
608
+ - Options (`Option[A]`)
609
+ - Wrappers (newtypes)
610
+
611
+ ## Error Handling
612
+
613
+ All decoding operations return `Either[SchemaError, A]` or `Either[XmlError, A]`:
614
+
615
+ ```scala
616
+ import zio.blocks.schema._
617
+ import zio.blocks.schema.xml._
618
+
619
+ case class Person(name: String, age: Int)
620
+ object Person {
621
+ implicit val schema: Schema[Person] = Schema.derived
622
+ }
623
+
624
+ val codec = Schema[Person].derive(XmlFormat)
625
+
626
+ // Decoding invalid XML
627
+ val invalid = "<Person><name>Alice</name></Person>" // missing age
628
+ val result = codec.decode(invalid)
629
+
630
+ result match {
631
+ case Right(person) => println(s"Decoded: $person")
632
+ case Left(error) =>
633
+ println(s"Error: ${error.getMessage}")
634
+ // Error information includes parse location and context
635
+ }
636
+ ```
637
+
638
+ ## Cross-Platform Support
639
+
640
+ The XML module works across all platforms:
641
+
642
+ - **JVM** - Full functionality
643
+ - **Scala.js** - Browser and Node.js
644
+
645
+ All features including parsing, writing, navigation, and patching work identically on both platforms.
646
+
647
+ ## Examples
648
+
649
+ ### Complete Example with Attributes and Namespaces
650
+
651
+ ```scala
652
+ import zio.blocks.schema._
653
+ import zio.blocks.schema.xml._
654
+
655
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
656
+ case class Entry(
657
+ @xmlAttribute() id: String,
658
+ title: String,
659
+ updated: String,
660
+ author: Author
661
+ )
662
+
663
+ case class Author(name: String, email: String)
664
+
665
+ object Entry {
666
+ implicit val authorSchema: Schema[Author] = Schema.derived
667
+ implicit val schema: Schema[Entry] = Schema.derived
668
+ }
669
+
670
+ val codec = Schema[Entry].derive(XmlFormat)
671
+
672
+ val entry = Entry(
673
+ id = "entry-1",
674
+ title = "First Post",
675
+ updated = "2024-01-01T00:00:00Z",
676
+ author = Author("Alice", "alice@example.com")
677
+ )
678
+
679
+ // Encode with pretty printing
680
+ val xml = codec.encodeToString(entry, WriterConfig.pretty)
681
+ println(xml)
682
+ // <atom:Entry xmlns:atom="http://www.w3.org/2005/Atom" id="entry-1">
683
+ // <title>First Post</title>
684
+ // <updated>2024-01-01T00:00:00Z</updated>
685
+ // <author>
686
+ // <name>Alice</name>
687
+ // <email>alice@example.com</email>
688
+ // </author>
689
+ // </atom:Entry>
690
+
691
+ // Decode back to typed value
692
+ val decoded = codec.decode(xml)
693
+ // Right(Entry("entry-1", "First Post", "2024-01-01T00:00:00Z", Author("Alice", "alice@example.com")))
694
+ ```
695
+
696
+ ### Navigation and Transformation
697
+
698
+ ```scala
699
+ import zio.blocks.schema._
700
+ import zio.blocks.schema.xml._
701
+
702
+ val xmlString = """
703
+ <library>
704
+ <books>
705
+ <book id="1">
706
+ <title>Functional Programming</title>
707
+ <price>49.99</price>
708
+ </book>
709
+ <book id="2">
710
+ <title>Advanced Scala</title>
711
+ <price>59.99</price>
712
+ </book>
713
+ </books>
714
+ </library>
715
+ """
716
+
717
+ val xml = XmlReader.read(xmlString).toOption.get
718
+
719
+ // Find all books
720
+ val books = xml.select.get("library").get("books").get("book")
721
+
722
+ // Extract all titles
723
+ val titles = books.get("title").toChunk.map { titleElem =>
724
+ titleElem.asInstanceOf[Xml.Element].children.head match {
725
+ case Xml.Text(text) => text
726
+ case _ => ""
727
+ }
728
+ }
729
+ // Chunk("Functional Programming", "Advanced Scala")
730
+
731
+ // Apply a patch to add a new book
732
+ val patch = XmlPatch.add(
733
+ p".library.books",
734
+ XmlBuilder.element("book")
735
+ .attr("id", "3")
736
+ .child(XmlBuilder.element("title").text("ZIO Essentials").build)
737
+ .child(XmlBuilder.element("price").text("39.99").build)
738
+ .build,
739
+ XmlPatch.Position.AppendChild
740
+ )
741
+
742
+ val updated = patch(xml)
743
+ ```