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.
@@ -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: "&lt;Admin&gt;"
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
+ }