@zio.dev/zio-blocks 0.0.26 → 0.0.28

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,1304 @@
1
+ ---
2
+ id: xml
3
+ title: "XML"
4
+ ---
5
+
6
+ `Xml` is a **sealed trait representing XML nodes**. It provides a type-safe, immutable representation of all valid XML document structures including elements, text nodes, CDATA sections, comments, and processing instructions.
7
+
8
+ ```scala
9
+ sealed trait Xml {
10
+ def xmlType: XmlType
11
+ def is(xmlType: XmlType): Boolean
12
+ def as(xmlType: XmlType): Option[xmlType.Type]
13
+ def unwrap(xmlType: XmlType): Option[xmlType.Unwrap]
14
+ def print: String
15
+ def printPretty: String
16
+ def select: XmlSelection
17
+ }
18
+ ```
19
+
20
+ `Xml` supports:
21
+ - Type-safe representation of all XML node types
22
+ - Fluent navigation and querying with XmlSelection
23
+ - Schema-derived automatic codec generation
24
+ - Zero external dependencies
25
+ - Full cross-platform support (JVM and Scala.js)
26
+
27
+ ## Overview
28
+
29
+ Zero-dependency XML codec for ZIO Blocks Schema with cross-platform support.
30
+
31
+ 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.
32
+
33
+ Key features:
34
+
35
+ - **Zero Dependencies**: No external XML libraries required
36
+ - **Cross-Platform**: Full support for JVM and Scala.js
37
+ - **Schema-Based**: Automatic codec derivation from Schema definitions
38
+ - **XML AST**: Complete representation of XML documents
39
+ - **Fluent API**: Navigation and transformation with XmlSelection
40
+ - **Attributes**: First-class support for XML attributes via annotations
41
+ - **Namespaces**: XML namespace support with prefix handling
42
+
43
+ ## Installation
44
+
45
+ To use the schema-xml module, add the following dependency to your `build.sbt`:
46
+
47
+ ```scala
48
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-xml" % "0.0.14"
49
+ ```
50
+
51
+ ## Basic Usage
52
+
53
+ Start by deriving an XML codec from your Schema definition:
54
+
55
+ ### Deriving Codecs
56
+
57
+ To create an XML codec, use `Schema[A].derive(XmlFormat)`:
58
+
59
+ ```scala
60
+ import zio.blocks.schema._
61
+ import zio.blocks.schema.xml._
62
+
63
+ case class Person(name: String, age: Int)
64
+
65
+ object Person {
66
+ implicit val schema: Schema[Person] = Schema.derived
67
+ }
68
+
69
+ // Derive XML codec using the unified format API
70
+ val codec = Schema[Person].derive(XmlFormat)
71
+ ```
72
+
73
+ ### Encoding to XML
74
+
75
+ Encode your values to XML using the codec:
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
+ val person = Person("Alice", 30)
88
+ ```
89
+
90
+ Encode to XML bytes:
91
+
92
+ ```scala
93
+ val bytes: Array[Byte] = codec.encode(person)
94
+ // bytes: Array[Byte] = Array(
95
+ // 60,
96
+ // 80,
97
+ // 101,
98
+ // 114,
99
+ // 115,
100
+ // 111,
101
+ // 110,
102
+ // 62,
103
+ // 60,
104
+ // 110,
105
+ // 97,
106
+ // 109,
107
+ // 101,
108
+ // 62,
109
+ // 65,
110
+ // 108,
111
+ // 105,
112
+ // 99,
113
+ // 101,
114
+ // 60,
115
+ // 47,
116
+ // 110,
117
+ // 97,
118
+ // 109,
119
+ // 101,
120
+ // 62,
121
+ // 60,
122
+ // 97,
123
+ // 103,
124
+ // 101,
125
+ // 62,
126
+ // 51,
127
+ // 48,
128
+ // 60,
129
+ // 47,
130
+ // 97,
131
+ // 103,
132
+ // 101,
133
+ // 62,
134
+ // 60,
135
+ // 47,
136
+ // 80,
137
+ // 101,
138
+ // 114,
139
+ // 115,
140
+ // 111,
141
+ // 110,
142
+ // 62
143
+ // )
144
+ ```
145
+
146
+ Encode to XML string:
147
+
148
+ ```scala
149
+ val xmlString: String = codec.encodeToString(person)
150
+ // xmlString: String = "<Person><name>Alice</name><age>30</age></Person>"
151
+ ```
152
+
153
+ Encode to pretty-printed XML:
154
+
155
+ ```scala
156
+ val prettyXml = codec.encodeToString(person, WriterConfig.pretty)
157
+ // prettyXml: String = """<Person>
158
+ // <name>Alice</name>
159
+ // <age>30</age>
160
+ // </Person>"""
161
+ ```
162
+
163
+ ### Decoding from XML
164
+
165
+ Decode XML strings or bytes back to your typed values:
166
+
167
+ ```scala
168
+ import zio.blocks.schema._
169
+ import zio.blocks.schema.xml._
170
+
171
+ case class Person(name: String, age: Int)
172
+ object Person {
173
+ implicit val schema: Schema[Person] = Schema.derived
174
+ }
175
+
176
+ val codec = Schema[Person].derive(XmlFormat)
177
+
178
+ // Decode from XML string
179
+ val xml = "<Person><name>Alice</name><age>30</age></Person>"
180
+ ```
181
+
182
+ Decode the XML string and see the result:
183
+
184
+ ```scala
185
+ val result: Either[SchemaError, Person] = codec.decode(xml)
186
+ // result: Either[SchemaError, Person] = Right(
187
+ // Person(name = "Alice", age = 30)
188
+ // )
189
+ ```
190
+
191
+ You can also decode from bytes:
192
+
193
+ ```scala
194
+ val bytes = xml.getBytes("UTF-8")
195
+ ```
196
+
197
+ ```scala
198
+ val fromBytes: Either[SchemaError, Person] = codec.decode(bytes)
199
+ // fromBytes: Either[SchemaError, Person] = Right(
200
+ // Person(name = "Alice", age = 30)
201
+ // )
202
+ ```
203
+
204
+ ## XML AST
205
+
206
+ The `Xml` ADT represents all valid XML node types:
207
+
208
+ ```
209
+ Xml
210
+ ├── Xml.Element (element with name, attributes, children)
211
+ ├── Xml.Text (character data)
212
+ ├── Xml.CData (unparsed character data)
213
+ ├── Xml.Comment (XML comment)
214
+ └── Xml.ProcessingInstruction (processing instruction)
215
+ ```
216
+
217
+ ### Creating XML Nodes
218
+
219
+ Construct XML nodes directly using the case class constructors:
220
+
221
+ ```scala
222
+ import zio.blocks.schema.xml._
223
+ import zio.blocks.chunk.Chunk
224
+
225
+ // Create elements
226
+ val simple = Xml.Element("person")
227
+ val withChildren = Xml.Element("person", Xml.Text("Alice"))
228
+
229
+ // Create with XmlName (for namespaces)
230
+ val namespaced = Xml.Element(
231
+ XmlName("person", "http://example.com"),
232
+ Chunk.empty,
233
+ Chunk.empty
234
+ )
235
+
236
+ // Create text nodes
237
+ val text = Xml.Text("Hello, World!")
238
+ val cdata = Xml.CData("<script>...</script>")
239
+
240
+ // Create comments and processing instructions
241
+ val comment = Xml.Comment("This is a comment")
242
+ val pi = Xml.ProcessingInstruction("xml-stylesheet", "href=\"style.css\"")
243
+ ```
244
+
245
+ ### XmlName
246
+
247
+ `XmlName` represents an element or attribute name with optional namespace. Create instances with different namespace configurations:
248
+
249
+ ```scala
250
+ import zio.blocks.schema.xml.XmlName
251
+
252
+ // Local name only
253
+ val simple = XmlName("person")
254
+ simple.localName // "person"
255
+ simple.namespace // ""
256
+ simple.prefix // ""
257
+
258
+ // With namespace URI
259
+ val ns = XmlName("person", "http://example.com/ns")
260
+
261
+ // With prefix (for prefixed elements like atom:feed)
262
+ val prefixed = XmlName("feed", Some("atom"), None)
263
+ prefixed.localName // "feed"
264
+ prefixed.prefix.contains("atom") // true
265
+ ```
266
+
267
+ ## XmlBuilder
268
+
269
+ Construct XML documents programmatically with a fluent API:
270
+
271
+ ```scala
272
+ import zio.blocks.schema.xml._
273
+
274
+ // Build an element with attributes and children
275
+ val doc = XmlBuilder.element("person")
276
+ .attr("id", "123")
277
+ .attr("status", "active")
278
+ .child(XmlBuilder.element("name").text("Alice").build)
279
+ .child(XmlBuilder.element("age").text("30").build)
280
+ .build
281
+
282
+ // Result:
283
+ // <person id="123" status="active">
284
+ // <name>Alice</name>
285
+ // <age>30</age>
286
+ // </person>
287
+
288
+ // Create other node types
289
+ val textNode = XmlBuilder.text("content")
290
+ val cdataNode = XmlBuilder.cdata("<![CDATA[raw content]]>")
291
+ val commentNode = XmlBuilder.comment("comment text")
292
+ ```
293
+
294
+ ## Configuration
295
+
296
+ The schema-xml module provides configuration options for both parsing and writing:
297
+
298
+ ### WriterConfig
299
+
300
+ Use `WriterConfig` to control XML output formatting:
301
+
302
+ ```scala
303
+ import zio.blocks.schema.xml.WriterConfig
304
+
305
+ // Compact output (default)
306
+ val compact = WriterConfig.default
307
+ // <Person><name>Alice</name></Person>
308
+
309
+ // Pretty-printed with 2-space indentation
310
+ val pretty = WriterConfig.pretty
311
+ // <Person>
312
+ // <name>Alice</name>
313
+ // </Person>
314
+
315
+ // With XML declaration
316
+ val withDecl = WriterConfig.withDeclaration
317
+ // <?xml version="1.0" encoding="UTF-8"?>
318
+ // <Person><name>Alice</name></Person>
319
+
320
+ // Custom configuration
321
+ val custom = WriterConfig(
322
+ indentStep = 4,
323
+ includeDeclaration = true,
324
+ encoding = "UTF-8"
325
+ )
326
+ ```
327
+
328
+ | Option | Default | Description |
329
+ |--------|---------|-------------|
330
+ | `indentStep` | `0` | Spaces per indentation level (0 = compact) |
331
+ | `includeDeclaration` | `false` | Include XML declaration |
332
+ | `encoding` | `"UTF-8"` | Character encoding in declaration |
333
+
334
+ ### ReaderConfig
335
+
336
+ Controls XML parsing behavior and security limits:
337
+
338
+ ```scala
339
+ import zio.blocks.schema.xml.ReaderConfig
340
+
341
+ // Default configuration
342
+ val default = ReaderConfig.default
343
+
344
+ // Custom limits
345
+ val custom = ReaderConfig(
346
+ maxDepth = 100,
347
+ maxAttributes = 50,
348
+ maxTextLength = 1000000,
349
+ preserveWhitespace = true
350
+ )
351
+ ```
352
+
353
+ | Option | Default | Description |
354
+ |--------|---------|-------------|
355
+ | `maxDepth` | `1000` | Maximum element nesting depth |
356
+ | `maxAttributes` | `1000` | Maximum attributes per element |
357
+ | `maxTextLength` | `10000000` | Maximum text content length |
358
+ | `preserveWhitespace` | `false` | Preserve whitespace in text nodes |
359
+
360
+ ## Attributes
361
+
362
+ Encode case class fields as XML attributes using the `@xmlAttribute` annotation:
363
+
364
+ ```scala
365
+ import zio.blocks.schema._
366
+ import zio.blocks.schema.xml._
367
+
368
+ case class Person(
369
+ @xmlAttribute() id: String,
370
+ @xmlAttribute("status") active: String,
371
+ name: String,
372
+ age: Int
373
+ )
374
+
375
+ object Person {
376
+ implicit val schema: Schema[Person] = Schema.derived
377
+ }
378
+
379
+ val codec = Schema[Person].derive(XmlFormat)
380
+ val person = Person("123", "active", "Alice", 30)
381
+ ```
382
+
383
+ Encode the person with attributes:
384
+
385
+ ```scala
386
+ val xml = codec.encodeToString(person)
387
+ // xml: String = "<Person><id>123</id><active>active</active><name>Alice</name><age>30</age></Person>"
388
+ ```
389
+
390
+ The `@xmlAttribute` annotation accepts an optional custom name:
391
+
392
+ - `@xmlAttribute()` - Uses the field name as the attribute name
393
+ - `@xmlAttribute("customName")` - Uses the provided name as the attribute name
394
+
395
+ ## Namespaces
396
+
397
+ Support for XML namespaces with the `@xmlNamespace` annotation:
398
+
399
+ ```scala
400
+ import zio.blocks.schema._
401
+ import zio.blocks.schema.xml._
402
+
403
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
404
+ case class Feed(
405
+ title: String,
406
+ updated: String
407
+ )
408
+
409
+ object Feed {
410
+ implicit val schema: Schema[Feed] = Schema.derived
411
+ }
412
+
413
+ val codec = Schema[Feed].derive(XmlFormat)
414
+ val feed = Feed("My Blog", "2024-01-01T00:00:00Z")
415
+ ```
416
+
417
+ Encode the feed with a namespace prefix:
418
+
419
+ ```scala
420
+ val xml = codec.encodeToString(feed)
421
+ // xml: String = "<Feed><title>My Blog</title><updated>2024-01-01T00:00:00Z</updated></Feed>"
422
+ ```
423
+
424
+ Without a prefix (default namespace):
425
+
426
+ ```scala
427
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom")
428
+ case class Feed(title: String)
429
+
430
+ object Feed {
431
+ implicit val schema: Schema[Feed] = Schema.derived
432
+ }
433
+
434
+ val codec = Schema[Feed].derive(XmlFormat)
435
+ val feed = Feed("My Blog")
436
+ ```
437
+
438
+ Encode the feed with default namespace:
439
+
440
+ ```scala
441
+ val xml = codec.encodeToString(feed)
442
+ // xml: String = "<Feed><title>My Blog</title></Feed>"
443
+ ```
444
+
445
+ ## XmlSelection
446
+
447
+ `XmlSelection` provides a fluent API for navigating and querying XML structures:
448
+
449
+ ### Navigation
450
+
451
+ Navigate to child elements, filter by type, and extract content:
452
+
453
+ ```scala
454
+ import zio.blocks.schema.xml._
455
+
456
+ val xml = XmlReader.read("""
457
+ <library>
458
+ <books>
459
+ <book id="1">
460
+ <title>Functional Programming</title>
461
+ <author>Alice</author>
462
+ </book>
463
+ <book id="2">
464
+ <title>Advanced Scala</title>
465
+ <author>Bob</author>
466
+ </book>
467
+ </books>
468
+ </library>
469
+ """).toOption.get
470
+
471
+ // Navigate to child elements
472
+ val books = xml.select.get("library").get("books")
473
+
474
+ // Navigate by index
475
+ val firstBook = books.get("book")(0)
476
+ ```
477
+
478
+ Extract text content from the first book:
479
+
480
+ ```scala
481
+ val title: Either[XmlError, String] = firstBook.get("title").text
482
+ // title: Either[XmlError, String] = Left(
483
+ // zio.blocks.schema.xml.XmlError: Expected single value but got 0
484
+ // )
485
+ ```
486
+
487
+ You can also navigate descendants recursively:
488
+
489
+ ```scala
490
+ val allTitles = xml.select.descendant("title")
491
+ // allTitles: XmlSelection = XmlSelection(
492
+ // Right(
493
+ // IndexedSeq(
494
+ // Element(
495
+ // name = XmlName(localName = "title", prefix = None, namespace = None),
496
+ // attributes = IndexedSeq(),
497
+ // children = IndexedSeq(Text("Functional Programming"))
498
+ // ),
499
+ // Element(
500
+ // name = XmlName(localName = "title", prefix = None, namespace = None),
501
+ // attributes = IndexedSeq(),
502
+ // children = IndexedSeq(Text("Advanced Scala"))
503
+ // )
504
+ // )
505
+ // )
506
+ // )
507
+ ```
508
+
509
+ ### Filtering
510
+
511
+ Filter selections by node type or custom predicates:
512
+
513
+ ```scala
514
+ import zio.blocks.schema.xml._
515
+
516
+ val selection: XmlSelection = ???
517
+
518
+ // Filter by type
519
+ val elements = selection.elements
520
+ val texts = selection.texts
521
+ val comments = selection.comments
522
+
523
+ // Custom filtering
524
+ val filtered = selection.filter(xml => xml.is(XmlType.Element))
525
+ ```
526
+
527
+ ### Terminal Operations
528
+
529
+ Execute a selection to extract values or convert to other formats:
530
+
531
+ ```scala
532
+ import zio.blocks.schema.xml._
533
+
534
+ val selection: XmlSelection = ???
535
+
536
+ // Get single value (fails if not exactly one)
537
+ val one: Either[XmlError, Xml] = selection.one
538
+
539
+ // Get any value (first of many)
540
+ val any: Either[XmlError, Xml] = selection.any
541
+
542
+ // Get all values as a single XML element
543
+ val all: Either[XmlError, Xml] = selection.all
544
+
545
+ // Convert to chunk
546
+ val chunk = selection.toChunk
547
+
548
+ // Extract text content
549
+ val text: Either[XmlError, String] = selection.text
550
+ val allText: String = selection.textContent
551
+ ```
552
+
553
+ ### Combinators
554
+
555
+ Combine and transform selections using monadic operations:
556
+
557
+ ```scala
558
+ import zio.blocks.schema.xml._
559
+
560
+ val selection1: XmlSelection = ???
561
+ val selection2: XmlSelection = ???
562
+
563
+ // Map over selections
564
+ val mapped = selection1.map(xml => xml)
565
+
566
+ // FlatMap for chaining
567
+ val nested = selection1.flatMap(xml => XmlSelection.succeed(xml))
568
+
569
+ // Combine selections
570
+ val combined = selection1 ++ selection2
571
+
572
+ // Alternative on failure
573
+ val withFallback = selection1.orElse(selection2)
574
+ ```
575
+
576
+ ## XmlPatch
577
+
578
+ `XmlPatch` provides composable XML modification operations:
579
+
580
+ ### Creating Patches
581
+
582
+ Create patches for add, remove, replace, and attribute operations:
583
+
584
+ ```scala
585
+ import zio.blocks.schema._
586
+ import zio.blocks.schema.xml._
587
+
588
+ val path = p".library.books.book"
589
+
590
+ // Add content
591
+ val addPatch = XmlPatch.add(
592
+ path,
593
+ XmlBuilder.element("book").attr("id", "3").build,
594
+ XmlPatch.Position.AppendChild
595
+ )
596
+
597
+ // Remove element
598
+ val removePatch = XmlPatch.remove(path)
599
+
600
+ // Replace element
601
+ val replacePatch = XmlPatch.replace(
602
+ path,
603
+ XmlBuilder.element("book").attr("id", "999").build
604
+ )
605
+
606
+ // Set attribute
607
+ val attrPatch = XmlPatch.setAttribute(path, "featured", "true")
608
+
609
+ // Remove attribute
610
+ val removeAttrPatch = XmlPatch.removeAttribute(path, "id")
611
+ ```
612
+
613
+ ### Position Options
614
+
615
+ Position options control where new content is inserted relative to the target:
616
+
617
+ ```scala
618
+ import zio.blocks.schema.xml.XmlPatch.Position
619
+
620
+ Position.Before // Insert before the target element
621
+ Position.After // Insert after the target element
622
+ Position.PrependChild // Insert as first child of target
623
+ Position.AppendChild // Insert as last child of target
624
+ ```
625
+
626
+ ### Applying Patches
627
+
628
+ Apply a patch to an XML document to produce a modified result:
629
+
630
+ ```scala
631
+ import zio.blocks.schema._
632
+ import zio.blocks.schema.xml._
633
+
634
+ val xml: Xml = ???
635
+ val patch = XmlPatch.setAttribute(p".person", "active", "true")
636
+
637
+ // Apply the patch
638
+ val result: Either[XmlError, Xml] = patch(xml)
639
+ ```
640
+
641
+ ### Composing Patches
642
+
643
+ Combine multiple patches to apply transformations in sequence:
644
+
645
+ ```scala
646
+ import zio.blocks.schema._
647
+ import zio.blocks.schema.xml._
648
+
649
+ val patch1 = XmlPatch.setAttribute(p".person", "id", "123")
650
+ val patch2 = XmlPatch.add(
651
+ p".person",
652
+ XmlBuilder.element("email").text("alice@example.com").build,
653
+ XmlPatch.Position.AppendChild
654
+ )
655
+
656
+ // Compose patches - applies patch1, then patch2
657
+ val combined = patch1 ++ patch2
658
+ ```
659
+
660
+ ## XmlEncoder and XmlDecoder
661
+
662
+ For more fine-grained control over XML serialization, use the separate `XmlEncoder` and `XmlDecoder` traits:
663
+
664
+ ### XmlEncoder
665
+
666
+ `XmlEncoder[A]` provides type-safe XML encoding:
667
+
668
+ ```scala
669
+ import zio.blocks.schema._
670
+ import zio.blocks.schema.xml._
671
+
672
+ // Automatic derivation from Schema
673
+ case class Person(name: String, age: Int)
674
+ object Person {
675
+ implicit val schema: Schema[Person] = Schema.derived
676
+ implicit val encoder: XmlEncoder[Person] = XmlEncoder.fromSchema
677
+ }
678
+
679
+ val person = Person("Alice", 30)
680
+ val xml: Xml = XmlEncoder[Person].encode(person)
681
+ ```
682
+
683
+ #### Creating custom encoders
684
+
685
+ Create custom encoders from functions or using contravariance:
686
+
687
+ ```scala
688
+ import zio.blocks.schema.xml._
689
+
690
+ // Create from a function
691
+ val customEncoder: XmlEncoder[Int] = XmlEncoder.instance(n =>
692
+ Xml.Element("number", Xml.Text(n.toString))
693
+ )
694
+
695
+ // Map with contravariance - encode a wrapper type
696
+ case class UserId(value: Int)
697
+
698
+ val userIdEncoder: XmlEncoder[UserId] =
699
+ customEncoder.contramap[UserId](_.value)
700
+ ```
701
+
702
+ #### Using implicit resolution
703
+
704
+ Leverage implicit resolution for automatic encoder derivation:
705
+
706
+ ```scala
707
+ import zio.blocks.schema._
708
+ import zio.blocks.schema.xml._
709
+
710
+ case class Product(id: String, price: Double)
711
+ object Product {
712
+ implicit val schema: Schema[Product] = Schema.derived
713
+ }
714
+
715
+ // No explicit encoder needed - derives automatically
716
+ def encodeProduct[A](value: A)(implicit encoder: XmlEncoder[A]): Xml =
717
+ encoder.encode(value)
718
+
719
+ val result = encodeProduct(Product("item-1", 99.99))
720
+ ```
721
+
722
+ ### XmlDecoder
723
+
724
+ `XmlDecoder[A]` provides type-safe XML decoding with error handling:
725
+
726
+ ```scala
727
+ import zio.blocks.schema._
728
+ import zio.blocks.schema.xml._
729
+
730
+ // Automatic derivation from Schema
731
+ case class Person(name: String, age: Int)
732
+ object Person {
733
+ implicit val schema: Schema[Person] = Schema.derived
734
+ implicit val decoder: XmlDecoder[Person] = XmlDecoder.fromSchema
735
+ }
736
+
737
+ val xml = Xml.Element("Person",
738
+ Xml.Element("name", Xml.Text("Alice")),
739
+ Xml.Element("age", Xml.Text("30"))
740
+ )
741
+ ```
742
+
743
+ Decode the XML:
744
+
745
+ ```scala
746
+ val result: Either[XmlError, Person] = XmlDecoder[Person].decode(xml)
747
+ // result: Either[XmlError, Person] = Right(Person(name = "Alice", age = 30))
748
+ ```
749
+
750
+ #### Creating custom decoders
751
+
752
+ Create custom decoders from functions or using covariance:
753
+
754
+ ```scala
755
+ import zio.blocks.schema.xml._
756
+ import zio.blocks.chunk.Chunk
757
+
758
+ // Create from a function
759
+ val numberDecoder: XmlDecoder[Int] = XmlDecoder.instance { xml =>
760
+ xml match {
761
+ case Xml.Element(_, _, Chunk(Xml.Text(text), _*)) =>
762
+ text.toIntOption.toRight(XmlError("Invalid number"))
763
+ case _ => Left(XmlError("Expected number element"))
764
+ }
765
+ }
766
+
767
+ // Map for covariance - decode to a wrapper type
768
+ case class UserId(value: Int)
769
+
770
+ val userIdDecoder: XmlDecoder[UserId] =
771
+ numberDecoder.map(UserId(_))
772
+ ```
773
+
774
+ #### Error handling with decoders
775
+
776
+ Handle decoding errors gracefully with fallback strategies:
777
+
778
+ ```scala
779
+ import zio.blocks.schema._
780
+ import zio.blocks.schema.xml._
781
+
782
+ case class Person(name: String, age: Int)
783
+ object Person {
784
+ implicit val schema: Schema[Person] = Schema.derived
785
+ }
786
+
787
+ def decodeWithFallback[A](
788
+ xml: Xml,
789
+ fallback: A
790
+ )(implicit decoder: XmlDecoder[A]): A = {
791
+ decoder.decode(xml).getOrElse(fallback)
792
+ }
793
+
794
+ val invalidXml = Xml.Element("Empty")
795
+ val defaultPerson = Person("Unknown", 0)
796
+ val result = decodeWithFallback(invalidXml, defaultPerson)
797
+ // Person("Unknown", 0)
798
+ ```
799
+
800
+ #### Combining encoders and decoders
801
+
802
+ Round-trip values by encoding and decoding:
803
+
804
+ ```scala
805
+ import zio.blocks.schema._
806
+ import zio.blocks.schema.xml._
807
+ import zio.blocks.schema.xml.syntax._
808
+
809
+ case class Message(id: String, text: String)
810
+ object Message {
811
+ implicit val schema: Schema[Message] = Schema.derived
812
+ }
813
+
814
+ // Round-trip: encode then decode
815
+ val message = Message("msg-1", "Hello")
816
+ val encoded: Xml = message.toXml
817
+ ```
818
+
819
+ Decode the encoded value:
820
+
821
+ ```scala
822
+ val result: Either[XmlError, Message] =
823
+ implicitly[XmlDecoder[Message]].decode(encoded)
824
+ // result: Either[XmlError, Message] = Right(
825
+ // Message(id = "msg-1", text = "Hello")
826
+ // )
827
+ ```
828
+
829
+ ## Extension Syntax
830
+
831
+ When a `Schema` is in scope, use convenient extension methods:
832
+
833
+ ```scala
834
+ import zio.blocks.schema._
835
+ import zio.blocks.schema.xml._
836
+ import zio.blocks.schema.xml.syntax._
837
+
838
+ case class Person(name: String, age: Int)
839
+ object Person {
840
+ implicit val schema: Schema[Person] = Schema.derived
841
+ }
842
+
843
+ val person = Person("Alice", 30)
844
+
845
+ // Encode to XML AST
846
+ val xml: Xml = person.toXml
847
+
848
+ // Encode to XML string
849
+ val xmlString: String = person.toXmlString
850
+ // <Person><name>Alice</name><age>30</age></Person>
851
+
852
+ // Encode to bytes
853
+ val bytes: Array[Byte] = person.toXmlBytes
854
+
855
+ // Decode from XML string
856
+ val parsed: Either[SchemaError, Person] =
857
+ "<Person><name>Bob</name><age>25</age></Person>".fromXml[Person]
858
+
859
+ // Decode from bytes
860
+ val fromBytes: Either[SchemaError, Person] = bytes.fromXml[Person]
861
+ ```
862
+
863
+ ## Printing XML
864
+
865
+ Format XML documents using compact or pretty-printed output. You can convert XML to string with different formatting options:
866
+
867
+ ```scala
868
+ import zio.blocks.schema.xml._
869
+
870
+ val xml = Xml.Element("person", Xml.Element("name", Xml.Text("Alice")))
871
+ ```
872
+
873
+ Compact output:
874
+
875
+ ```scala
876
+ val compact: String = xml.print
877
+ // compact: String = "<person><name>Alice</name></person>"
878
+ ```
879
+
880
+ Pretty-printed output:
881
+
882
+ ```scala
883
+ val pretty: String = xml.printPretty
884
+ // pretty: String = """<person>
885
+ // <name>Alice</name>
886
+ // </person>"""
887
+ ```
888
+
889
+ Custom configuration:
890
+
891
+ ```scala
892
+ val custom: String = xml.print(WriterConfig(indentStep = 4))
893
+ ```
894
+
895
+ ## Type Testing and Access
896
+
897
+ Test and extract values from XML nodes using type guards and unwrapping:
898
+
899
+ ```scala
900
+ import zio.blocks.schema.xml._
901
+
902
+ val xml: Xml = Xml.Element("person")
903
+
904
+ // Type testing
905
+ xml.is(XmlType.Element) // true
906
+ xml.is(XmlType.Text) // false
907
+
908
+ // Type narrowing (returns Option)
909
+ val elem: Option[Xml.Element] = xml.as(XmlType.Element)
910
+
911
+ // Value extraction (returns Option)
912
+ val (name, attrs, children) = xml.unwrap(XmlType.Element).get
913
+ ```
914
+
915
+ ## Supported Types
916
+
917
+ All standard ZIO Blocks Schema types are supported:
918
+
919
+ **Numeric Types**:
920
+ - `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `Char`
921
+ - `BigInt`, `BigDecimal`
922
+
923
+ **Text Types**:
924
+ - `String`
925
+
926
+ **Special Types**:
927
+ - `Unit`, `UUID`, `Currency`
928
+
929
+ **Java Time Types**:
930
+ - `Instant`, `LocalDate`, `LocalTime`, `LocalDateTime`
931
+ - `OffsetTime`, `OffsetDateTime`, `ZonedDateTime`
932
+ - `Duration`, `Period`
933
+ - `Year`, `YearMonth`, `MonthDay`
934
+ - `DayOfWeek`, `Month`
935
+ - `ZoneId`, `ZoneOffset`
936
+
937
+ **Composite Types**:
938
+ - Records (case classes)
939
+ - Variants (sealed traits)
940
+ - Sequences (`List`, `Vector`, `Set`, etc.)
941
+ - Maps (`Map[K, V]`)
942
+ - Options (`Option[A]`)
943
+ - Wrappers (newtypes)
944
+
945
+ ## XmlBinaryCodec
946
+
947
+ `XmlBinaryCodec[A]` is the low-level codec interface that bridges Schema definitions with XML serialization. While usually derived automatically, you can work with it directly:
948
+
949
+ ```scala
950
+ import zio.blocks.schema._
951
+ import zio.blocks.schema.xml._
952
+
953
+ case class Person(name: String, age: Int)
954
+ object Person {
955
+ implicit val schema: Schema[Person] = Schema.derived
956
+ }
957
+
958
+ // Get the underlying binary codec
959
+ val codec: XmlBinaryCodec[Person] =
960
+ Schema[Person].derive(XmlBinaryCodecDeriver)
961
+
962
+ // Encode to Xml directly
963
+ val person = Person("Alice", 30)
964
+ val xml: Xml = codec.encodeValue(person)
965
+
966
+ // Decode from Xml directly
967
+ val decoded: Either[XmlError, Person] = codec.decodeValue(xml)
968
+ ```
969
+
970
+ **XmlBinaryCodec supports all Schema types:**
971
+ - Primitives (Int, String, Boolean, etc.)
972
+ - Java time types (Instant, LocalDate, Duration, etc.)
973
+ - Records (case classes with field-level configuration)
974
+ - Variants (sealed traits with discriminators)
975
+ - Collections (List, Vector, Map, etc.)
976
+ - Optional fields (Option[A])
977
+ - Custom wrappers and dynamic values
978
+
979
+ ## Error Handling
980
+
981
+ All decoding operations return `Either[SchemaError, A]` or `Either[XmlError, A]`. The `XmlError` type provides detailed error information:
982
+
983
+ ```scala
984
+ import zio.blocks.schema._
985
+ import zio.blocks.schema.xml._
986
+
987
+ case class Person(name: String, age: Int)
988
+ object Person {
989
+ implicit val schema: Schema[Person] = Schema.derived
990
+ }
991
+
992
+ val codec = Schema[Person].derive(XmlFormat)
993
+
994
+ // Decoding invalid XML
995
+ val invalid = "<Person><name>Alice</name></Person>" // missing age
996
+ val result = codec.decode(invalid)
997
+
998
+ result match {
999
+ case Right(person) => println(s"Decoded: $person")
1000
+ case Left(error) =>
1001
+ println(s"Error: ${error.getMessage}")
1002
+ // Error information includes parse location and context
1003
+ }
1004
+ ```
1005
+
1006
+ `XmlError` provides detailed error information for debugging:
1007
+
1008
+ ```scala
1009
+ import zio.blocks.schema.xml._
1010
+
1011
+ val error = XmlError("Parse failed")
1012
+
1013
+ // Error message
1014
+ val message: String = error.getMessage
1015
+
1016
+ // Get error message for inspection
1017
+ val errorMsg: String = error.getMessage
1018
+ ```
1019
+
1020
+ Error handling best practices:
1021
+
1022
+ ```scala
1023
+ import zio.blocks.schema._
1024
+ import zio.blocks.schema.xml._
1025
+
1026
+ case class Config(database: String, port: Int)
1027
+ object Config {
1028
+ implicit val schema: Schema[Config] = Schema.derived
1029
+ }
1030
+
1031
+ def loadConfig(xml: String): Either[String, Config] = {
1032
+ val codec = Schema[Config].derive(XmlFormat)
1033
+ codec.decode(xml).left.map { error =>
1034
+ s"Configuration error: ${error.getMessage}\nCheck XML format and required fields."
1035
+ }
1036
+ }
1037
+ ```
1038
+
1039
+ ## Cross-Platform Support
1040
+
1041
+ The XML module works across all platforms:
1042
+
1043
+ - **JVM** - Full functionality
1044
+ - **Scala.js** - Browser and Node.js
1045
+
1046
+ All features including parsing, writing, navigation, and patching work identically on both platforms.
1047
+
1048
+ ## Examples
1049
+
1050
+ These examples demonstrate common use cases and patterns with the XML module:
1051
+
1052
+ ### Complete Example with Attributes and Namespaces
1053
+
1054
+ Define a schema with attributes and namespaces, then encode and decode:
1055
+
1056
+ ```scala
1057
+ import zio.blocks.schema._
1058
+ import zio.blocks.schema.xml._
1059
+
1060
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
1061
+ case class Entry(
1062
+ @xmlAttribute() id: String,
1063
+ title: String,
1064
+ updated: String,
1065
+ author: Author
1066
+ )
1067
+
1068
+ case class Author(name: String, email: String)
1069
+
1070
+ object Entry {
1071
+ implicit val authorSchema: Schema[Author] = Schema.derived
1072
+ implicit val schema: Schema[Entry] = Schema.derived
1073
+ }
1074
+
1075
+ val codec = Schema[Entry].derive(XmlFormat)
1076
+
1077
+ val entry = Entry(
1078
+ id = "entry-1",
1079
+ title = "First Post",
1080
+ updated = "2024-01-01T00:00:00Z",
1081
+ author = Author("Alice", "alice@example.com")
1082
+ )
1083
+ ```
1084
+
1085
+ Encode the entry with pretty printing:
1086
+
1087
+ ```scala
1088
+ val xmlStr = codec.encodeToString(entry, WriterConfig.pretty)
1089
+ // xmlStr: String = """<Entry>
1090
+ // <id>entry-1</id>
1091
+ // <title>First Post</title>
1092
+ // <updated>2024-01-01T00:00:00Z</updated>
1093
+ // <author>
1094
+ // <name>Alice</name>
1095
+ // <email>alice@example.com</email>
1096
+ // </author>
1097
+ // </Entry>"""
1098
+ ```
1099
+
1100
+ Decode the XML back to a typed value:
1101
+
1102
+ ```scala
1103
+ val decoded = codec.decode(xmlStr)
1104
+ // decoded: Either[SchemaError, Entry] = Right(
1105
+ // Entry(
1106
+ // id = "entry-1",
1107
+ // title = "First Post",
1108
+ // updated = "2024-01-01T00:00:00Z",
1109
+ // author = Author(name = "Alice", email = "alice@example.com")
1110
+ // )
1111
+ // )
1112
+ ```
1113
+
1114
+ ### Navigation and Transformation
1115
+
1116
+ Find elements, extract data, and apply patches to modify XML:
1117
+
1118
+ ```scala
1119
+ import zio.blocks.schema._
1120
+ import zio.blocks.schema.xml._
1121
+
1122
+ val xmlString = """
1123
+ <library>
1124
+ <books>
1125
+ <book id="1">
1126
+ <title>Functional Programming</title>
1127
+ <price>49.99</price>
1128
+ </book>
1129
+ <book id="2">
1130
+ <title>Advanced Scala</title>
1131
+ <price>59.99</price>
1132
+ </book>
1133
+ </books>
1134
+ </library>
1135
+ """
1136
+
1137
+ val xml = XmlReader.read(xmlString).toOption.get
1138
+
1139
+ // Find all books
1140
+ val books = xml.select.get("library").get("books").get("book")
1141
+
1142
+ // Extract all titles
1143
+ val titles = books.get("title").toChunk.map { titleElem =>
1144
+ titleElem.asInstanceOf[Xml.Element].children.head match {
1145
+ case Xml.Text(text) => text
1146
+ case _ => ""
1147
+ }
1148
+ }
1149
+ // Chunk("Functional Programming", "Advanced Scala")
1150
+
1151
+ // Apply a patch to add a new book
1152
+ val patch = XmlPatch.add(
1153
+ p".library.books",
1154
+ XmlBuilder.element("book")
1155
+ .attr("id", "3")
1156
+ .child(XmlBuilder.element("title").text("ZIO Essentials").build)
1157
+ .child(XmlBuilder.element("price").text("39.99").build)
1158
+ .build,
1159
+ XmlPatch.Position.AppendChild
1160
+ )
1161
+
1162
+ val updated = patch(xml)
1163
+ ```
1164
+
1165
+ ## Real-World Examples
1166
+
1167
+ Learn by examining practical examples of XML codecs in action:
1168
+
1169
+ ### RSS Feed Parsing
1170
+
1171
+ Parse RSS feeds by defining a schema and decoding XML:
1172
+
1173
+ ```scala
1174
+ import zio.blocks.schema._
1175
+ import zio.blocks.schema.xml._
1176
+
1177
+ @xmlNamespace(uri = "http://www.rss.org/", prefix = "rss")
1178
+ case class Item(
1179
+ @xmlAttribute() guid: String,
1180
+ title: String,
1181
+ link: String,
1182
+ pubDate: String,
1183
+ description: String
1184
+ )
1185
+
1186
+ @xmlNamespace(uri = "http://www.rss.org/", prefix = "rss")
1187
+ case class Channel(
1188
+ title: String,
1189
+ link: String,
1190
+ description: String,
1191
+ items: List[Item]
1192
+ )
1193
+
1194
+ object Channel {
1195
+ implicit val itemSchema: Schema[Item] = Schema.derived
1196
+ implicit val schema: Schema[Channel] = Schema.derived
1197
+ }
1198
+
1199
+ // Parse RSS feed from string
1200
+ val feedXml = """<rss:Channel xmlns:rss="http://www.rss.org/">
1201
+ <title>Tech Blog</title>
1202
+ <link>https://example.com</link>
1203
+ <description>Latest tech articles</description>
1204
+ <items>
1205
+ <Item guid="1">
1206
+ <title>Functional Programming in Scala</title>
1207
+ <link>https://example.com/fp</link>
1208
+ <pubDate>2024-01-01</pubDate>
1209
+ <description>Deep dive into FP concepts</description>
1210
+ </Item>
1211
+ </items>
1212
+ </rss:Channel>"""
1213
+
1214
+ val codec = Schema[Channel].derive(XmlFormat)
1215
+ val result: Either[SchemaError, Channel] = codec.decode(feedXml)
1216
+ ```
1217
+
1218
+ ### Atom Feed with Advanced Features
1219
+
1220
+ Work with Atom feeds using attributes, multiple entries, and custom configurations:
1221
+
1222
+ ```scala
1223
+ import zio.blocks.schema._
1224
+ import zio.blocks.schema.xml._
1225
+
1226
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
1227
+ case class Entry(
1228
+ @xmlAttribute() id: String,
1229
+ title: String,
1230
+ author: String,
1231
+ updated: String,
1232
+ @xmlAttribute("href") link: String
1233
+ )
1234
+
1235
+ @xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
1236
+ case class Feed(
1237
+ @xmlAttribute() id: String,
1238
+ title: String,
1239
+ updated: String,
1240
+ entries: List[Entry]
1241
+ )
1242
+
1243
+ object Feed {
1244
+ implicit val entrySchema: Schema[Entry] = Schema.derived
1245
+ implicit val schema: Schema[Feed] = Schema.derived
1246
+ }
1247
+
1248
+ // Encode feed to XML with custom formatting
1249
+ val feed = Feed(
1250
+ id = "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6",
1251
+ title = "Example Feed",
1252
+ updated = "2024-01-01T18:30:02Z",
1253
+ entries = List(
1254
+ Entry(
1255
+ id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a",
1256
+ title = "Atom-Powered Robots Run Amok",
1257
+ author = "John Doe",
1258
+ updated = "2024-01-01T18:30:02Z",
1259
+ link = "http://example.org/2024/01/entry"
1260
+ )
1261
+ )
1262
+ )
1263
+
1264
+ val codec = Schema[Feed].derive(XmlFormat)
1265
+ val xmlOutput = codec.encodeToString(feed, WriterConfig.pretty)
1266
+ ```
1267
+
1268
+ ### Sitemap XML
1269
+
1270
+ Generate sitemap XML with URLs and optional metadata fields:
1271
+
1272
+ ```scala
1273
+ import zio.blocks.schema._
1274
+ import zio.blocks.schema.xml._
1275
+
1276
+ case class Url(
1277
+ loc: String,
1278
+ lastmod: Option[String],
1279
+ changefreq: Option[String],
1280
+ priority: Option[Double]
1281
+ )
1282
+
1283
+ case class Urlset(
1284
+ urls: List[Url]
1285
+ )
1286
+
1287
+ object Urlset {
1288
+ implicit val urlSchema: Schema[Url] = Schema.derived
1289
+ implicit val schema: Schema[Urlset] = Schema.derived
1290
+ }
1291
+
1292
+ // Build and encode sitemap
1293
+ val sitemap = Urlset(List(
1294
+ Url("https://example.com", Some("2024-01-01"), Some("monthly"), Some(1.0)),
1295
+ Url("https://example.com/about", Some("2024-01-01"), Some("monthly"), Some(0.8)),
1296
+ Url("https://example.com/contact", None, Some("monthly"), Some(0.5))
1297
+ ))
1298
+
1299
+ val codec = Schema[Urlset].derive(XmlFormat)
1300
+ val sitemapXml = codec.encodeToString(sitemap, WriterConfig(
1301
+ indentStep = 2,
1302
+ includeDeclaration = true
1303
+ ))
1304
+ ```