@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.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +1195 -0
- package/index.md +21 -12
- package/package.json +1 -1
- package/reference/allows.md +1377 -0
- package/reference/binding-resolver.md +469 -0
- package/reference/binding.md +1 -1
- package/reference/codec.md +10 -10
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +5 -0
- package/reference/dynamic-schema.md +602 -0
- package/reference/dynamic-value.md +5 -0
- package/reference/json-patch.md +803 -0
- package/reference/json.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/modifier.md +9 -9
- package/reference/patch.md +4 -0
- package/reference/schema-error.md +569 -0
- package/reference/schema-evolution/as.md +587 -0
- package/reference/schema-evolution/index.md +50 -0
- package/reference/schema-evolution/into.md +1027 -0
- package/reference/schema-expr.md +2 -2
- package/reference/schema.md +29 -0
- package/reference/structural-types.md +369 -0
- package/reference/type-class-derivation.md +329 -324
- package/reference/validation.md +1 -1
- package/reference/xml.md +1304 -0
- package/scope.md +241 -17
- package/sidebars.js +21 -1
- package/reference/schema-evolution.md +0 -540
package/reference/xml.md
ADDED
|
@@ -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
|
+
```
|