# Custom Constructors and Representers

This guide explains how to create custom YAML constructors and representers with `yayaml`.

## Overview

- **Constructors** convert YAML nodes into Python objects during loading
- **Representers** convert Python objects into YAML nodes during dumping

`yayaml` provides decorators and helper functions to simplify creating both.


## Custom Constructors

### Using the `@is_constructor` Decorator

The simplest way to add a custom constructor is with the `@is_constructor` decorator:

```python
from yayaml import is_constructor, yaml

@is_constructor("!point")
def construct_point(loader, node):
    """Construct a Point from a YAML sequence [x, y]."""
    coords = loader.construct_sequence(node, deep=True)
    return Point(coords[0], coords[1])
```

Now you can use the tag in YAML:

```yaml
location: !point [10, 20]
```

### Constructor with Aliases

You can register multiple tags for the same constructor:

```python
@is_constructor("!pt", aliases=("!point", "!coord"))
def construct_point(loader, node):
    coords = loader.construct_sequence(node, deep=True)
    return Point(coords[0], coords[1])
```

### Constructor with Error Hints

Add helpful hints that appear in error messages:

```python
@is_constructor("!point", hint="Expected sequence [x, y] for !point tag")
def construct_point(loader, node):
    coords = loader.construct_sequence(node, deep=True)
    if len(coords) != 2:
        raise ValueError("Point requires exactly 2 coordinates")
    return Point(coords[0], coords[1])
```

### Using `add_constructor` Directly

For simple cases, you can use `add_constructor` without a decorator:

```python
from yayaml import add_constructor, yaml

def construct_uppercase(loader, node):
    return loader.construct_scalar(node).upper()

add_constructor("!upper", construct_uppercase)
```


## Using `construct_from_func`

For constructors that simply apply a function to the node value, use `construct_from_func`:

```python
from functools import partial
from yayaml import add_constructor, construct_from_func

# Create a constructor that applies `str.upper` to the scalar value
add_constructor(
    "!upper",
    partial(construct_from_func, func=str.upper, unpack=False)
)
```

The `unpack` parameter controls argument handling:
- `unpack=False`: Pass the constructed value directly to the function
- `unpack=True`: Unpack sequences as `*args` and mappings as `**kwargs`

### Example with Unpacking

```python
from functools import partial
from yayaml import add_constructor, construct_from_func

def create_rectangle(width, height):
    return {"width": width, "height": height, "area": width * height}

add_constructor(
    "!rect",
    partial(construct_from_func, func=create_rectangle, unpack=True)
)
```

Usage:

```yaml
# As sequence (unpacked as positional args)
box1: !rect [10, 20]

# As mapping (unpacked as keyword args)
box2: !rect
  width: 10
  height: 20
```


## Custom Representers

### Using the `@is_representer` Decorator

```python
from yayaml import is_representer, yaml

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

@is_representer(Point)
def represent_point(representer, point, *, tag):
    return representer.represent_sequence(tag, [point.x, point.y])
```

Now when you dump a `Point`, it will be serialized:

```python
from yayaml import yaml_dumps

pt = Point(10, 20)
print(yaml_dumps({"location": pt}))
# Output:
# location: !Point [10, 20]
```

### Custom Tag Name

By default, the tag is derived from the class name. You can customize it:

```python
@is_representer(Point, tag="!pt")
def represent_point(representer, point, *, tag):
    return representer.represent_sequence(tag, [point.x, point.y])
```

### Using `add_representer` Directly

```python
from yayaml import add_representer, yaml

def represent_point(representer, point, *, tag):
    return representer.represent_mapping(tag, {"x": point.x, "y": point.y})

add_representer(Point, represent_point, tag="!point")
```


## Using `build_representer`

For simple cases where you just need to convert an object to a serializable form, use `build_representer`:

```python
from yayaml import add_representer, build_representer

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Represent as a sequence [x, y]
add_representer(
    Point,
    build_representer(lambda pt: [pt.x, pt.y])
)

# Or represent as a mapping {x: ..., y: ...}
add_representer(
    Point,
    build_representer(lambda pt: {"x": pt.x, "y": pt.y})
)

# Or represent as a scalar (string)
add_representer(
    Point,
    build_representer(lambda pt: f"({pt.x}, {pt.y})")
)
```

The `build_representer` function automatically determines the representation type based on what your simplification function returns:
- `list` or `tuple` -> sequence node
- `dict` -> mapping node
- anything else -> scalar node


## Complete Example

Here's a complete example showing both a constructor and representer for a custom class:

```python
from yayaml import (
    is_constructor,
    is_representer,
    yaml,
    yaml_dumps,
    load_yml,
    write_yml,
)

class Color:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __repr__(self):
        return f"Color({self.r}, {self.g}, {self.b})"

# Register constructor
@is_constructor("!color")
def construct_color(loader, node):
    values = loader.construct_sequence(node, deep=True)
    return Color(*values)

# Register representer
@is_representer(Color, tag="!color")
def represent_color(representer, color, *, tag):
    return representer.represent_sequence(tag, [color.r, color.g, color.b])


# Usage
yaml_content = """
background: !color [255, 255, 255]
foreground: !color [0, 0, 0]
"""

data = yaml.load(yaml_content)
print(data["background"])  # Color(255, 255, 255)

# Round-trip
output = yaml_dumps(data)
print(output)
# background: !color [255, 255, 255]
# foreground: !color [0, 0, 0]
```


## Working with Different Node Types

YAML has three node types that your constructors may need to handle:

### Scalar Nodes

Single values (strings, numbers, booleans, null):

```python
@is_constructor("!upper")
def construct_upper(loader, node):
    # For scalar nodes, use construct_scalar
    return loader.construct_scalar(node).upper()
```

### Sequence Nodes

Lists/arrays:

```python
@is_constructor("!sorted")
def construct_sorted(loader, node):
    # For sequence nodes, use construct_sequence
    items = loader.construct_sequence(node, deep=True)
    return sorted(items)
```

### Mapping Nodes

Dictionaries/objects:

```python
@is_constructor("!config")
def construct_config(loader, node):
    # For mapping nodes, use construct_mapping
    data = loader.construct_mapping(node, deep=True)
    return Config(**data)
```

### Handling Multiple Node Types

Use `scalar_node_to_object` for flexible scalar handling:

```python
from yayaml import scalar_node_to_object
import ruamel.yaml

@is_constructor("!flexible")
def construct_flexible(loader, node):
    if isinstance(node, ruamel.yaml.nodes.ScalarNode):
        return scalar_node_to_object(loader, node)
    elif isinstance(node, ruamel.yaml.nodes.SequenceNode):
        return loader.construct_sequence(node, deep=True)
    elif isinstance(node, ruamel.yaml.nodes.MappingNode):
        return loader.construct_mapping(node, deep=True)
```


## Registering with Different YAML Instances

By default, constructors and representers are registered with the main `yaml` instance from `yayaml`. You can specify a different instance:

```python
from yayaml import is_constructor, is_representer, yaml, yaml_safe

# Register with the safe yaml instance
@is_constructor("!mytag", _yaml=yaml_safe)
def construct_mytag(loader, node):
    ...

# Register with the main yaml instance (default)
@is_representer(MyClass, _yaml=yaml)
def represent_myclass(representer, obj, *, tag):
    ...
```
