README.md

# parsed_it

[![Package Version](https://img.shields.io/hexpm/v/parsed_it)](https://hex.pm/packages/parsed_it)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/parsed_it/)

A parsing and serialization library for JSON and XML in Gleam, supporting both
Erlang and JavaScript targets.

## Installation

```sh
gleam add parsed_it@0.1
```

## Quick Start

### JSON Parsing

```gleam
import gleam/dynamic/decode
import parsed_it/json

type User {
  User(name: String, email: String)
}

fn user_decoder() -> decode.Decoder(User) {
  use name <- decode.field("name", decode.string)
  use email <- decode.field("email", decode.string)
  decode.success(User(name:, email:))
}

pub fn example() {
  let json_string = "{\"name\":\"Lucy\",\"email\":\"lucy@example.com\"}"
  let result = json.parse(from: json_string, using: user_decoder())
  // result == Ok(User("Lucy", "lucy@example.com"))
}
```

### XML Parsing

```gleam
import gleam/dynamic/decode
import parsed_it/xml

type Book {
  Book(title: String, author: String)
}

fn book_decoder() -> decode.Decoder(Book) {
  use title <- decode.field("title", decode.field("$text", decode.string))
  use author <- decode.field("author", decode.field("$text", decode.string))
  decode.success(Book(title:, author:))
}

pub fn example() {
  let xml_string = "<book><title>Gleam Guide</title><author>Lucy</author></book>"
  let result = xml.parse(from: xml_string, using: book_decoder())
  // result == Ok(Book(title: "Gleam Guide", author: "Lucy"))
}
```

### Building JSON/XML

```gleam
import parsed_it/json
import parsed_it/xml

pub fn json_example() {
  json.object([
    #("name", json.string("Lucy")),
    #("age", json.int(30)),
  ])
  |> json.to_string
  // "{\"name\":\"Lucy\",\"age\":30}"
}

pub fn xml_example() {
  xml.element("user", [xml.attr("id", "1")], [
    xml.element("name", [], [xml.string("Lucy")]),
  ])
  |> xml.to_string
  // "<user id=\"1\"><name>Lucy</name></user>"
}
```

## Module Organization

This library uses `parsed_it/*` as the module namespace:
- `parsed_it/json` - JSON parsing and serialization
- `parsed_it/xml` - XML parsing and serialization

Import modules like this:
```gleam
import parsed_it/json
import parsed_it/xml
```

## API Design

### Type-Safe Parsing with `parse`

The primary API uses labeled arguments for clarity:

```gleam
json.parse(from: json_string, using: decoder)
xml.parse(from: xml_string, using: decoder)
```

### Dynamic Parsing with `parse_dynamic`

For cases where you need to inspect raw structure before decoding:

```gleam
json.parse_dynamic(from: json_string)
xml.parse_dynamic(from: xml_string)
```

## XML Dynamic Structure

When using `parse_dynamic` or writing decoders for XML, understand the structure
that the parser produces:

```gleam
// XML: <book id="123"><title>Hello</title></book>
// Becomes:
// {
//   "$tag": "book",
//   "$attrs": { "id": "123" },
//   "title": { "$tag": "title", "$text": "Hello" }
// }
```

Special keys:
- `$tag` - The element's tag name (always present)
- `$attrs` - Object containing attributes (present if element has attributes)
- `$text` - Text content (present if element has text content)

### Child Element Multiplicity

**Important:** Child elements with the same tag name are grouped into arrays,
while unique children remain as single objects:

```gleam
// XML: <list><item>A</item><item>B</item></list>
// Becomes: { "$tag": "list", "item": [{ "$tag": "item", "$text": "A" }, ...] }

// XML: <list><item>A</item></list>
// Becomes: { "$tag": "list", "item": { "$tag": "item", "$text": "A" } }  // NOT an array!
```

Write decoders that handle both cases if the multiplicity can vary:

```gleam
fn items_decoder() -> decode.Decoder(List(String)) {
  decode.one_of([
    // Handle array case
    decode.field("item", decode.list(decode.field("$text", decode.string))),
    // Handle single element case
    decode.field("item", decode.field("$text", decode.string))
    |> decode.map(fn(s) { [s] }),
  ])
}
```

### Leaf Elements and Tag Names

When a leaf element (no children, only text) is accessed as a child, you get
the full element object including `$tag`. The text is in `$text`:

```gleam
// To decode: <name>Lucy</name>
decode.field("name", decode.field("$text", decode.string))
```

## Known Issues

| Issue | Platform | Impact |
|-------|----------|--------|
| Unicode corruption in XML | Erlang | Multi-byte UTF-8 may be corrupted |
| Error message extraction | JavaScript | May return empty error details in some engines |

## Error Handling

### JSON Errors

```gleam
pub type JsonDecodeError {
  UnexpectedEnd          // JSON truncated
  UnexpectedChar(String) // Invalid character (hex code)
  UnexpectedSequence(String)
  UnableToDecode(List(DecodeError)) // Valid JSON, wrong shape
}
```

### XML Errors

```gleam
pub type XmlDecodeError {
  InvalidXml(String)     // Malformed XML
  UnableToDecode(List(DecodeError)) // Valid XML, wrong shape
}
```

> **Note:** Error types are currently public ADTs. This means adding new error
> variants in future versions would be a breaking change. We may make these
> opaque in a future major version to allow for better error handling evolution.

## Development

```sh
gleam test  # Run the tests
gleam docs build  # Build documentation
```

## Further Documentation

API documentation is available at <https://hexdocs.pm/parsed_it>.