# Records and Classes

Structured data types with fixed schemas, type checking, and serialization support. PRecord provides a dict-like interface while PClass provides an object-like interface, both with field specifications and validation.

## Capabilities

### PRecord - Persistent Record

Dict-like persistent data structure with fixed schema and field validation. Extends PMap with type checking and field constraints.

```python { .api }
class PRecord(PMap):
    """
    Persistent record with fixed fields and optional validation.
    
    Define fields as class attributes using field() specifications.
    """
    
    _precord_fields: dict
    _precord_initial_values: dict
    
    def __init__(self, **kwargs): ...
    
    def set(self, *args, **kwargs) -> 'PRecord':
        """
        Set one or more fields, returning new PRecord instance.
        
        Supports both positional (key, value) and keyword arguments.
        Unlike PMap.set(), accepts multiple key-value pairs.
        """
    
    @classmethod
    def create(cls, kwargs: dict, _factory_fields=None, ignore_extra: bool = False) -> 'PRecord':
        """
        Create PRecord instance from dictionary with validation.
        
        Parameters:
        - kwargs: Dictionary of field values
        - ignore_extra: If True, ignore fields not defined in schema
        
        Returns:
        New PRecord instance
        """
    
    def serialize(self, format=None) -> dict:
        """
        Serialize record using field serializers.
        
        Parameters:
        - format: Optional format parameter passed to field serializers
        
        Returns:
        Dictionary with serialized field values
        """
    
    def discard(self, key) -> 'PRecord':
        """Return new PRecord without specified field."""
    
    def remove(self, key) -> 'PRecord':
        """Return new PRecord without specified field (raises KeyError if missing)."""
```

### PClass - Persistent Class

Object-like persistent data structure with fixed schema and field validation. Provides attribute-style access to fields.

```python { .api }
class PClass:
    """
    Persistent class with fixed fields and optional validation.
    
    Define fields as class attributes using field() specifications.
    Provides object-style attribute access.
    """
    
    def __new__(cls, **kwargs): ...
    
    def set(self, *args, **kwargs) -> 'PClass':
        """
        Set one or more fields, returning new PClass instance.
        
        Supports both positional (name, value) and keyword arguments.
        """
    
    @classmethod
    def create(cls, kwargs: dict, _factory_fields=None, ignore_extra: bool = False) -> 'PClass':
        """
        Create PClass instance from dictionary with validation.
        
        Parameters:
        - kwargs: Dictionary of field values
        - ignore_extra: If True, ignore fields not defined in schema
        
        Returns:
        New PClass instance
        """
    
    def serialize(self, format=None) -> dict:
        """
        Serialize class using field serializers.
        
        Parameters:
        - format: Optional format parameter passed to field serializers
        
        Returns:
        Dictionary with serialized field values
        """
    
    def transform(self, *transformations) -> 'PClass':
        """Apply path-based transformations to nested structure."""
    
    def evolver(self) -> 'PClassEvolver':
        """Return mutable-like interface for efficient batch updates."""
    
    def remove(self, name: str) -> 'PClass':
        """Return new PClass instance without specified field."""

class PClassMeta(type):
    """Metaclass for PClass that processes field definitions."""

class PClassEvolver:
    """Mutable-like interface for efficient PClass updates."""
    
    def __init__(self, original: PClass, initial_dict: dict): ...
    def __getitem__(self, item): ...
    def __setitem__(self, key, value): ...
    def __delitem__(self, item): ...
    def set(self, key, value) -> 'PClassEvolver': ...
    def remove(self, item) -> 'PClassEvolver': ...
    def persistent(self) -> PClass: ...
    def __getattr__(self, item): ...
```

## Field Specifications

### General Field Definition

Define field schemas with type checking, validation, defaults, and serialization.

```python { .api }
def field(
    type=(), 
    invariant=lambda _: (True, None), 
    initial=object(), 
    mandatory: bool = False, 
    factory=lambda x: x, 
    serializer=lambda _, value: value
) -> 'PField':
    """
    Define a field specification for PRecord or PClass.
    
    Parameters:
    - type: Required type(s) - single type, tuple of types, or empty tuple for any
    - invariant: Validation function returning (bool, error_msg) tuple
    - initial: Default value (use object() for no default)
    - mandatory: If True, field must be provided during creation
    - factory: Function to transform input values
    - serializer: Function to transform values during serialization
    
    Returns:
    PField specification object
    """
```

### Specialized Collection Fields

Pre-configured field types for persistent collections with type checking.

```python { .api }
def pset_field(
    item_type, 
    optional: bool = False, 
    initial=(), 
    invariant=lambda _: (True, None),
    item_invariant=lambda _: (True, None)
) -> 'PField':
    """
    Create a field that holds a type-checked PSet.
    
    Parameters:
    - item_type: Required type for set elements
    - optional: If True, field can be None
    - initial: Default PSet contents
    - invariant: Additional validation function for the field
    - item_invariant: Additional validation function for individual items
    
    Returns:
    PField for CheckedPSet
    """

def pvector_field(
    item_type, 
    optional: bool = False, 
    initial=(), 
    invariant=lambda _: (True, None),
    item_invariant=lambda _: (True, None)
) -> 'PField':
    """
    Create a field that holds a type-checked PVector.
    
    Parameters:
    - item_type: Required type for vector elements
    - optional: If True, field can be None
    - initial: Default PVector contents
    - invariant: Additional validation function for the field
    - item_invariant: Additional validation function for individual items
    
    Returns:
    PField for CheckedPVector
    """

def pmap_field(
    key_type, 
    value_type, 
    optional: bool = False, 
    initial=None,
    invariant=lambda _: (True, None)
) -> 'PField':
    """
    Create a field that holds a type-checked PMap.
    
    Parameters:
    - key_type: Required type for map keys
    - value_type: Required type for map values
    - optional: If True, field can be None
    - initial: Default PMap contents (defaults to empty pmap)
    - invariant: Additional validation function
    
    Returns:
    PField for CheckedPMap
    """

class PField:
    """Field specification object (internal use)."""
```

