@zio.dev/zio-blocks 0.0.22 → 0.0.25

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,460 @@
1
+ ---
2
+ id: media-type
3
+ title: "MediaType"
4
+ ---
5
+
6
+ `MediaType` is a **type-safe representation of IANA media types** (also known as MIME types). It captures structured metadata about content types including compressibility, binary/text classification, and associated file extensions.
7
+
8
+ ZIO Blocks MediaType is designed to be a comprehensive and efficient implementation for handling media types in Scala applications, especially those involving HTTP content negotiation, file handling, and data serialization.
9
+
10
+ `MediaType`:
11
+
12
+ - is a zero-dependency data type in the `zio-blocks-mediatype` module
13
+ - ships with 2,600+ predefined IANA media types auto-generated from the [mime-db](https://github.com/jshttp/mime-db) database
14
+ - provides a `mediaType"..."` string interpolator with compile-time validation
15
+ - supports wildcard and case-insensitive matching
16
+ - is cross-platform (JVM and Scala.js) and cross-version (Scala 2.13 and 3.x)
17
+
18
+ ```scala
19
+ final case class MediaType(
20
+ mainType: String,
21
+ subType: String,
22
+ compressible: Boolean = false,
23
+ binary: Boolean = false,
24
+ fileExtensions: List[String] = Nil,
25
+ extensions: Map[String, String] = Map.empty,
26
+ parameters: Map[String, String] = Map.empty
27
+ )
28
+ ```
29
+
30
+ ## Motivation
31
+
32
+ Media types are fundamental to content negotiation in HTTP, file type detection, and serialization format selection. Working with raw strings like `"application/json"` is error-prone — typos go undetected, metadata (is it compressible? binary? what file extensions?) must be tracked separately, and matching logic must account for wildcards and case insensitivity.
33
+
34
+ `MediaType` solves these problems by providing:
35
+
36
+ - **Structured representation** — main type, subtype, and parameters as distinct fields
37
+ - **Rich metadata** — compressibility, binary/text classification, and file extensions baked in
38
+ - **Compile-time safety** — the `mediaType"..."` interpolator catches malformed types at compile time
39
+ - **Correct matching** — wildcard and case-insensitive matching with parameter awareness
40
+
41
+ ```text
42
+ MediaType
43
+ ┌──────────┼──────────┐
44
+ mainType subType parameters
45
+ │ │ │
46
+ "application" "json" Map("charset" -> "utf-8")
47
+ │
48
+ ┌─────┴──────────────────────────────────┐
49
+ │ compressible = true │
50
+ │ binary = false │
51
+ │ fileExtensions = List("json", "map") │
52
+ └────────────────────────────────────────┘
53
+ ```
54
+
55
+ A quick example:
56
+
57
+ ```scala
58
+ import zio.blocks.mediatype._
59
+
60
+ // Compile-time validated media type
61
+ val json = mediaType"application/json"
62
+
63
+ // Look up by file extension
64
+ val detected = MediaType.forFileExtension("png")
65
+ // Some(MediaType("image", "png", binary = true, ...))
66
+
67
+ // Wildcard matching
68
+ val textAny = mediaType"text/*"
69
+ val html = mediaType"text/html"
70
+ textAny.matches(html) // true
71
+ ```
72
+
73
+ ## Installation
74
+
75
+ Add the following to your `build.sbt`:
76
+
77
+ ```scala
78
+ libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.25"
79
+ ```
80
+
81
+ For cross-platform projects (Scala.js):
82
+
83
+ ```scala
84
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.25"
85
+ ```
86
+
87
+ Supported Scala versions: 2.13.x and 3.x.
88
+
89
+ ## Creating Instances
90
+
91
+ ### Direct Construction
92
+
93
+ We can create a `MediaType` by specifying the main type and subtype directly. All other fields have sensible defaults:
94
+
95
+ ```scala
96
+ import zio.blocks.mediatype.MediaType
97
+
98
+ // Minimal — just mainType and subType
99
+ val plain = MediaType("text", "plain")
100
+ // compressible = false, binary = false, fileExtensions = Nil, ...
101
+
102
+ // With all fields
103
+ val json = MediaType(
104
+ mainType = "application",
105
+ subType = "json",
106
+ compressible = true,
107
+ binary = false,
108
+ fileExtensions = List("json", "map"),
109
+ extensions = Map("source" -> "iana"),
110
+ parameters = Map("charset" -> "utf-8")
111
+ )
112
+ ```
113
+
114
+ ### Parsing from a String
115
+
116
+ `MediaType.parse` parses a standard media type string in the format `mainType/subType[; key=value]*`:
117
+
118
+ ```scala
119
+ import zio.blocks.mediatype.{MediaType, MediaTypes}
120
+
121
+ // Simple type
122
+ val json: Either[String, MediaType] = MediaType.parse("application/json")
123
+ // Right(MediaType("application", "json", compressible = true, ...))
124
+
125
+ // With parameters
126
+ val html = MediaType.parse("text/html; charset=utf-8")
127
+ // Right(MediaType("text", "html", ..., parameters = Map("charset" -> "utf-8")))
128
+
129
+ // Predefined instances are reused — parse returns the same object
130
+ val parsed = MediaType.parse("application/json").toOption.get
131
+ parsed eq MediaTypes.application.`json` // true (reference equality)
132
+
133
+ // Invalid input returns Left with an error message
134
+ MediaType.parse("") // Left("Invalid media type: cannot be empty")
135
+ MediaType.parse("applicationjson") // Left("Invalid media type: must contain '/' separator")
136
+ MediaType.parse("/json") // Left("Invalid media type: main type cannot be empty")
137
+ MediaType.parse("application/") // Left("Invalid media type: subtype cannot be empty")
138
+ ```
139
+
140
+ ### Unsafe Parsing
141
+
142
+ When we are certain the input is valid, `unsafeFromString` returns a `MediaType` directly or throws an `IllegalArgumentException`:
143
+
144
+ ```scala
145
+ import zio.blocks.mediatype.MediaType
146
+
147
+ val json = MediaType.unsafeFromString("application/json")
148
+ // MediaType("application", "json", compressible = true, ...)
149
+
150
+ // Throws IllegalArgumentException for invalid input:
151
+ // MediaType.unsafeFromString("invalid")
152
+ ```
153
+
154
+ ### String Interpolator
155
+
156
+ The `mediaType"..."` interpolator validates the media type at compile time. Invalid types produce compile errors, not runtime failures:
157
+
158
+ ```scala
159
+ import zio.blocks.mediatype._
160
+
161
+ val json = mediaType"application/json"
162
+ val htmlUtf8 = mediaType"text/html; charset=utf-8"
163
+ val wildcard = mediaType"*/*"
164
+ val vendor = mediaType"text/vnd.api+json"
165
+ ```
166
+
167
+ The interpolator reuses predefined instances when available (reference equality with `MediaTypes` constants). It does not support variable interpolation — only literal strings are accepted.
168
+
169
+ :::note
170
+ The `mediaType` interpolator is available after importing `zio.blocks.mediatype._`. Invalid inputs produce clear compile-time errors:
171
+
172
+ ```text
173
+ mediaType"" → "Invalid media type: cannot be empty"
174
+ mediaType"applicationjson" → "Invalid media type: must contain '/' separator"
175
+ mediaType"/json" → "Invalid media type: main type cannot be empty"
176
+ mediaType"application/" → "Invalid media type: subtype cannot be empty"
177
+ ```
178
+ :::
179
+
180
+ ### File Extension Lookup
181
+
182
+ `MediaType.forFileExtension` finds a `MediaType` by its associated file extension. The lookup is case-insensitive and strips a leading `.` if present:
183
+
184
+ ```scala
185
+ import zio.blocks.mediatype.MediaType
186
+
187
+ MediaType.forFileExtension("json") // Some(MediaType("application", "json", ...))
188
+ MediaType.forFileExtension(".html") // Some(MediaType("text", "html", ...))
189
+ MediaType.forFileExtension("PNG") // Some(MediaType("image", "png", ...))
190
+ MediaType.forFileExtension("jpg") // Some(MediaType("image", "jpeg", ...))
191
+ MediaType.forFileExtension("pdf") // Some(MediaType("application", "pdf", ...))
192
+
193
+ MediaType.forFileExtension("xyz123") // None (unknown extension)
194
+ MediaType.forFileExtension("") // None
195
+ MediaType.forFileExtension(".") // None
196
+ ```
197
+
198
+ ## Predefined Media Types
199
+
200
+ The `MediaTypes` object contains 2,600+ predefined media type constants auto-generated from the [jshttp/mime-db](https://github.com/jshttp/mime-db) database. Types are organized by main type category:
201
+
202
+ | Category | Object | Examples |
203
+ |--------------|---------------------------|--------------------------------------|
204
+ | Application | `MediaTypes.application` | `json`, `pdf`, `xml`, `octet-stream` |
205
+ | Audio | `MediaTypes.audio` | `mpeg`, `ogg`, `wav` |
206
+ | Chemical | `MediaTypes.chemical` | `x-cif`, `x-pdb` |
207
+ | Font | `MediaTypes.font` | `woff`, `woff2`, `otf` |
208
+ | Image | `MediaTypes.image` | `png`, `jpeg`, `gif`, `svg+xml` |
209
+ | Message | `MediaTypes.message` | `rfc822`, `partial` |
210
+ | Model | `MediaTypes.model` | `gltf+json`, `stl` |
211
+ | Multipart | `MediaTypes.multipart` | `form-data`, `mixed` |
212
+ | Text | `MediaTypes.text` | `html`, `css`, `plain`, `csv` |
213
+ | Video | `MediaTypes.video` | `mp4`, `webm`, `ogg` |
214
+ | X-Conference | `MediaTypes.x_conference` | `x-cooltalk` |
215
+ | X-Shader | `MediaTypes.x_shader` | `x-vertex`, `x-fragment` |
216
+ | Wildcard | `MediaTypes.any` | `*/*` |
217
+
218
+ ### Accessing Predefined Types
219
+
220
+ Since many subtype names contain special characters (hyphens, dots, plus signs), predefined constants use backtick identifiers:
221
+
222
+ ```scala
223
+ import zio.blocks.mediatype.MediaTypes
224
+
225
+ // Common application types
226
+ val json = MediaTypes.application.`json`
227
+ val pdf = MediaTypes.application.`pdf`
228
+ val xmlType = MediaTypes.application.`xml`
229
+
230
+ // Common text types
231
+ val html = MediaTypes.text.`html`
232
+ val css = MediaTypes.text.`css`
233
+ val plain = MediaTypes.text.`plain`
234
+
235
+ // Common image types
236
+ val png = MediaTypes.image.`png`
237
+ val jpeg = MediaTypes.image.`jpeg`
238
+
239
+ // Wildcard (matches any type)
240
+ val any = MediaTypes.any
241
+ ```
242
+
243
+ ### Listing All Types
244
+
245
+ Each category object has an `all` field returning a `List[MediaType]` of all types in that category. The top-level `allMediaTypes` aggregates every category:
246
+
247
+ ```scala
248
+ import zio.blocks.mediatype.MediaTypes
249
+
250
+ // All types in a category
251
+ val appTypes: List[zio.blocks.mediatype.MediaType] = MediaTypes.application.all
252
+
253
+ // All 2,600+ predefined types
254
+ val everything: List[zio.blocks.mediatype.MediaType] = MediaTypes.allMediaTypes
255
+ ```
256
+
257
+ ### Predefined Metadata
258
+
259
+ Each predefined instance comes with rich metadata from the IANA registry:
260
+
261
+ ```scala
262
+ import zio.blocks.mediatype.MediaTypes
263
+
264
+ val json = MediaTypes.application.`json`
265
+ json.compressible // true
266
+ json.binary // false
267
+ json.fileExtensions // List("json", "map")
268
+
269
+ val jpeg = MediaTypes.image.`jpeg`
270
+ jpeg.compressible // false
271
+ jpeg.binary // true
272
+ jpeg.fileExtensions // List("jpg", "jpeg", "jpe")
273
+
274
+ val html = MediaTypes.text.`html`
275
+ html.compressible // true
276
+ html.binary // false
277
+ html.fileExtensions // List("html", "htm", "shtml")
278
+ ```
279
+
280
+ ## Core Operations
281
+
282
+ ### `fullType`
283
+
284
+ Returns the complete media type string by combining `mainType` and `subType` with a `/` separator:
285
+
286
+ ```scala
287
+ import zio.blocks.mediatype.MediaType
288
+
289
+ val mt = MediaType("application", "json")
290
+ mt.fullType // "application/json"
291
+
292
+ MediaType("*", "*").fullType // "*/*"
293
+ MediaType("text", "*").fullType // "text/*"
294
+ ```
295
+
296
+ ### `matches`
297
+
298
+ Compares two `MediaType` values with support for wildcards, case insensitivity, and parameter matching:
299
+
300
+ ```scala
301
+ final case class MediaType(...) {
302
+ def matches(other: MediaType, ignoreParameters: Boolean = false): Boolean
303
+ }
304
+ ```
305
+
306
+ The matching rules are:
307
+
308
+ 1. **Wildcard matching** — `"*"` in either `mainType` or `subType` matches any value
309
+ 2. **Case-insensitive** — `"APPLICATION/JSON"` matches `"application/json"`
310
+ 3. **Parameter subset** — when `ignoreParameters = false` (the default), all parameters in `this` must exist in `other` with matching values (case-insensitive). Extra parameters in `other` are allowed.
311
+
312
+ ```scala
313
+ import zio.blocks.mediatype._
314
+
315
+ val json = mediaType"application/json"
316
+ val textAll = mediaType"text/*"
317
+ val html = mediaType"text/html"
318
+ val any = mediaType"*/*"
319
+
320
+ // Exact match
321
+ json.matches(json) // true
322
+
323
+ // Wildcard matching
324
+ any.matches(json) // true — */* matches anything
325
+ textAll.matches(html) // true — text/* matches text/html
326
+ textAll.matches(json) // false — text/* does not match application/json
327
+
328
+ // Case-insensitive
329
+ MediaType("APPLICATION", "JSON").matches(json) // true
330
+
331
+ // Parameter matching
332
+ val htmlUtf8 = MediaType("text", "html", parameters = Map("charset" -> "utf-8"))
333
+ val htmlLatin = MediaType("text", "html", parameters = Map("charset" -> "iso-8859-1"))
334
+ val htmlFull = MediaType("text", "html", parameters = Map("charset" -> "utf-8", "boundary" -> "xxx"))
335
+
336
+ htmlUtf8.matches(htmlLatin) // false — charset mismatch
337
+ htmlUtf8.matches(htmlLatin, ignoreParameters = true) // true — parameters ignored
338
+ htmlUtf8.matches(htmlFull) // true — subset match (charset matches)
339
+ ```
340
+
341
+ ### `forFileExtension`
342
+
343
+ Looks up a `MediaType` by file extension. The lookup is case-insensitive and strips a leading `.` if present:
344
+
345
+ ```scala
346
+ object MediaType {
347
+ def forFileExtension(ext: String): Option[MediaType]
348
+ }
349
+ ```
350
+
351
+ ```scala
352
+ import zio.blocks.mediatype.MediaType
353
+
354
+ MediaType.forFileExtension("json") // Some(MediaType("application", "json", ...))
355
+ MediaType.forFileExtension(".html") // Some(MediaType("text", "html", ...))
356
+ MediaType.forFileExtension("PNG") // Some(MediaType("image", "png", ...))
357
+ MediaType.forFileExtension("") // None
358
+ MediaType.forFileExtension("xyz") // None
359
+ ```
360
+
361
+ :::tip
362
+ `forFileExtension` gives priority to `text/*` types when an extension maps to multiple types. For example, `"js"` maps to `text/javascript` rather than `application/javascript`.
363
+ :::
364
+
365
+ ### `parse`
366
+
367
+ Parses a media type string into a `MediaType`, returning `Left` with an error message for invalid input:
368
+
369
+ ```scala
370
+ object MediaType {
371
+ def parse(s: String): Either[String, MediaType]
372
+ }
373
+ ```
374
+
375
+ When the parsed type matches a predefined instance, that instance is returned (preserving reference equality and all metadata). Parameters from the input string are merged into the result:
376
+
377
+ ```scala
378
+ import zio.blocks.mediatype.{MediaType, MediaTypes}
379
+
380
+ // Returns predefined instance with full metadata
381
+ val json = MediaType.parse("application/json")
382
+ // Right(MediaType("application", "json", compressible=true, binary=false, ...))
383
+
384
+ // Parameters are parsed and attached
385
+ val result = MediaType.parse("multipart/form-data; boundary=abc; charset=utf-8")
386
+ // Right(MediaType(..., parameters = Map("boundary" -> "abc", "charset" -> "utf-8")))
387
+
388
+ // Unknown types get a fresh instance
389
+ val custom = MediaType.parse("custom/x-my-format")
390
+ // Right(MediaType("custom", "x-my-format"))
391
+
392
+ // Malformed parameters (no "=") are silently ignored
393
+ val partial = MediaType.parse("text/html; charset=utf-8; malformed")
394
+ // Right(MediaType(..., parameters = Map("charset" -> "utf-8")))
395
+ ```
396
+
397
+ ### `unsafeFromString`
398
+
399
+ Like `parse`, but throws `IllegalArgumentException` instead of returning `Left`:
400
+
401
+ ```scala
402
+ object MediaType {
403
+ def unsafeFromString(s: String): MediaType
404
+ }
405
+ ```
406
+
407
+ For valid input, it returns the corresponding `MediaType` (reusing predefined instances when possible). For invalid input, it throws an exception with a descriptive message:
408
+
409
+ ```scala
410
+ import zio.blocks.mediatype.MediaType
411
+
412
+ val json = MediaType.unsafeFromString("application/json")
413
+
414
+ // Throws IllegalArgumentException:
415
+ // MediaType.unsafeFromString("not-a-media-type")
416
+ ```
417
+
418
+ ## Advanced Usage
419
+
420
+ ### Content Negotiation
421
+
422
+ We can use `matches` with wildcard types to implement HTTP-style content negotiation:
423
+
424
+ ```scala
425
+ import zio.blocks.mediatype._
426
+
427
+ def negotiate(
428
+ accept: List[MediaType],
429
+ available: List[MediaType]
430
+ ): Option[MediaType] =
431
+ available.find(avail => accept.exists(_.matches(avail, ignoreParameters = true)))
432
+
433
+ val accept = List(mediaType"text/*", mediaType"application/json")
434
+ val available = List(mediaType"application/xml", mediaType"application/json", mediaType"text/html")
435
+
436
+ negotiate(accept, available)
437
+ // Some(MediaType("application", "json", ...))
438
+ ```
439
+
440
+ ### File Type Detection
441
+
442
+ Combine `forFileExtension` with file path processing to detect content types:
443
+
444
+ ```scala
445
+ import zio.blocks.mediatype.MediaType
446
+
447
+ def detectContentType(filename: String): Option[MediaType] = {
448
+ val ext = filename.lastIndexOf('.') match {
449
+ case -1 => ""
450
+ case i => filename.substring(i + 1)
451
+ }
452
+ MediaType.forFileExtension(ext)
453
+ }
454
+
455
+ detectContentType("report.pdf") // Some(MediaType("application", "pdf", ...))
456
+ detectContentType("photo.jpg") // Some(MediaType("image", "jpeg", ...))
457
+ detectContentType("data.json") // Some(MediaType("application", "json", ...))
458
+ detectContentType("Makefile") // None
459
+ ```
460
+
@@ -5,6 +5,10 @@ title: "Optics"
5
5
 
6
6
  Optics are a fundamental feature of ZIO Blocks that enable type-safe, composable access and modification of nested data structures. What sets ZIO Blocks apart is its implementation of **reflective optics** — a novel construct that combines the operational capabilities of traditional optics with embedded structural metadata, enabling both data manipulation AND introspection.
7
7
 
8
+ :::tip
9
+ For a practical walkthrough of building query DSLs with optics, see the [Writing a Query DSL with Reified Optics](../guides/query-dsl-reified-optics.md) guide.
10
+ :::
11
+
8
12
  ## What Are Optics?
9
13
 
10
14
  Optics are abstractions that allow you to focus on a specific part of a data structure. They provide a way to **view**, **update**, and **traverse** nested fields in immutable data types without boilerplate code: