caspian-utils 0.0.1
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.
- package/dist/docs/auth.md +690 -0
- package/dist/docs/cache.md +257 -0
- package/dist/docs/commands.md +117 -0
- package/dist/docs/database.md +351 -0
- package/dist/docs/fetch-data.md +260 -0
- package/dist/docs/index.md +65 -0
- package/dist/docs/installation.md +98 -0
- package/dist/docs/metadata.md +196 -0
- package/dist/docs/project-structure.md +193 -0
- package/dist/docs/pulsepoint.md +368 -0
- package/dist/docs/routing.md +231 -0
- package/dist/docs/state.md +264 -0
- package/dist/docs/validation.md +293 -0
- package/package.json +16 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: State Management
|
|
3
|
+
description: Manage transient Caspian server state with `casp.state_manager` and `StateManager`, using request-scoped ContextVar storage, shallow AttributeDict reads, listener callbacks, and session-backed JSON persistence.
|
|
4
|
+
related:
|
|
5
|
+
title: Related docs
|
|
6
|
+
description: Pair server request state with auth and RPC flows, then read the PulsePoint guide when the state really belongs in the browser instead of the request lifecycle.
|
|
7
|
+
links:
|
|
8
|
+
- /docs/auth
|
|
9
|
+
- /docs/fetch-data
|
|
10
|
+
- /docs/pulsepoint
|
|
11
|
+
- /docs/project-structure
|
|
12
|
+
- /docs/index
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
This page explains the current installed Caspian state manager API for request-scoped state, session-backed persistence, listener callbacks, and AttributeDict reads.
|
|
16
|
+
|
|
17
|
+
Treat `casp.state_manager` as the transient server-state layer in Caspian. Use it for short-lived state that should be available during the current request and, where the middleware flow allows it, across a tightly scoped follow-up request. Do not treat it as a database, a long-lived session model, or a replacement for PulsePoint browser state.
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Caspian exposes request state through `casp.state_manager`.
|
|
22
|
+
|
|
23
|
+
Import the public API like this:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from casp.state_manager import StateManager
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The current installed implementation lives in `.venv/Lib/site-packages/casp/state_manager.py`.
|
|
30
|
+
|
|
31
|
+
The real API surface is:
|
|
32
|
+
|
|
33
|
+
- request initialization with `StateManager.init(request)`
|
|
34
|
+
- top-level state reads with `StateManager.get_state(...)`
|
|
35
|
+
- top-level state writes with `StateManager.set_state(...)`
|
|
36
|
+
- reset operations with `StateManager.reset_state(...)`
|
|
37
|
+
- request-local subscriptions with `StateManager.subscribe(...)`
|
|
38
|
+
|
|
39
|
+
Use this layer when middleware, route handlers, auth flows, or RPC actions need a shared transient state bag without explicitly threading the request object through every helper.
|
|
40
|
+
|
|
41
|
+
## Default State Rule
|
|
42
|
+
|
|
43
|
+
- Use `StateManager` for short-lived server state such as flash-style messages, transient form state, or middleware-to-route coordination.
|
|
44
|
+
- Keep values JSON-serializable because every write persists the full state dict with `json.dumps(...)`.
|
|
45
|
+
- Read and write top-level keys. The current implementation does not provide deep merge, nested path helpers, or delete helpers.
|
|
46
|
+
- Let middleware initialize the manager. App code usually should not call `StateManager.init(...)` directly.
|
|
47
|
+
- Use PulsePoint for browser reactivity and persistent data stores for durable application state.
|
|
48
|
+
|
|
49
|
+
## Request-Scoped Storage Model
|
|
50
|
+
|
|
51
|
+
The installed implementation stores state in `ContextVar` containers:
|
|
52
|
+
|
|
53
|
+
- `_state_data` holds the current request's state dict
|
|
54
|
+
- `_state_listeners` holds the current request's listener callbacks
|
|
55
|
+
- `_current_request` holds the current request object
|
|
56
|
+
|
|
57
|
+
That means the state manager is request-scoped, not process-global. One request does not share in-memory state or listeners with another request.
|
|
58
|
+
|
|
59
|
+
The session persistence layer is separate from the in-memory request copy. `save_state()` serializes the current dict to JSON and stores it under the constant key `app_state_cL7y4KirLp`.
|
|
60
|
+
|
|
61
|
+
Important implementation detail:
|
|
62
|
+
|
|
63
|
+
- the current code reads and writes `request.state.session`, not `request.session`
|
|
64
|
+
|
|
65
|
+
In other words, cross-request persistence depends on the surrounding Caspian middleware keeping `request.state.session` available and in sync with session storage.
|
|
66
|
+
|
|
67
|
+
## Request Lifecycle
|
|
68
|
+
|
|
69
|
+
The current request lifecycle is:
|
|
70
|
+
|
|
71
|
+
1. `StateManager.init(request)` stores the request object.
|
|
72
|
+
2. It clears the in-memory state dict and listener list for the new request.
|
|
73
|
+
3. It calls `load_state()` and tries to decode any previously saved JSON from the session bucket.
|
|
74
|
+
4. It checks the `X-PulsePoint-Wire` header.
|
|
75
|
+
5. When that header is missing or falsy, it immediately calls `reset_state()`.
|
|
76
|
+
|
|
77
|
+
That last step matters. In the current implementation, non-wire requests start by clearing the loaded state and writing the empty state back to the session bucket.
|
|
78
|
+
|
|
79
|
+
Treat the current behavior as request-local state plus framework-managed transient persistence for wire-driven flows. If you expect full-page redirect flash semantics, verify the exact middleware and request path in your local Caspian version before depending on it.
|
|
80
|
+
|
|
81
|
+
You usually do not initialize this manually in route code. The current auth middleware docs already show `StateManager.init(request)` running during request setup.
|
|
82
|
+
|
|
83
|
+
## Reading State
|
|
84
|
+
|
|
85
|
+
Use `StateManager.get_state(...)` to read the whole state bag or a single top-level key.
|
|
86
|
+
|
|
87
|
+
Behavior:
|
|
88
|
+
|
|
89
|
+
- `StateManager.get_state()` returns an `AttributeDict` wrapping the full top-level state dict
|
|
90
|
+
- `StateManager.get_state("key", initial_value)` returns the stored value or the provided default
|
|
91
|
+
- when the returned value is a dict, the direct return value is wrapped in `AttributeDict`
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from casp.state_manager import StateManager
|
|
97
|
+
|
|
98
|
+
StateManager.set_state("flash", {
|
|
99
|
+
"type": "success",
|
|
100
|
+
"message": "Profile updated.",
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
flash = StateManager.get_state("flash")
|
|
104
|
+
if flash is not None:
|
|
105
|
+
print(flash.type)
|
|
106
|
+
print(flash.message)
|
|
107
|
+
|
|
108
|
+
state = StateManager.get_state()
|
|
109
|
+
print(state.flash["message"])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Implementation note:
|
|
113
|
+
|
|
114
|
+
- `AttributeDict` is shallow in the current file
|
|
115
|
+
|
|
116
|
+
`StateManager.get_state("flash")` wraps that one dict, so `flash.message` works. `StateManager.get_state()` wraps only the top-level state object, so nested dict values remain plain dicts unless you fetch that key directly.
|
|
117
|
+
|
|
118
|
+
Do not assume recursive dot notation such as `StateManager.get_state().flash.message` unless your stored nested value has already been wrapped elsewhere.
|
|
119
|
+
|
|
120
|
+
## Writing State
|
|
121
|
+
|
|
122
|
+
Use `StateManager.set_state(key, value)` to replace a top-level key.
|
|
123
|
+
|
|
124
|
+
Current behavior:
|
|
125
|
+
|
|
126
|
+
- updates the current in-memory dict in place
|
|
127
|
+
- notifies every registered listener
|
|
128
|
+
- serializes the entire state dict into the session bucket
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from casp.rpc import rpc
|
|
134
|
+
from casp.state_manager import StateManager
|
|
135
|
+
|
|
136
|
+
@rpc(require_auth=True)
|
|
137
|
+
async def save_profile(data: dict):
|
|
138
|
+
StateManager.set_state("flash", {
|
|
139
|
+
"type": "success",
|
|
140
|
+
"message": "Profile updated.",
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return {"success": True}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Keep writes JSON-safe. The current `save_state()` implementation does not catch `TypeError` from `json.dumps(...)`, so non-serializable values such as file handles, request objects, response objects, or arbitrary class instances can break the write path.
|
|
147
|
+
|
|
148
|
+
## Resetting State
|
|
149
|
+
|
|
150
|
+
Use `StateManager.reset_state(...)` to clear either one key or the whole state dict.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from casp.state_manager import StateManager
|
|
156
|
+
|
|
157
|
+
StateManager.reset_state("flash")
|
|
158
|
+
StateManager.reset_state()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Current behavior is slightly surprising:
|
|
162
|
+
|
|
163
|
+
- `StateManager.reset_state()` replaces the whole state dict with `{}`
|
|
164
|
+
- `StateManager.reset_state("flash")` does not delete the key; it sets the existing key to `None`
|
|
165
|
+
- both forms notify listeners and save the updated state back to the session bucket
|
|
166
|
+
|
|
167
|
+
If you need true key removal, there is no public delete helper in the current implementation.
|
|
168
|
+
|
|
169
|
+
## Request-Local Listeners
|
|
170
|
+
|
|
171
|
+
`StateManager.subscribe(listener)` registers a callback for the current request context.
|
|
172
|
+
|
|
173
|
+
Behavior:
|
|
174
|
+
|
|
175
|
+
- appends the listener to the current request's listener list
|
|
176
|
+
- immediately invokes the listener with the current state dict
|
|
177
|
+
- returns an `unsubscribe()` function that removes that listener from the current request list
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from casp.state_manager import StateManager
|
|
183
|
+
|
|
184
|
+
def log_changes(state):
|
|
185
|
+
if state.get("error"):
|
|
186
|
+
print(f"Error detected: {state['error']}")
|
|
187
|
+
|
|
188
|
+
unsubscribe = StateManager.subscribe(log_changes)
|
|
189
|
+
|
|
190
|
+
StateManager.set_state("error", "Invalid credentials")
|
|
191
|
+
|
|
192
|
+
unsubscribe()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Treat these listeners as request-local callbacks, not as a cross-request event bus.
|
|
196
|
+
|
|
197
|
+
The current implementation resets `_state_listeners` inside `StateManager.init(...)`, so subscriptions do not survive into later requests.
|
|
198
|
+
|
|
199
|
+
## API Reference
|
|
200
|
+
|
|
201
|
+
| Method | Purpose | Current behavior |
|
|
202
|
+
| --- | --- | --- |
|
|
203
|
+
| `init(request)` | Prepare request-bound state | Stores the request, clears in-memory state and listeners, loads saved JSON state, then clears state again for non-wire requests. |
|
|
204
|
+
| `get_state(key=None, initial_value=None)` | Read all state or one key | Returns a shallow `AttributeDict` for dict values and the provided default for missing keys. |
|
|
205
|
+
| `set_state(key, value)` | Replace one top-level key | Mutates the state dict, notifies listeners, and JSON-saves the full state bag. |
|
|
206
|
+
| `reset_state(key=None)` | Clear one key or everything | Sets one key to `None` or replaces the full dict with `{}`. |
|
|
207
|
+
| `subscribe(listener)` | Listen for request-local changes | Immediately invokes the listener and returns an unsubscribe closure. |
|
|
208
|
+
| `save_state()` | Persist current state | Serializes the full dict into the session bucket under `APP_STATE_KEY`. |
|
|
209
|
+
| `load_state()` | Rehydrate saved state | Loads JSON when the session bucket exists and ignores malformed JSON. |
|
|
210
|
+
|
|
211
|
+
## Current Implementation Notes
|
|
212
|
+
|
|
213
|
+
- The persistence bucket key is `StateManager.APP_STATE_KEY = "app_state_cL7y4KirLp"`.
|
|
214
|
+
- `load_state()` only catches `json.JSONDecodeError`; malformed JSON is ignored silently.
|
|
215
|
+
- `save_state()` writes the entire state dict every time `set_state()` or `reset_state()` runs.
|
|
216
|
+
- `set_state()` replaces a top-level key value. There is no deep merge behavior.
|
|
217
|
+
- `notify_listeners()` runs on every `set_state()`, successful `load_state()`, and every `reset_state()` call.
|
|
218
|
+
- Listener callbacks receive the raw state dict, not an `AttributeDict` wrapper.
|
|
219
|
+
- `AttributeDict.__getattr__` raises `AttributeError` for missing keys and `__setattr__` writes back into the dict.
|
|
220
|
+
- Because persistence uses JSON, keep stored values simple: strings, numbers, booleans, lists, dicts, and `None`.
|
|
221
|
+
- The current lifecycle resets loaded state on non-wire requests, so verify redirect and flash behavior against the actual request type you are using.
|
|
222
|
+
|
|
223
|
+
## Recommended Usage Pattern
|
|
224
|
+
|
|
225
|
+
Use `StateManager` close to the request boundary and keep the stored payloads small.
|
|
226
|
+
|
|
227
|
+
Common placement patterns are:
|
|
228
|
+
|
|
229
|
+
- route handlers that need to share transient data with helpers in the same request
|
|
230
|
+
- `@rpc()` actions that record short-lived success or error state during a wire-driven flow
|
|
231
|
+
- auth-aware flows that need request-bound coordination between middleware and route logic
|
|
232
|
+
- shared helpers under `src/lib/` when multiple routes use the same transient-state conventions
|
|
233
|
+
|
|
234
|
+
Good candidates for this state layer are:
|
|
235
|
+
|
|
236
|
+
- success or error messages
|
|
237
|
+
- temporary redirect or workflow flags
|
|
238
|
+
- lightweight request-bound metadata
|
|
239
|
+
- small structured dicts that can be serialized safely
|
|
240
|
+
|
|
241
|
+
Poor candidates are:
|
|
242
|
+
|
|
243
|
+
- ORM models or request objects
|
|
244
|
+
- file handles and uploads
|
|
245
|
+
- large payloads or cached query results
|
|
246
|
+
- durable business data that belongs in a database
|
|
247
|
+
- browser interaction state that should live in PulsePoint
|
|
248
|
+
|
|
249
|
+
For browser-triggered writes and route actions, pair this page with [fetch-data.md](./fetch-data.md). For middleware and session-aware auth flows, pair it with [auth.md](./auth.md).
|
|
250
|
+
|
|
251
|
+
## AI Routing Notes
|
|
252
|
+
|
|
253
|
+
If an AI agent is deciding how to use transient state in a Caspian app, apply these rules first.
|
|
254
|
+
|
|
255
|
+
- Use `StateManager` from `casp.state_manager` for request-scoped server state, not for durable application records.
|
|
256
|
+
- Assume the current API is top-level-key based only.
|
|
257
|
+
- Keep stored values JSON-serializable.
|
|
258
|
+
- Prefer `StateManager.get_state("key")` when you want dot notation on a dict payload.
|
|
259
|
+
- Do not assume recursive `AttributeDict` wrapping for nested dicts.
|
|
260
|
+
- Treat `subscribe(...)` listeners as request-local and short-lived.
|
|
261
|
+
- Be careful with full-page redirect assumptions because `init(request)` clears loaded state on non-wire requests.
|
|
262
|
+
- Use PulsePoint state for client interactivity and `StateManager` for server request flows.
|
|
263
|
+
- Check [auth.md](./auth.md) for middleware ordering and request initialization details.
|
|
264
|
+
- Check [project-structure.md](./project-structure.md) before deciding whether a transient-state helper belongs next to a route or in `src/lib/`.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Validation
|
|
3
|
+
description: Validate and sanitize Caspian inputs with `casp.validate`, `Validate`, `Rule`, direct validators, rule-based checks, and file or date or ID validation before route or RPC logic persists data, with Validate as the default server-side validation layer.
|
|
4
|
+
related:
|
|
5
|
+
title: Related docs
|
|
6
|
+
description: Use the auth guide for session and credential flows, the fetch-data guide when validation runs inside RPC actions, then use the structure guide to place reusable validators in the right layer.
|
|
7
|
+
links:
|
|
8
|
+
- /docs/auth
|
|
9
|
+
- /docs/fetch-data
|
|
10
|
+
- /docs/routing
|
|
11
|
+
- /docs/project-structure
|
|
12
|
+
- /docs/index
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
This page explains the current installed Caspian validation API for direct field checks, rule-based validation, sanitization, and reusable input guards.
|
|
16
|
+
|
|
17
|
+
Treat `casp.validate` as the default validation layer in Caspian app code. Do not rely on client-only checks or hand-rolled validation helpers when `Validate` and `Rule` already cover the input boundary.
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Caspian exposes validation through `casp.validate`.
|
|
22
|
+
|
|
23
|
+
Import the public API like this:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from casp.validate import Validate, Rule
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The current installed implementation lives in `.venv/Lib/site-packages/casp/validate.py`.
|
|
30
|
+
|
|
31
|
+
The real API surface is:
|
|
32
|
+
|
|
33
|
+
- direct single-value validation with `Validate.*(...)`
|
|
34
|
+
- multi-rule validation with `Validate.with_rules(...)`
|
|
35
|
+
- rule-string helpers built with `Rule`
|
|
36
|
+
|
|
37
|
+
Use validation at the boundary where untrusted data enters the app, especially in form handling, auth flows, and RPC mutations.
|
|
38
|
+
|
|
39
|
+
## Default Validation Rule
|
|
40
|
+
|
|
41
|
+
- Use `Validate.*(...)` for direct coercion, sanitization, and simple boundary checks.
|
|
42
|
+
- Use `Validate.with_rules(...)` plus `Rule` for maintained forms, RPC payloads, and multi-constraint workflows.
|
|
43
|
+
- Keep PulsePoint-side checks as UX improvements only; the authoritative validation still belongs on the server boundary.
|
|
44
|
+
- Validate before persistence, authorization decisions, external API calls, or any RPC side effect.
|
|
45
|
+
|
|
46
|
+
## Framework Internals Note
|
|
47
|
+
|
|
48
|
+
- The validation implementation lives in `.venv/Lib/site-packages/casp/validate.py`.
|
|
49
|
+
- Treat that file as framework code. Read it when the task is about validation internals, debugging framework behavior, or documenting the installed Caspian implementation.
|
|
50
|
+
- If upstream docs and the installed file disagree, prefer the installed file for local project guidance.
|
|
51
|
+
|
|
52
|
+
## Direct Validation
|
|
53
|
+
|
|
54
|
+
Use direct validators when you only need to check one value. These helpers usually return a sanitized or coerced value on success and `None` on failure.
|
|
55
|
+
|
|
56
|
+
The current `Validate` class exposes these main groups of helpers:
|
|
57
|
+
|
|
58
|
+
| Category | Methods | Notes |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| Strings and identifiers | `string`, `email`, `url`, `ip`, `uuid`, `ulid`, `cuid`, `cuid2`, `nanoid`, `bytes`, `xml` | `string()` trims input and escapes HTML by default. |
|
|
61
|
+
| Numbers | `int`, `big_int`, `float`, `decimal` | `decimal()` returns a quantized `Decimal`, with `scale=30` by default. |
|
|
62
|
+
| Dates and times | `date`, `date_time` | Uses PHP-style format tokens such as `Y-m-d` and `Y-m-d H:i:s`. |
|
|
63
|
+
| Booleans | `boolean` | Accepts `1`, `true`, `on`, `yes`, `y` and the corresponding false forms. |
|
|
64
|
+
| Structured values | `json`, `is_json`, `enum`, `enum_class` | `json()` serializes non-strings and validates JSON strings. |
|
|
65
|
+
| Text utility | `emojis` | Replaces supported text tokens such as `:wave:` and `<3`. |
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from casp.validate import Validate
|
|
71
|
+
|
|
72
|
+
email = Validate.email(" User@Example.com ")
|
|
73
|
+
# Result: "User@Example.com"
|
|
74
|
+
|
|
75
|
+
identifier = Validate.cuid2("tz4a98xxat96iws9zmbrgj3a")
|
|
76
|
+
# Result: "tz4a98xxat96iws9zmbrgj3a"
|
|
77
|
+
|
|
78
|
+
invalid = Validate.url("not-a-url")
|
|
79
|
+
# Result: None
|
|
80
|
+
|
|
81
|
+
safe_name = Validate.string(" <Admin> ")
|
|
82
|
+
# Result: "<Admin>"
|
|
83
|
+
|
|
84
|
+
enabled = Validate.boolean("yes")
|
|
85
|
+
# Result: True
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Implementation notes from the installed file:
|
|
89
|
+
|
|
90
|
+
- `Validate.string(value, escape_html=True)` trims and HTML-escapes by default.
|
|
91
|
+
- `Validate.date(...)` and `Validate.date_time(...)` use PHP-style format tokens mapped internally to Python datetime formats.
|
|
92
|
+
- `Validate.decimal(...)` returns a `Decimal` rounded with `ROUND_HALF_UP`.
|
|
93
|
+
- `Validate.cuid(...)` accepts either installed-library-backed legacy CUID or CUID2-like values depending on available packages.
|
|
94
|
+
- `Validate.is_json(...)` is the strict boolean check. `Validate.json(...)` returns serialized JSON for non-strings and parser text for invalid JSON strings.
|
|
95
|
+
|
|
96
|
+
Use this style for:
|
|
97
|
+
|
|
98
|
+
- one-off field checks
|
|
99
|
+
- simple preconditions before a query or mutation
|
|
100
|
+
- cases where `None` is enough to branch into an error response
|
|
101
|
+
|
|
102
|
+
For sign-in, signup, reset-password, and other session-aware flows, pair these checks with `casp.auth` patterns from `auth.md`.
|
|
103
|
+
|
|
104
|
+
## Rules Engine With `with_rules`
|
|
105
|
+
|
|
106
|
+
Use `Validate.with_rules(...)` when a field needs multiple constraints.
|
|
107
|
+
|
|
108
|
+
The current function signature is conceptually:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
Validate.with_rules(value, rules, confirmation_value=None)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It accepts two rule formats:
|
|
115
|
+
|
|
116
|
+
- a pipe-separated string such as `required|min:3|max:20|regex:^[a-zA-Z0-9_]+$`
|
|
117
|
+
- a list of rule strings, often created with `Rule`
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from casp.validate import Validate, Rule
|
|
123
|
+
|
|
124
|
+
def register_user(data):
|
|
125
|
+
username = data.get("username")
|
|
126
|
+
password = data.get("password")
|
|
127
|
+
password_confirmation = data.get("password_confirmation")
|
|
128
|
+
|
|
129
|
+
check = Validate.with_rules(
|
|
130
|
+
username,
|
|
131
|
+
"required|min:3|max:20|regex:^[a-zA-Z0-9_]+$",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
check_safe = Validate.with_rules(username, [
|
|
135
|
+
Rule.REQUIRED,
|
|
136
|
+
Rule.min(3),
|
|
137
|
+
Rule.max(20),
|
|
138
|
+
Rule.regex(r"^[a-zA-Z0-9_]+$"),
|
|
139
|
+
Rule.not_in_list(["admin", "root"]),
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
password_check = Validate.with_rules(password, [
|
|
143
|
+
Rule.REQUIRED,
|
|
144
|
+
Rule.min(8),
|
|
145
|
+
Rule.confirmed(),
|
|
146
|
+
], confirmation_value=password_confirmation)
|
|
147
|
+
|
|
148
|
+
if check_safe is not True:
|
|
149
|
+
return {"error": check_safe}
|
|
150
|
+
|
|
151
|
+
if password_check is not True:
|
|
152
|
+
return {"error": password_check}
|
|
153
|
+
|
|
154
|
+
return {"success": True}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The current implementation applies rules in order and stops on the first failure.
|
|
158
|
+
|
|
159
|
+
Contract:
|
|
160
|
+
|
|
161
|
+
- `True` means the value passed all rules
|
|
162
|
+
- a non-`True` result is the first validation error message
|
|
163
|
+
|
|
164
|
+
Prefer the `Rule` form when the rules will live in maintained project code because it reduces string typos and gives better autocomplete, even though `Rule` ultimately returns rule strings.
|
|
165
|
+
|
|
166
|
+
## Implemented Rule Set
|
|
167
|
+
|
|
168
|
+
The installed `apply_rule(...)` implementation currently supports these rule names:
|
|
169
|
+
|
|
170
|
+
- Presence and length: `required`, `min`, `max`, `size`, `between`
|
|
171
|
+
- Prefix and suffix: `startsWith`, `endsWith`
|
|
172
|
+
- Confirmation: `confirmed`
|
|
173
|
+
- Format and identity: `email`, `url`, `ip`, `uuid`, `ulid`, `cuid`, `cuid2`, `nanoid`, `json`, `regex`
|
|
174
|
+
- Numeric and boolean: `int`, `float`, `boolean`, `digits`, `digitsBetween`
|
|
175
|
+
- Membership: `in`, `notIn`
|
|
176
|
+
- Dates: `date`, `dateFormat`, `before`, `after`
|
|
177
|
+
- Files: `file`, `extensions`, `mimes`
|
|
178
|
+
|
|
179
|
+
The `Rule` helper currently provides:
|
|
180
|
+
|
|
181
|
+
- Constants: `REQUIRED`, `EMAIL`, `URL`, `IP`, `UUID`, `ULID`, `CUID`, `CUID2`, `NANOID`, `INTEGER`, `FLOAT`, `BOOLEAN`, `JSON`, `FILE`
|
|
182
|
+
- Methods: `min`, `max`, `size`, `starts_with`, `ends_with`, `confirmed`, `in_list`, `not_in_list`, `between`, `digits`, `digits_between`, `date`, `date_format`, `before`, `after`, `regex`, `extensions`, `mimes`
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
|
|
186
|
+
| Rule | Helper | Purpose |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| `required` | `Rule.REQUIRED` | Fails for `None`, empty strings, and empty collections. |
|
|
189
|
+
| `email` | `Rule.EMAIL` | Validates email format. |
|
|
190
|
+
| `min` / `max` | `Rule.min(5)` / `Rule.max(20)` | Enforces string length constraints. |
|
|
191
|
+
| `confirmed` | `Rule.confirmed()` | Compares the value to `confirmation_value`. |
|
|
192
|
+
| `in` / `notIn` | `Rule.in_list([...])` / `Rule.not_in_list([...])` | Restricts or blocks specific values. |
|
|
193
|
+
| `regex` | `Rule.regex(r"^[a-z]+$")` | Matches a custom regular expression. |
|
|
194
|
+
| `extensions` / `mimes` | `Rule.extensions([...])` / `Rule.mimes([...])` | Validates upload extensions or MIME types. |
|
|
195
|
+
|
|
196
|
+
## File Validation
|
|
197
|
+
|
|
198
|
+
The installed file includes upload-oriented helpers and rules.
|
|
199
|
+
|
|
200
|
+
- `file` passes when the value is a filesystem path, a readable file-like object, or an object with `read(...)`.
|
|
201
|
+
- `extensions` validates file suffixes from a path or `filename` attribute.
|
|
202
|
+
- `mimes` uses `python-magic` when installed for MIME sniffing, then falls back to `mimetypes.guess_type(...)`.
|
|
203
|
+
|
|
204
|
+
Use these rules for RPC uploads and form submissions that accept files.
|
|
205
|
+
|
|
206
|
+
## Sanitization
|
|
207
|
+
|
|
208
|
+
Validation in Caspian is more than rule checking. The installed implementation also sanitizes or normalizes several values.
|
|
209
|
+
|
|
210
|
+
Documented behaviors include:
|
|
211
|
+
|
|
212
|
+
- trimming and cleaning string inputs during direct validation
|
|
213
|
+
- sanitization helpers for safer string handling
|
|
214
|
+
- normalization for formats such as dates and booleans
|
|
215
|
+
|
|
216
|
+
Concrete examples from the current file:
|
|
217
|
+
|
|
218
|
+
- `Validate.string(...)` escapes HTML unless `escape_html=False`
|
|
219
|
+
- `Validate.boolean(...)` normalizes common truthy and falsy text values
|
|
220
|
+
- `Validate.date_time(...)` accepts multiple parse paths before formatting the final string
|
|
221
|
+
|
|
222
|
+
This means validation should happen before application code persists, compares, or authorizes incoming values.
|
|
223
|
+
|
|
224
|
+
## Optional Dependency Behavior
|
|
225
|
+
|
|
226
|
+
The installed implementation uses optional packages when they are available:
|
|
227
|
+
|
|
228
|
+
- `email-validator` for stronger email validation
|
|
229
|
+
- `python-dateutil` for more flexible datetime parsing
|
|
230
|
+
- `python-magic` for content-based MIME detection
|
|
231
|
+
- `cuid2` and `cuid` packages to recognize specific ID formats
|
|
232
|
+
|
|
233
|
+
If those packages are missing, the module falls back to simpler behavior.
|
|
234
|
+
|
|
235
|
+
## Current Implementation Notes
|
|
236
|
+
|
|
237
|
+
- Unknown rule names currently fall through and pass because `apply_rule(...)` returns `True` when a rule is not recognized.
|
|
238
|
+
- The upstream docs show `alpha_num`, but the current installed `apply_rule(...)` implementation does not implement an `alpha_num` rule.
|
|
239
|
+
- Use `regex` for alphanumeric username rules until the framework adds a dedicated `alpha_num` rule.
|
|
240
|
+
- `Rule` is an IntelliSense and string-builder helper, not an enum-backed validator by itself.
|
|
241
|
+
|
|
242
|
+
## Recommended Usage Pattern
|
|
243
|
+
|
|
244
|
+
Validate close to the input boundary.
|
|
245
|
+
|
|
246
|
+
Common placement patterns are:
|
|
247
|
+
|
|
248
|
+
- inside `@rpc()` actions before database writes
|
|
249
|
+
- inside route-level backend files such as `src/app/**/index.py`
|
|
250
|
+
- in shared helpers under `src/lib/` when multiple routes or actions reuse the same rule sets
|
|
251
|
+
|
|
252
|
+
For browser-triggered writes and form submissions, pair this page with [fetch-data.md](./fetch-data.md).
|
|
253
|
+
|
|
254
|
+
Example RPC action:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from casp.rpc import rpc
|
|
258
|
+
from casp.validate import Validate, Rule
|
|
259
|
+
|
|
260
|
+
@rpc()
|
|
261
|
+
def create_account(data: dict):
|
|
262
|
+
email = Validate.email(data.get("email"))
|
|
263
|
+
password = data.get("password")
|
|
264
|
+
|
|
265
|
+
password_check = Validate.with_rules(password, [
|
|
266
|
+
Rule.REQUIRED,
|
|
267
|
+
Rule.min(8),
|
|
268
|
+
])
|
|
269
|
+
|
|
270
|
+
if email is None:
|
|
271
|
+
return {"error": "Invalid email address."}
|
|
272
|
+
|
|
273
|
+
if password_check is not True:
|
|
274
|
+
return {"error": password_check}
|
|
275
|
+
|
|
276
|
+
return {"success": True}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## AI Routing Notes
|
|
280
|
+
|
|
281
|
+
If an AI agent is deciding how to validate input in Caspian, apply these rules first.
|
|
282
|
+
|
|
283
|
+
- Treat `Validate` and `Rule` as the default validation API for Caspian app code.
|
|
284
|
+
- Use direct `Validate.*(...)` calls for quick single-field checks.
|
|
285
|
+
- Use `Validate.with_rules(...)` for multi-constraint validation.
|
|
286
|
+
- Prefer `Rule` helpers in maintained project code instead of long rule strings.
|
|
287
|
+
- Validate request, form, and RPC payloads before database writes or auth-sensitive logic.
|
|
288
|
+
- Put reusable validation workflows in `src/lib/` when multiple routes need them.
|
|
289
|
+
- Keep route-specific validation next to the route or action that owns the input boundary.
|
|
290
|
+
- Use `.venv/Lib/site-packages/casp/validate.py` only when the task is about Caspian core validation internals or framework debugging.
|
|
291
|
+
- Prefer the implemented local rule set over upstream examples when they differ.
|
|
292
|
+
- Pair validation with [fetch-data.md](./fetch-data.md) when user input arrives through `pp.rpc()`.
|
|
293
|
+
- Check [project-structure.md](./project-structure.md) before deciding whether a validator belongs in `src/app/` or `src/lib/`.
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "caspian-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Caspian tooling",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"caspian",
|
|
11
|
+
"docs"
|
|
12
|
+
],
|
|
13
|
+
"author": "Jefferson Abraham Omier <thesteelninjacode@gmail.com>",
|
|
14
|
+
"license": "MTI",
|
|
15
|
+
"type": "module"
|
|
16
|
+
}
|