## Exception Classes

```python { .api }
class PTypeError(TypeError):
    """
    Type error for record/class fields.
    
    Attributes:
    - source_class: Class that raised the error
    - field: Field name that caused the error
    - expected_types: Tuple of expected types
    - actual_type: Actual type that was provided
    """
    
    source_class: type
    field: str
    expected_types: tuple
    actual_type: type
```

## Usage Examples

### Basic PRecord Usage

```python
from pyrsistent import PRecord, field

class Person(PRecord):
    name = field(type=str, mandatory=True)
    age = field(type=int, initial=0)
    email = field(type=str, initial='')

# Create instances
person = Person(name='Alice', age=30, email='alice@example.com')
person2 = Person(name='Bob')  # Uses default age=0, email=''

# Update fields (returns new instance)
older_person = person.set(age=31)
updated_person = person.set(age=31, email='alice@newdomain.com')

# Access like a dictionary
print(person['name'])  # 'Alice'
print(person.get('age', 0))  # 30

# Validation happens automatically
try:
    Person(name=123)  # Invalid type for name
except PTypeError as e:
    print(f"Type error in field {e.field}: expected {e.expected_types}, got {e.actual_type}")
```

### Basic PClass Usage

```python
from pyrsistent import PClass, field

class Point(PClass):
    x = field(type=(int, float), initial=0)
    y = field(type=(int, float), initial=0)

# Create instances
point = Point(x=1, y=2)
origin = Point()  # Uses defaults x=0, y=0

# Update fields (returns new instance)
moved_point = point.set(x=5, y=10)

# Access like object attributes
print(point.x)  # 1
print(point.y)  # 2

# Attribute-style access
distance_squared = point.x**2 + point.y**2
```

### Advanced Field Specifications

```python
from pyrsistent import PRecord, field, pset_field, pvector_field

class Product(PRecord):
    name = field(
        type=str, 
        mandatory=True
    )
    price = field(
        type=(int, float),
        mandatory=True,
        invariant=lambda price: (price > 0, "Price must be positive")
    )
    tags = pset_field(
        item_type=str,
        initial=pset()
    )
    reviews = pvector_field(
        item_type=str,
        initial=pvector()
    )
    metadata = field(
        type=dict,
        initial={},
        factory=lambda d: d.copy(),  # Ensure we get a copy
        serializer=lambda _, value: dict(value)  # Convert to regular dict for JSON
    )

# Create product
product = Product(
    name='Laptop',
    price=999.99,
    tags=pset(['electronics', 'computers']),
    reviews=pvector(['Great laptop!', 'Fast delivery'])
)

# Validation works
try:
    Product(name='Invalid', price=-100)  # Negative price
except InvariantException as e:
    print(f"Invariant failed: {e}")
```

### Serialization

```python
class User(PRecord):
    username = field(type=str, mandatory=True)
    created_at = field(
        type=str,
        factory=lambda dt: dt.isoformat() if hasattr(dt, 'isoformat') else str(dt),
        serializer=lambda _, value: value  # Already converted by factory
    )
    preferences = field(
        type=dict,
        initial={},
        serializer=lambda _, prefs: {k: v for k, v in prefs.items() if v is not None}
    )

from datetime import datetime
user = User(
    username='alice',
    created_at=datetime.now(),
    preferences={'theme': 'dark', 'notifications': True, 'temp': None}
)

# Serialize for JSON/API output
user_data = user.serialize()
# {'username': 'alice', 'created_at': '2023-...', 'preferences': {'theme': 'dark', 'notifications': True}}
```

### Factory Methods and Error Handling

```python
# Use create() for construction from external data
external_data = {
    'name': 'Alice',
    'age': '30',  # String that needs conversion
    'unknown_field': 'ignored'
}

class StrictPerson(PRecord):
    name = field(type=str, mandatory=True)
    age = field(type=int, factory=int)  # Convert strings to int

# Ignore extra fields
person = StrictPerson.create(external_data, ignore_extra=True)

# Or handle unknown fields
try:
    StrictPerson.create(external_data)
except Exception as e:
    print(f"Unknown field error: {e}")
```

### Evolvers for Batch Updates

```python
class Config(PClass):
    debug = field(type=bool, initial=False)
    port = field(type=int, initial=8080)
    host = field(type=str, initial='localhost')

config = Config()

# Efficient batch updates
evolver = config.evolver()
evolver.debug = True
evolver.port = 3000
evolver.host = '0.0.0.0'
new_config = evolver.persistent()

# Or using set method
evolver2 = config.evolver()
evolver2.set('debug', True).set('port', 3000)
new_config2 = evolver2.persistent()
```