Metadata-Version: 2.4
Name: latychain
Version: 0.1.0a1
Summary: Chain-structured data and pattern matching with .xxx.yyy syntax
Project-URL: Homepage, https://github.com/fnsii/latychain
Project-URL: Repository, https://github.com/fnsii/latychain
Project-URL: Documentation, https://github.com/fnsii/latychain#readme
Project-URL: Bug Tracker, https://github.com/fnsii/latychain/issues
Author: laty contributors
License: MIT
License-File: LICENSE
Keywords: chain,dsl,path,pattern-matching
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# latychain

**Immutable chain-structured data with backtracking pattern matching and `.xxx.yyy` syntax sugar.**

```python
from latychain import Chain, ChainRuleAtom

# Data chain
data = Chain(['heading', 'h1'])

# Rule chain — pattern to match against
rule = Chain([ChainRuleAtom.any(0), 'uuu', ChainRuleAtom.rex(r'x\d')])

data.match(rule)   # False — no 'uuu' before h1

Chain(['pre', 'uuu', 'x1']).match(rule)   # True
```

---

## Install

```bash
pip install latychain
```

Python 3.10+.

---

## Core concepts

### `Chain` — immutable ordered container

Elements are plain strings (data) or `ChainRuleAtom` instances (pattern rules). Hashable, usable as dict keys.

```python
c = Chain(['a', 'b', 'c'])
len(c)           # 3
c[0]             # 'a'
c + Chain(['d']) # Chain(['a','b','c','d'])
str(c)           # '.a.b.c'
c.to_list()      # ['a','b','c']

# Matching (backtracking, non-greedy)
c.match(Chain(['a', 'b']))              # False — full match fails
c.match(Chain(['a', 'b']), partial=True) # True — prefix match
c.startswith(Chain(['a', 'b']))         # True — same as partial
```

### `ChainRuleAtom` — rule atoms for pattern building

All created via static factories:

| Factory | Matches |
|---------|---------|
| `any(min=0, max=0)` | N arbitrary elements (`max=0` = unbounded). Non-greedy. |
| `rex(pattern)` | Single element via regex `fullmatch` |
| `enum(*chains)` | One of several alternative chains |
| `apply(func, long=1)` | Custom predicate receiving a `Chain` of `long` elements |
| `long(min, max=None)` | String length in `[min, max]` |
| `un(value)` | Any element not equal to `value` |
| `ext(chain=None)` | Optional segment — match or skip |

```python
from latychain import Chain, ChainRuleAtom

# any — between min and max arbitrary elements
rule = Chain([ChainRuleAtom.any(0), 'yyy'])  # 0 or more before 'yyy'
Chain(['x', 'yyy']).match(rule)              # True
Chain(['yyy']).match(rule)                   # True

# rex — regex fullmatch on one element
Chain(['h1']).match(Chain([ChainRuleAtom.rex(r'h[12]')]))    # True
Chain(['123']).match(Chain([ChainRuleAtom.rex(r'\d+')]))     # True

# enum — pick one alternative
rule = Chain([ChainRuleAtom.enum(
    Chain(['user', ChainRuleAtom.any(0)]),
    Chain(['admin', ChainRuleAtom.any(0)]),
)])
Chain(['user', 'login']).match(rule)   # True
Chain(['guest']).match(rule)           # False

# ext — optional segment
rule = Chain(['a', ChainRuleAtom.ext(Chain(['pi'])), 'b'])
Chain(['a', 'b']).match(rule)      # True — ext skipped
Chain(['a', 'pi', 'b']).match(rule) # True — ext matched
Chain(['a', 'x', 'b']).match(rule)  # False

# apply — custom predicate
rule = Chain([ChainRuleAtom.apply(lambda c: str(c).startswith('.x'))])
Chain(['xhello']).match(rule)   # True

# long — string length constraint
Chain(['abc']).match(Chain([ChainRuleAtom.long(3)]))        # True
Chain(['abc']).match(Chain([ChainRuleAtom.long(2, 5)]))     # True

# un — negation
Chain(['user']).match(Chain([ChainRuleAtom.un('admin')]))   # True
```

---

## `.xxx.yyy` syntax sugar

An import hook transforms `.xxx.yyy` expressions into `Chain([...])` calls at compile time.
**Opt-in per module** — add `# useLatyChain` to the first few lines. `Chain` and `ChainRuleAtom`
are auto-injected, no explicit import needed.

**Requires two files** — the hook only transforms imported modules, not the entry script:

**runner.py** (no sugar):
```python
import latychain.ChainDotRule   # registers the hook
import my_code                   # this file gets transformed
```

**my_code.py** (uses sugar):
```python
# useLatyChain

# Segments without () → strings; with () → ChainRuleAtom.xxx()
data = .heading.h1                       # → Chain(['heading', 'h1'])
rule = .any(0).uuu.rex(r'x\d')

x = .x.uuu.x1
x.match(rule)                             # True

# Nested enum
rule2 = .any(0).enum(
    .admin.any(0),
    .user.any(0),
).rex(r'\d+')

Chain(['user', 'login', '123']).match(rule2)   # True
```

The transformer skips strings, comments, float literals, and `obj.attr` access.
Only files with `# useLatyChain` are transformed — all other imports pass through untouched.

**Numeric segments (`.123`) are not supported** — use `Chain(['123'])` instead.

---

## Develop

```bash
git clone https://github.com/fnsii/latychain
cd latychain
uv venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
uv run python test/run_all.py           # 61 tests
```
