@rip-lang/schema 0.2.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/README.md +1042 -0
- package/SCHEMA.md +1113 -0
- package/emit-sql.js +460 -0
- package/emit-types.js +366 -0
- package/generate.js +144 -0
- package/grammar.rip +504 -0
- package/index.js +39 -0
- package/lexer.js +438 -0
- package/orm.js +916 -0
- package/package.json +62 -0
- package/parser.js +246 -0
- package/runtime.js +494 -0
package/SCHEMA.md
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
# Rip Unified Schema System
|
|
2
|
+
|
|
3
|
+
**A Single Source of Truth for Types, Validation, Database, UI, and State**
|
|
4
|
+
|
|
5
|
+
## Vision
|
|
6
|
+
|
|
7
|
+
Create a unified schema mechanism that combines the best ideas from:
|
|
8
|
+
|
|
9
|
+
- **SPOT/Sparse Notation** — Formal type system, proven with 100+ Sage widgets
|
|
10
|
+
- **rip-schema** — Concise Rails-like DSL, modern tooling
|
|
11
|
+
- **TypeScript** — Type inference, generics, discriminated unions
|
|
12
|
+
- **Zod** — Runtime validation, composable schemas
|
|
13
|
+
- **Sage** — Declarative UI, layout system, event model
|
|
14
|
+
- **Vue State** — Path-based state management, hydration
|
|
15
|
+
|
|
16
|
+
The goal: **Define once, generate everything.**
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
1. [Design Principles](#design-principles)
|
|
23
|
+
- [Schema-First Architecture](#5-schema-first-architecture)
|
|
24
|
+
2. [Syntax Comparison](#syntax-comparison)
|
|
25
|
+
3. [Unified Syntax Specification](#unified-syntax-specification)
|
|
26
|
+
4. [Generation Targets](#generation-targets)
|
|
27
|
+
5. [Core Types](#core-types)
|
|
28
|
+
6. [Enums](#enums)
|
|
29
|
+
7. [Models (Database + Validation)](#models)
|
|
30
|
+
8. [Widgets (UI Components)](#widgets)
|
|
31
|
+
9. [Forms (Model + Layout)](#forms)
|
|
32
|
+
10. [State Management](#state-management)
|
|
33
|
+
11. [Composition and Reuse](#composition-and-reuse)
|
|
34
|
+
12. [Future Work](#future-work)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Design Principles
|
|
39
|
+
|
|
40
|
+
### 1. Common Things Easy, Rare Things Possible
|
|
41
|
+
|
|
42
|
+
```coffeescript
|
|
43
|
+
# Easy (90% of cases)
|
|
44
|
+
@string 'username!', [3, 20] # Required, 3-20 chars
|
|
45
|
+
|
|
46
|
+
# Possible (10% of cases)
|
|
47
|
+
@string 'code', min: 3, max: 20, pattern: /^[A-Z]+$/, transform: upper
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Concise but Complete
|
|
51
|
+
|
|
52
|
+
```coffeescript
|
|
53
|
+
# Concise syntax with full power
|
|
54
|
+
@model User
|
|
55
|
+
email!# email # Required + Unique (one line)
|
|
56
|
+
role! Role, [user] # Enum with default
|
|
57
|
+
settings? json, [{}] # Optional with default
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Single Source of Truth (SPOT)
|
|
61
|
+
|
|
62
|
+
One schema definition generates:
|
|
63
|
+
- TypeScript types
|
|
64
|
+
- Zod validators
|
|
65
|
+
- SQL DDL / migrations
|
|
66
|
+
- UI component props
|
|
67
|
+
- Form layouts
|
|
68
|
+
- API contracts
|
|
69
|
+
|
|
70
|
+
### 4. Familiar Conventions
|
|
71
|
+
|
|
72
|
+
| Convention | Meaning | Origin |
|
|
73
|
+
|------------|---------|--------|
|
|
74
|
+
| `!` suffix | Required | rip-schema |
|
|
75
|
+
| `#` suffix | Unique | rip-schema (CSS `#id`) |
|
|
76
|
+
| `?` suffix | Optional | TypeScript |
|
|
77
|
+
| `[min, max]` | Range | rip-schema |
|
|
78
|
+
| `[default]` | Default value | rip-schema |
|
|
79
|
+
| `$Parent` | Inheritance | Sage |
|
|
80
|
+
| `@name` | Named definition | Sage |
|
|
81
|
+
|
|
82
|
+
### 5. Schema-First Architecture
|
|
83
|
+
|
|
84
|
+
When you think schema-first, state becomes the single source of truth, and the UI becomes a pure renderer of valid state:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
88
|
+
│ │
|
|
89
|
+
│ Schema ──────► State ──────► UI │
|
|
90
|
+
│ │ │ │ │
|
|
91
|
+
│ defines always just │
|
|
92
|
+
│ structure valid renders │
|
|
93
|
+
│ │
|
|
94
|
+
│ User Action ──► Validate ──► Update State ──► Re-render │
|
|
95
|
+
│ │ │
|
|
96
|
+
│ reject if │
|
|
97
|
+
│ invalid │
|
|
98
|
+
│ │
|
|
99
|
+
└─────────────────────────────────────────────────────────────┘
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**The guarantees:**
|
|
103
|
+
|
|
104
|
+
| Layer | Guarantee |
|
|
105
|
+
|-------|-----------|
|
|
106
|
+
| **State** | Always matches schema |
|
|
107
|
+
| **UI** | Always receives valid data |
|
|
108
|
+
| **Actions** | Can't corrupt state |
|
|
109
|
+
| **Persistence** | Only valid data saved |
|
|
110
|
+
|
|
111
|
+
**UI becomes dumb (in a good way):**
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
// BEFORE: Defensive coding everywhere
|
|
115
|
+
function PatientCard({ patient }) {
|
|
116
|
+
if (!patient) return null
|
|
117
|
+
if (!patient.name || typeof patient.name !== 'string') return <Error />
|
|
118
|
+
if (patient.name.length > 100) patient.name = patient.name.slice(0, 100)
|
|
119
|
+
// ... 50 more checks
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// AFTER: Trust the state - it's always valid
|
|
123
|
+
function PatientCard({ patient }) {
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<h2>{patient.name}</h2> {/* Guaranteed string, 1-100 chars */}
|
|
127
|
+
<p>{patient.email}</p> {/* Guaranteed valid email */}
|
|
128
|
+
<Badge>{patient.role}</Badge> {/* Guaranteed valid enum */}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Mutations go through schema validation:**
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
// State update = validate + commit
|
|
138
|
+
app.set('/currentPatient/name', 'John Doe')
|
|
139
|
+
// → Schema validates 'John Doe' is string [1,100]
|
|
140
|
+
// → If valid: state updates, UI re-renders
|
|
141
|
+
// → If invalid: rejected, state unchanged
|
|
142
|
+
|
|
143
|
+
// Batch updates are atomic
|
|
144
|
+
app.update('/currentPatient', { name: 'John', email: 'john@example.com' })
|
|
145
|
+
// → Schema validates entire Patient object
|
|
146
|
+
// → All or nothing
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**The UI is just a function of valid state:**
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
UI = render(validState)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
No more validity checks in components. No more defensive coding. No more "what if the data is wrong?" — the data CAN'T be wrong because it passed schema validation.
|
|
156
|
+
|
|
157
|
+
This is exactly how Sage worked: the UI was "dumb" — it just rendered whatever state the engine gave it. All the smarts were in the schema + engine layer. This architecture enabled teams to build complex medical UIs (like CPRS) faster than traditional approaches.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Syntax Comparison
|
|
162
|
+
|
|
163
|
+
### Field Definitions
|
|
164
|
+
|
|
165
|
+
| Concept | SPOT | rip-schema | Unified |
|
|
166
|
+
|---------|------|------------|---------|
|
|
167
|
+
| Required string | `name PrintableString Range(0..100)` | `@string 'name!', 100` | `name! string, [1, 100]` |
|
|
168
|
+
| Optional string | `name PrintableString Range(0..100) Optional` | `@string 'name', 100` | `name? string, [0, 100]` |
|
|
169
|
+
| Required + Unique | (manual index) | `@email 'email!#'` | `email!# email` |
|
|
170
|
+
| Integer with range | `age Integer Range(0..120)` | `@integer 'age', [0, 120]` | `age integer, [0, 120]` |
|
|
171
|
+
| Integer with default | `count Integer Default 0` | `@integer 'count', [0]` | `count integer, [0]` |
|
|
172
|
+
| Boolean with default | `active Boolean Default true` | `@boolean 'active', true` | `active boolean, [true]` |
|
|
173
|
+
| Enum field | `role Enumerated { admin(0), user(1) }` | `@enum Role: admin, user` | `role Role, [user]` |
|
|
174
|
+
|
|
175
|
+
### Type Definitions
|
|
176
|
+
|
|
177
|
+
| Concept | SPOT | Unified |
|
|
178
|
+
|---------|------|---------|
|
|
179
|
+
| Enum | `Enumerated { a(0), b(1), c(2) }` | `@enum Name: a, b, c` |
|
|
180
|
+
| Object/Sequence | `Name ::= Sequence { ... }` | `@type Name` or `@model Name` |
|
|
181
|
+
| Inheritance | `Child ::= Extends Parent { ... }` | `@widget Child: $Parent` |
|
|
182
|
+
| Reference | `field Type Reference` | `field Type` or `belongs_to` |
|
|
183
|
+
| Collection | `Set { item Type }` | `field Type[]` |
|
|
184
|
+
| Polymorphic | `Any DefinedBy Widget` | `field Widget` (union) |
|
|
185
|
+
|
|
186
|
+
### Attributes and Events
|
|
187
|
+
|
|
188
|
+
| Concept | SPOT/Sage | Unified |
|
|
189
|
+
|---------|-----------|---------|
|
|
190
|
+
| Attributes | `[ color, size="16" ]` | `{ color, size: "16" }` |
|
|
191
|
+
| Events | `[ onAction, onChange ]` | `@events onAction, onChange` |
|
|
192
|
+
| Event handler | `[ onAction="doSomething()" ]` | `onAction "doSomething()"` |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Unified Syntax Specification
|
|
197
|
+
|
|
198
|
+
### Basic Structure
|
|
199
|
+
|
|
200
|
+
```coffeescript
|
|
201
|
+
# Import other schemas
|
|
202
|
+
@import "./common.schema"
|
|
203
|
+
|
|
204
|
+
# Enum definitions
|
|
205
|
+
@enum EnumName: value1, value2, value3
|
|
206
|
+
|
|
207
|
+
# Type definitions (reusable structures)
|
|
208
|
+
@type TypeName
|
|
209
|
+
field1! type, [constraints], { attributes }
|
|
210
|
+
field2? type, [default]
|
|
211
|
+
|
|
212
|
+
# Model definitions (database-backed)
|
|
213
|
+
@model ModelName
|
|
214
|
+
field1! type
|
|
215
|
+
@timestamps
|
|
216
|
+
@index field1#
|
|
217
|
+
|
|
218
|
+
# Widget definitions (UI components)
|
|
219
|
+
@widget WidgetName: $ParentWidget
|
|
220
|
+
prop1 type, [default]
|
|
221
|
+
@events event1, event2
|
|
222
|
+
|
|
223
|
+
# Form definitions (model + layout)
|
|
224
|
+
@form FormName: $ModelName
|
|
225
|
+
layout type
|
|
226
|
+
field1 { x: 0, y: 0 }
|
|
227
|
+
@actions
|
|
228
|
+
submit { ... }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Field Syntax
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
fieldName[!][#][?] type[, [min, max]][, [default]][, { attributes }]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Modifiers:**
|
|
238
|
+
- `!` — Required (NOT NULL)
|
|
239
|
+
- `#` — Unique (creates unique index)
|
|
240
|
+
- `?` — Explicitly optional
|
|
241
|
+
|
|
242
|
+
**Constraints:**
|
|
243
|
+
- `[min, max]` — Range (length for strings, value for numbers)
|
|
244
|
+
- `[value]` — Default value
|
|
245
|
+
- `{ key: value }` — Named attributes
|
|
246
|
+
|
|
247
|
+
### Examples
|
|
248
|
+
|
|
249
|
+
```coffeescript
|
|
250
|
+
# Required string, 1-100 chars
|
|
251
|
+
name! string, [1, 100]
|
|
252
|
+
|
|
253
|
+
# Required email, unique
|
|
254
|
+
email!# email
|
|
255
|
+
|
|
256
|
+
# Optional string, max 500 chars
|
|
257
|
+
bio? string, [0, 500]
|
|
258
|
+
|
|
259
|
+
# Integer, range 1-5, default 3
|
|
260
|
+
rating integer, [1, 5], [3]
|
|
261
|
+
|
|
262
|
+
# Enum with default
|
|
263
|
+
status Status, [pending]
|
|
264
|
+
|
|
265
|
+
# JSON with default empty object
|
|
266
|
+
settings json, [{}]
|
|
267
|
+
|
|
268
|
+
# Reference to another type
|
|
269
|
+
address? Address
|
|
270
|
+
|
|
271
|
+
# Array of items
|
|
272
|
+
tags string[]
|
|
273
|
+
|
|
274
|
+
# With attributes (UI hints, etc.)
|
|
275
|
+
phone! string, [10, 15], { label: "Phone Number", mask: "(###) ###-####" }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Generation Targets
|
|
281
|
+
|
|
282
|
+
From a single schema definition, generate:
|
|
283
|
+
|
|
284
|
+
### 1. TypeScript Types
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// From: @model User with email!# email, role! Role
|
|
288
|
+
type User = {
|
|
289
|
+
id: number
|
|
290
|
+
email: string
|
|
291
|
+
role: Role
|
|
292
|
+
createdAt: Date
|
|
293
|
+
updatedAt: Date
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type Role = 'admin' | 'user' | 'guest'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 2. Zod Validators
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// From: @model User
|
|
303
|
+
const UserSchema = z.object({
|
|
304
|
+
id: z.number().int(),
|
|
305
|
+
email: z.string().email(),
|
|
306
|
+
role: z.enum(['admin', 'user', 'guest']).default('user'),
|
|
307
|
+
createdAt: z.date(),
|
|
308
|
+
updatedAt: z.date()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
type User = z.infer<typeof UserSchema>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 3. SQL DDL
|
|
315
|
+
|
|
316
|
+
```sql
|
|
317
|
+
-- From: @model User
|
|
318
|
+
CREATE TABLE users (
|
|
319
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
320
|
+
email TEXT NOT NULL UNIQUE,
|
|
321
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
322
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
323
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
CREATE UNIQUE INDEX users_email_unique ON users(email);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 4. Sage SDF (for Java runtime)
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
User ::= Sequence {
|
|
333
|
+
id Integer,
|
|
334
|
+
email PrintableString Range(0..255),
|
|
335
|
+
role Enumerated { admin(0), user(1), guest(2) } Default user,
|
|
336
|
+
createdAt PrintableString,
|
|
337
|
+
updatedAt PrintableString
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 5. UI Component Props
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// From: @widget Table
|
|
345
|
+
interface TableProps {
|
|
346
|
+
selectionMode?: 'none' | 'single' | 'multiple'
|
|
347
|
+
columns: Column[]
|
|
348
|
+
data: Row[]
|
|
349
|
+
onAction?: (event: ActionEvent) => void
|
|
350
|
+
onChange?: (event: ChangeEvent) => void
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 6. Form Components
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// From: @form UserForm
|
|
358
|
+
<UserForm
|
|
359
|
+
data={user}
|
|
360
|
+
onSubmit={handleSave}
|
|
361
|
+
layout="grid"
|
|
362
|
+
/>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Core Types
|
|
368
|
+
|
|
369
|
+
### Primitive Types
|
|
370
|
+
|
|
371
|
+
| Type | Description | SQL | TypeScript | Zod |
|
|
372
|
+
|------|-------------|-----|------------|-----|
|
|
373
|
+
| `string` | Text | TEXT/VARCHAR | `string` | `z.string()` |
|
|
374
|
+
| `text` | Long text | TEXT | `string` | `z.string()` |
|
|
375
|
+
| `integer` | Whole number | INTEGER | `number` | `z.number().int()` |
|
|
376
|
+
| `bigint` | Large integer | BIGINT | `bigint` | `z.bigint()` |
|
|
377
|
+
| `float` | Single precision | REAL | `number` | `z.number()` |
|
|
378
|
+
| `double` | Double precision | REAL | `number` | `z.number()` |
|
|
379
|
+
| `decimal` | Exact decimal | DECIMAL | `number` | `z.number()` |
|
|
380
|
+
| `boolean` | True/false | INTEGER | `boolean` | `z.boolean()` |
|
|
381
|
+
| `date` | Date only | TEXT | `Date` | `z.date()` |
|
|
382
|
+
| `time` | Time only | TEXT | `string` | `z.string()` |
|
|
383
|
+
| `datetime` | Date and time | TEXT | `Date` | `z.date()` |
|
|
384
|
+
| `timestamp` | Unix timestamp | INTEGER | `number` | `z.number()` |
|
|
385
|
+
| `json` | JSON data | TEXT | `Record<string, unknown>` | `z.record()` |
|
|
386
|
+
| `binary` | Binary data | BLOB | `Buffer` | `z.instanceof(Buffer)` |
|
|
387
|
+
| `uuid` | UUID v4 | TEXT | `string` | `z.string().uuid()` |
|
|
388
|
+
|
|
389
|
+
### Special Types
|
|
390
|
+
|
|
391
|
+
| Type | Description | Validation |
|
|
392
|
+
|------|-------------|------------|
|
|
393
|
+
| `email` | Email address | RFC 5322 format |
|
|
394
|
+
| `url` | URL | Valid URL format |
|
|
395
|
+
| `phone` | Phone number | E.164 or regional |
|
|
396
|
+
| `color` | Color value | Hex, RGB, named |
|
|
397
|
+
| `dimension` | Size with unit | `"100px"`, `"1ln"`, `"50%"` |
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Enums
|
|
402
|
+
|
|
403
|
+
### Definition
|
|
404
|
+
|
|
405
|
+
```coffeescript
|
|
406
|
+
# Simple enum (values are the names)
|
|
407
|
+
@enum Role: admin, user, guest
|
|
408
|
+
|
|
409
|
+
# Enum with explicit values
|
|
410
|
+
@enum Status
|
|
411
|
+
pending: 0
|
|
412
|
+
active: 1
|
|
413
|
+
suspended: 2
|
|
414
|
+
deleted: 3
|
|
415
|
+
|
|
416
|
+
# Enum with string values
|
|
417
|
+
@enum Priority
|
|
418
|
+
low: "low"
|
|
419
|
+
medium: "medium"
|
|
420
|
+
high: "high"
|
|
421
|
+
critical: "critical"
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Usage
|
|
425
|
+
|
|
426
|
+
```coffeescript
|
|
427
|
+
@model User
|
|
428
|
+
role! Role, [user] # Default to 'user'
|
|
429
|
+
status! Status, [active] # Default to 'active'
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Generation
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// TypeScript
|
|
436
|
+
enum Role { Admin = 'admin', User = 'user', Guest = 'guest' }
|
|
437
|
+
type Role = 'admin' | 'user' | 'guest'
|
|
438
|
+
|
|
439
|
+
// Zod
|
|
440
|
+
const RoleSchema = z.enum(['admin', 'user', 'guest'])
|
|
441
|
+
const RoleSchema = z.nativeEnum(Role)
|
|
442
|
+
|
|
443
|
+
// SQL
|
|
444
|
+
role TEXT CHECK(role IN ('admin', 'user', 'guest'))
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Models
|
|
450
|
+
|
|
451
|
+
Models represent database-backed entities with validation.
|
|
452
|
+
|
|
453
|
+
### Definition
|
|
454
|
+
|
|
455
|
+
```coffeescript
|
|
456
|
+
@model User
|
|
457
|
+
# Fields with types and constraints
|
|
458
|
+
name! string, [1, 100]
|
|
459
|
+
email!# email
|
|
460
|
+
password! string, [8, 100], { writeOnly: true }
|
|
461
|
+
role! Role, [user]
|
|
462
|
+
avatar? url
|
|
463
|
+
bio? text, [0, 1000]
|
|
464
|
+
preferences json, [{}]
|
|
465
|
+
active boolean, [true]
|
|
466
|
+
|
|
467
|
+
# Embedded type
|
|
468
|
+
address? Address
|
|
469
|
+
|
|
470
|
+
# Automatic timestamps
|
|
471
|
+
@timestamps
|
|
472
|
+
|
|
473
|
+
# Soft delete support
|
|
474
|
+
@softDelete
|
|
475
|
+
|
|
476
|
+
# Indexes
|
|
477
|
+
@index email# # Unique (from field)
|
|
478
|
+
@index [role, active] # Composite
|
|
479
|
+
@index name # Non-unique
|
|
480
|
+
|
|
481
|
+
# Computed fields (read-only)
|
|
482
|
+
@computed
|
|
483
|
+
displayName -> "#{@name} <#{@email}>"
|
|
484
|
+
isAdmin -> @role is 'admin'
|
|
485
|
+
|
|
486
|
+
# Validations beyond type constraints
|
|
487
|
+
@validate
|
|
488
|
+
password -> @matches /[A-Z]/ and @matches /[0-9]/
|
|
489
|
+
email -> not @endsWith '@test.com' if @env is 'production'
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Relationships
|
|
493
|
+
|
|
494
|
+
```coffeescript
|
|
495
|
+
@model Post
|
|
496
|
+
title! string, [1, 200]
|
|
497
|
+
content! text
|
|
498
|
+
published boolean, [false]
|
|
499
|
+
|
|
500
|
+
# Foreign key relationships
|
|
501
|
+
@belongs_to User # Creates user_id
|
|
502
|
+
@belongs_to Category, optional: true
|
|
503
|
+
|
|
504
|
+
@timestamps
|
|
505
|
+
@index [user_id, published]
|
|
506
|
+
|
|
507
|
+
@model Comment
|
|
508
|
+
content! text
|
|
509
|
+
approved boolean, [false]
|
|
510
|
+
|
|
511
|
+
@belongs_to Post
|
|
512
|
+
@belongs_to User
|
|
513
|
+
|
|
514
|
+
@timestamps
|
|
515
|
+
@softDelete
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Shorthand Aliases
|
|
519
|
+
|
|
520
|
+
`@one` and `@many` are shorthand aliases for `@has_one` and `@has_many`:
|
|
521
|
+
|
|
522
|
+
```coffeescript
|
|
523
|
+
@model User
|
|
524
|
+
@one Profile # Same as @has_one Profile
|
|
525
|
+
@many Post # Same as @has_many Post
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Links (Universal Temporal Associations)
|
|
529
|
+
|
|
530
|
+
`@link` declares a named, temporal, any-to-any relationship stored in a shared
|
|
531
|
+
`links` table:
|
|
532
|
+
|
|
533
|
+
```coffeescript
|
|
534
|
+
@model User
|
|
535
|
+
@link "admin", Organization # User can be admin of Organization
|
|
536
|
+
@link "mentor", User # Self-referential: User mentors User
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
The `links` table is auto-generated when any model uses `@link`:
|
|
540
|
+
|
|
541
|
+
```sql
|
|
542
|
+
CREATE TABLE links (
|
|
543
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
544
|
+
source_type VARCHAR NOT NULL,
|
|
545
|
+
source_id UUID NOT NULL,
|
|
546
|
+
target_type VARCHAR NOT NULL,
|
|
547
|
+
target_id UUID NOT NULL,
|
|
548
|
+
role VARCHAR NOT NULL,
|
|
549
|
+
when_from TIMESTAMP, -- null = beginning of time
|
|
550
|
+
when_till TIMESTAMP, -- null = end of time
|
|
551
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
ORM methods:
|
|
556
|
+
|
|
557
|
+
- `user.link("admin", org, { from, till })` — create a temporal link
|
|
558
|
+
- `user.unlink("admin", org)` — end the link (sets `when_till`)
|
|
559
|
+
- `user.links("admin")` — query active outgoing links
|
|
560
|
+
- `User.linked("admin", org)` — query active incoming links (who is admin of org?)
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Widgets
|
|
565
|
+
|
|
566
|
+
Widgets define UI components with props, events, and behavior.
|
|
567
|
+
|
|
568
|
+
### Definition
|
|
569
|
+
|
|
570
|
+
```coffeescript
|
|
571
|
+
@widget Table: $Viewer
|
|
572
|
+
# Behavior properties
|
|
573
|
+
selectionMode SelectionMode, [single]
|
|
574
|
+
columnSorting boolean, [true]
|
|
575
|
+
columnResizing boolean, [true]
|
|
576
|
+
columnReordering boolean, [false]
|
|
577
|
+
singleClickAction boolean, [false]
|
|
578
|
+
|
|
579
|
+
# Appearance properties
|
|
580
|
+
gridLineType GridLineType, [both]
|
|
581
|
+
gridLineColor? color
|
|
582
|
+
gridLineStyle LineStyle, [solid]
|
|
583
|
+
rowHeight dimension, ["1ln"]
|
|
584
|
+
headerHeight dimension, ["-1"]
|
|
585
|
+
boldColumnHeaders boolean, [false]
|
|
586
|
+
|
|
587
|
+
alternatingHighlight AlternatingType, [none]
|
|
588
|
+
alternatingColor? color
|
|
589
|
+
|
|
590
|
+
# Structure
|
|
591
|
+
columns ItemDescription[]
|
|
592
|
+
scrollPane ScrollPane?
|
|
593
|
+
popupMenu MenuItem[]?
|
|
594
|
+
|
|
595
|
+
# Events
|
|
596
|
+
@events onAction, onChange, onItemChanged
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Enum Dependencies
|
|
600
|
+
|
|
601
|
+
```coffeescript
|
|
602
|
+
@enum SelectionMode: none, single, multiple, block, invisible
|
|
603
|
+
@enum GridLineType: none, horizontal, vertical, both
|
|
604
|
+
@enum LineStyle: solid, dashed, dotted
|
|
605
|
+
@enum AlternatingType: none, row, column
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Inheritance
|
|
609
|
+
|
|
610
|
+
```coffeescript
|
|
611
|
+
@widget TreeTable: $Table
|
|
612
|
+
expandableColumn integer, [0]
|
|
613
|
+
expandAll boolean, [false]
|
|
614
|
+
showRootHandles boolean, [true]
|
|
615
|
+
showTreeLines boolean, [false]
|
|
616
|
+
treeLineColor? color
|
|
617
|
+
indentBy integer, [16]
|
|
618
|
+
|
|
619
|
+
@events onExpand, onCollapse
|
|
620
|
+
|
|
621
|
+
@widget PropertyTable: $TreeTable
|
|
622
|
+
usePane boolean, [true]
|
|
623
|
+
propertiesOrder PropertiesOrder, [categorized]
|
|
624
|
+
categorySortOrder SortOrder, [ascending]
|
|
625
|
+
|
|
626
|
+
@enum PropertiesOrder: unsorted, sorted, categorized
|
|
627
|
+
@enum SortOrder: descending, unsorted, ascending
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Forms
|
|
633
|
+
|
|
634
|
+
Forms combine model structure with UI layout.
|
|
635
|
+
|
|
636
|
+
### Definition
|
|
637
|
+
|
|
638
|
+
```coffeescript
|
|
639
|
+
@form UserForm: $User
|
|
640
|
+
# Layout configuration
|
|
641
|
+
layout forms
|
|
642
|
+
columns "d,4dlu,100px,4dlu,d,4dlu,d"
|
|
643
|
+
rows "d,2dlu,d,2dlu,d,4dlu,d"
|
|
644
|
+
|
|
645
|
+
# Field placement and customization
|
|
646
|
+
name { x: 0, y: 0, span: 7, label: "Full Name" }
|
|
647
|
+
email { x: 0, y: 2, span: 3 }
|
|
648
|
+
role { x: 4, y: 2, widget: dropdown }
|
|
649
|
+
active { x: 6, y: 2 }
|
|
650
|
+
bio { x: 0, y: 4, span: 7, widget: textarea, rows: 3 }
|
|
651
|
+
|
|
652
|
+
# Nested form section
|
|
653
|
+
address { x: 0, y: 6, span: 7, collapsible: true, title: "Address" }
|
|
654
|
+
|
|
655
|
+
# Actions
|
|
656
|
+
@actions
|
|
657
|
+
submit { value: "Save", icon: save, primary: true }
|
|
658
|
+
cancel { value: "Cancel", icon: cancel }
|
|
659
|
+
|
|
660
|
+
# Form-level events
|
|
661
|
+
@events onSubmit, onReset, onValidate
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Layout Types
|
|
665
|
+
|
|
666
|
+
```coffeescript
|
|
667
|
+
# Table layout (grid)
|
|
668
|
+
layout table
|
|
669
|
+
rows 3
|
|
670
|
+
columns 2
|
|
671
|
+
|
|
672
|
+
# Forms layout (JGoodies-style)
|
|
673
|
+
layout forms
|
|
674
|
+
columns "d,4dlu,100px:grow"
|
|
675
|
+
rows "d,2dlu,d,2dlu,d"
|
|
676
|
+
|
|
677
|
+
# Absolute layout
|
|
678
|
+
layout absolute
|
|
679
|
+
|
|
680
|
+
# Flow layout
|
|
681
|
+
layout flow
|
|
682
|
+
direction horizontal # or vertical
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Widget Overrides
|
|
686
|
+
|
|
687
|
+
```coffeescript
|
|
688
|
+
@form UserForm: $User
|
|
689
|
+
# Override widget type
|
|
690
|
+
role { widget: dropdown }
|
|
691
|
+
bio { widget: textarea, rows: 5 }
|
|
692
|
+
password { widget: password }
|
|
693
|
+
avatar { widget: imageUpload }
|
|
694
|
+
|
|
695
|
+
# Override validation message
|
|
696
|
+
email { errorMessage: "Please enter a valid email" }
|
|
697
|
+
|
|
698
|
+
# Conditional visibility
|
|
699
|
+
adminNotes { visible: -> @currentUser.isAdmin }
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## State Management
|
|
705
|
+
|
|
706
|
+
### Global State Definition
|
|
707
|
+
|
|
708
|
+
```coffeescript
|
|
709
|
+
@state App
|
|
710
|
+
# Current user (nullable)
|
|
711
|
+
currentUser? User
|
|
712
|
+
|
|
713
|
+
# Collections
|
|
714
|
+
users User[], [->]
|
|
715
|
+
posts Post[], [->]
|
|
716
|
+
|
|
717
|
+
# UI state
|
|
718
|
+
sidebarOpen boolean, [true]
|
|
719
|
+
theme Theme, [light]
|
|
720
|
+
locale string, ["en"]
|
|
721
|
+
|
|
722
|
+
# Computed state
|
|
723
|
+
@computed
|
|
724
|
+
isLoggedIn -> @currentUser?
|
|
725
|
+
isAdmin -> @currentUser?.role is 'admin'
|
|
726
|
+
userCount -> @users.length
|
|
727
|
+
publishedPosts -> @posts.filter (p) -> p.published
|
|
728
|
+
|
|
729
|
+
# Actions (mutations)
|
|
730
|
+
@actions
|
|
731
|
+
login: (credentials) ->
|
|
732
|
+
user = await api.login(credentials)
|
|
733
|
+
@currentUser = user
|
|
734
|
+
|
|
735
|
+
logout: ->
|
|
736
|
+
@currentUser = null
|
|
737
|
+
|
|
738
|
+
addPost: (post) ->
|
|
739
|
+
@posts.push(post)
|
|
740
|
+
|
|
741
|
+
@enum Theme: light, dark, system
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Path-Based Access
|
|
745
|
+
|
|
746
|
+
```coffeescript
|
|
747
|
+
# Global state (starts with /)
|
|
748
|
+
app.get '/currentUser/name'
|
|
749
|
+
app.set '/theme', 'dark'
|
|
750
|
+
app.inc '/stats/pageViews'
|
|
751
|
+
|
|
752
|
+
# Component local state (starts with .)
|
|
753
|
+
@get '.selectedIndex'
|
|
754
|
+
@set '.items[0]/checked', true
|
|
755
|
+
|
|
756
|
+
# Negative array indices
|
|
757
|
+
app.get '/posts[-1]' # Last post
|
|
758
|
+
app.set '/users[-1]/active', false
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Hydration
|
|
762
|
+
|
|
763
|
+
Components can hydrate their local state from global state:
|
|
764
|
+
|
|
765
|
+
```coffeescript
|
|
766
|
+
@component UserList
|
|
767
|
+
# Local state, hydrated from global
|
|
768
|
+
@state
|
|
769
|
+
users -> app.get '/users' # Reactive binding
|
|
770
|
+
filter string, [""] # Local only
|
|
771
|
+
sortBy string, ["name"] # Local only
|
|
772
|
+
|
|
773
|
+
# Computed from local state
|
|
774
|
+
@computed
|
|
775
|
+
filteredUsers ->
|
|
776
|
+
@users
|
|
777
|
+
.filter (u) => u.name.includes @filter
|
|
778
|
+
.sortBy @sortBy
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Serialization
|
|
782
|
+
|
|
783
|
+
Global state can be serialized for persistence:
|
|
784
|
+
|
|
785
|
+
```coffeescript
|
|
786
|
+
# Save state
|
|
787
|
+
localStorage.setItem 'appState', JSON.stringify app.get '/'
|
|
788
|
+
|
|
789
|
+
# Restore state
|
|
790
|
+
saved = JSON.parse localStorage.getItem 'appState'
|
|
791
|
+
app.set '/', saved
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Composition and Reuse
|
|
797
|
+
|
|
798
|
+
### Mixins
|
|
799
|
+
|
|
800
|
+
```coffeescript
|
|
801
|
+
# Define reusable field groups
|
|
802
|
+
@mixin Timestamps
|
|
803
|
+
createdAt! datetime
|
|
804
|
+
updatedAt! datetime
|
|
805
|
+
|
|
806
|
+
@mixin SoftDelete
|
|
807
|
+
deletedAt? datetime
|
|
808
|
+
|
|
809
|
+
@mixin Auditable
|
|
810
|
+
@include Timestamps
|
|
811
|
+
@include SoftDelete
|
|
812
|
+
createdBy? integer
|
|
813
|
+
updatedBy? integer
|
|
814
|
+
|
|
815
|
+
# Use in models
|
|
816
|
+
@model Post
|
|
817
|
+
title! string, [1, 200]
|
|
818
|
+
content! text
|
|
819
|
+
|
|
820
|
+
@include Auditable
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Type Composition
|
|
824
|
+
|
|
825
|
+
```coffeescript
|
|
826
|
+
# Base type
|
|
827
|
+
@type PersonName
|
|
828
|
+
first! string, [1, 50]
|
|
829
|
+
middle? string, [0, 50]
|
|
830
|
+
last! string, [1, 50]
|
|
831
|
+
|
|
832
|
+
@computed
|
|
833
|
+
full -> [@first, @middle, @last].filter(Boolean).join(' ')
|
|
834
|
+
|
|
835
|
+
# Extended type
|
|
836
|
+
@type ContactInfo
|
|
837
|
+
email! email
|
|
838
|
+
phone? phone
|
|
839
|
+
|
|
840
|
+
# Composed type
|
|
841
|
+
@type Person
|
|
842
|
+
name! PersonName
|
|
843
|
+
contact! ContactInfo
|
|
844
|
+
|
|
845
|
+
@model Employee: $Person
|
|
846
|
+
employeeId!# string, [5, 10]
|
|
847
|
+
department! Department
|
|
848
|
+
hireDate! date
|
|
849
|
+
|
|
850
|
+
@timestamps
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Template Definitions (Sage-style)
|
|
854
|
+
|
|
855
|
+
```coffeescript
|
|
856
|
+
# Define a named template
|
|
857
|
+
@template prototypeTable: Table
|
|
858
|
+
alternatingHighlight row
|
|
859
|
+
alternatingColor "rowHilite"
|
|
860
|
+
borders shadow, { thickness: 7 }
|
|
861
|
+
gridLineType both
|
|
862
|
+
selectionMode single
|
|
863
|
+
boldColumnHeaders true
|
|
864
|
+
|
|
865
|
+
# Use the template
|
|
866
|
+
@widget VitalsTable: $prototypeTable
|
|
867
|
+
columns
|
|
868
|
+
{ title: "Type", width: "6ch" }
|
|
869
|
+
{ title: "Result", width: "6ch" }
|
|
870
|
+
{ title: "Date", valueType: datetime }
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Future Work
|
|
876
|
+
|
|
877
|
+
The data layer (parser, code generators, runtime validation, ORM) is
|
|
878
|
+
complete. The schema language is designed to extend into UI and application
|
|
879
|
+
state — applying the same "define once, generate everything" principle to
|
|
880
|
+
presentation layers.
|
|
881
|
+
|
|
882
|
+
### Widget System
|
|
883
|
+
|
|
884
|
+
Widget definition parser, prop type generation, event handler types,
|
|
885
|
+
inheritance resolution, and component generation (Vue/React). The syntax
|
|
886
|
+
and design are documented above in [Widgets](#widgets).
|
|
887
|
+
|
|
888
|
+
### Form System
|
|
889
|
+
|
|
890
|
+
Form definition parser, layout engine (grid, flow, JGoodies-style),
|
|
891
|
+
field placement, widget overrides, and validation integration. See
|
|
892
|
+
[Forms](#forms) above.
|
|
893
|
+
|
|
894
|
+
### State Management
|
|
895
|
+
|
|
896
|
+
State definition parser, path-based access, computed properties,
|
|
897
|
+
actions/mutations, hydration, and serialization. See
|
|
898
|
+
[State Management](#state-management) above.
|
|
899
|
+
|
|
900
|
+
### Sage Compatibility
|
|
901
|
+
|
|
902
|
+
SDF output generation, SPOT format export, and widget library mapping
|
|
903
|
+
for interoperability with existing Sage systems.
|
|
904
|
+
|
|
905
|
+
### Other Extensions
|
|
906
|
+
|
|
907
|
+
- **`@computed` / `@validate` in the DSL** — These work in model code
|
|
908
|
+
today. Moving them into `.schema` files would let code generators emit
|
|
909
|
+
them across all targets.
|
|
910
|
+
- **Migration diffing** — Compare two schema ASTs and emit `ALTER TABLE`
|
|
911
|
+
SQL. Use external tools (dbmate, Flyway) in the meantime.
|
|
912
|
+
- **Additional SQL dialects** — PostgreSQL, MySQL, SQLite. The emitter
|
|
913
|
+
is a simple AST walker; adding dialects is straightforward.
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
## Appendix: Full Example
|
|
918
|
+
|
|
919
|
+
```coffeescript
|
|
920
|
+
# ============================================
|
|
921
|
+
# Medical Application Schema
|
|
922
|
+
# ============================================
|
|
923
|
+
|
|
924
|
+
@import "./common.schema"
|
|
925
|
+
|
|
926
|
+
# Enums
|
|
927
|
+
@enum Gender: male, female, other, unknown
|
|
928
|
+
@enum VitalType: temp, pulse, resp, bp, height, weight, pain
|
|
929
|
+
@enum AllergyStatus: active, inactive, resolved
|
|
930
|
+
|
|
931
|
+
# Types
|
|
932
|
+
@type Address
|
|
933
|
+
street! string, [1, 200]
|
|
934
|
+
city! string, [1, 100]
|
|
935
|
+
state! string, [2, 2]
|
|
936
|
+
zip! string, /^\d{5}(-\d{4})?$/
|
|
937
|
+
|
|
938
|
+
@type ContactInfo
|
|
939
|
+
phone? phone
|
|
940
|
+
email? email
|
|
941
|
+
fax? phone
|
|
942
|
+
|
|
943
|
+
# Models
|
|
944
|
+
@model Patient
|
|
945
|
+
name! string, [1, 100]
|
|
946
|
+
mrn!# string, [1, 20] # Medical Record Number
|
|
947
|
+
ssn?# string, [9, 11] # SSN (encrypted)
|
|
948
|
+
dob! date
|
|
949
|
+
gender! Gender
|
|
950
|
+
photo? url
|
|
951
|
+
address? Address
|
|
952
|
+
contact? ContactInfo
|
|
953
|
+
preferences json, [{}]
|
|
954
|
+
active boolean, [true]
|
|
955
|
+
|
|
956
|
+
@timestamps
|
|
957
|
+
@softDelete
|
|
958
|
+
|
|
959
|
+
@computed
|
|
960
|
+
age -> yearsFrom @dob
|
|
961
|
+
displayName -> "#{@name} (#{@mrn})"
|
|
962
|
+
|
|
963
|
+
@model Vital
|
|
964
|
+
type! VitalType
|
|
965
|
+
value! string, [1, 50]
|
|
966
|
+
unit? string, [1, 20]
|
|
967
|
+
takenAt! datetime
|
|
968
|
+
notes? text
|
|
969
|
+
|
|
970
|
+
@belongs_to Patient
|
|
971
|
+
@timestamps
|
|
972
|
+
|
|
973
|
+
@model Allergy
|
|
974
|
+
allergen! string, [1, 200]
|
|
975
|
+
reaction? string, [0, 500]
|
|
976
|
+
severity? integer, [1, 5]
|
|
977
|
+
status! AllergyStatus, [active]
|
|
978
|
+
onsetDate? date
|
|
979
|
+
|
|
980
|
+
@belongs_to Patient
|
|
981
|
+
@timestamps
|
|
982
|
+
@softDelete
|
|
983
|
+
|
|
984
|
+
# Widgets
|
|
985
|
+
@widget VitalsTable: $prototypeTable
|
|
986
|
+
title "Vitals"
|
|
987
|
+
columns
|
|
988
|
+
{ name: "type", title: "Type", width: "6ch", fgColor: "#1C0B5A" }
|
|
989
|
+
{ name: "value", title: "Result", width: "8ch" }
|
|
990
|
+
{ name: "takenAt", title: "Date/Time", valueType: datetime, format: "MMM dd, yyyy HH:mm" }
|
|
991
|
+
|
|
992
|
+
@events onAction
|
|
993
|
+
|
|
994
|
+
@widget AllergiesList: $ListBox
|
|
995
|
+
title "Allergies"
|
|
996
|
+
fgColor "#800000"
|
|
997
|
+
selectionMode none
|
|
998
|
+
itemDescription
|
|
999
|
+
icon "warning"
|
|
1000
|
+
|
|
1001
|
+
# Forms
|
|
1002
|
+
@form PatientForm: $Patient
|
|
1003
|
+
layout forms
|
|
1004
|
+
columns "d,4dlu,100px,4dlu,d,4dlu,d,4dlu,100px"
|
|
1005
|
+
rows "d,2dlu,d,2dlu,d,4dlu,d,2dlu,d"
|
|
1006
|
+
|
|
1007
|
+
name { x: 0, y: 0, span: 5 }
|
|
1008
|
+
mrn { x: 6, y: 0, span: 3 }
|
|
1009
|
+
dob { x: 0, y: 2, widget: datePicker }
|
|
1010
|
+
gender { x: 2, y: 2, widget: dropdown }
|
|
1011
|
+
ssn { x: 4, y: 2, widget: masked, mask: "###-##-####" }
|
|
1012
|
+
photo { x: 8, y: 0, rowSpan: 3, widget: imageUpload }
|
|
1013
|
+
|
|
1014
|
+
address { x: 0, y: 4, span: 9, collapsible: true }
|
|
1015
|
+
contact { x: 0, y: 6, span: 9, collapsible: true }
|
|
1016
|
+
|
|
1017
|
+
@actions
|
|
1018
|
+
save { value: "Save Patient", primary: true }
|
|
1019
|
+
cancel { value: "Cancel" }
|
|
1020
|
+
|
|
1021
|
+
# State
|
|
1022
|
+
@state MedicalApp
|
|
1023
|
+
currentPatient? Patient
|
|
1024
|
+
patients Patient[], [->]
|
|
1025
|
+
|
|
1026
|
+
# UI state
|
|
1027
|
+
infobarCollapsed boolean, [false]
|
|
1028
|
+
activeTab string, ["summary"]
|
|
1029
|
+
|
|
1030
|
+
@computed
|
|
1031
|
+
patientName -> @currentPatient?.displayName
|
|
1032
|
+
patientAge -> @currentPatient?.age
|
|
1033
|
+
hasPatient -> @currentPatient?
|
|
1034
|
+
|
|
1035
|
+
@actions
|
|
1036
|
+
selectPatient: (patient) ->
|
|
1037
|
+
@currentPatient = patient
|
|
1038
|
+
|
|
1039
|
+
clearPatient: ->
|
|
1040
|
+
@currentPatient = null
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## Appendix: Syntax Exploration
|
|
1046
|
+
|
|
1047
|
+
Exploring syntax options for Rip Schema, ranging from the original SPOT/ASN.1 style to compact sigil-based alternatives. SPOT (part of the Sage framework) uses an ASN.1-inspired syntax that proved itself at enterprise scale for complex applications like hospital systems.
|
|
1048
|
+
|
|
1049
|
+
### Original SPOT Syntax
|
|
1050
|
+
|
|
1051
|
+
From `sage.spot` — the proven baseline:
|
|
1052
|
+
|
|
1053
|
+
```
|
|
1054
|
+
Application ::= Sequence {
|
|
1055
|
+
name PrintableString Range(0..32) Optional,
|
|
1056
|
+
lookAndFeel PrintableString Range(0..255) Optional [ style ],
|
|
1057
|
+
deferredLoadingMode Enumerated {
|
|
1058
|
+
auto (0),
|
|
1059
|
+
always (1),
|
|
1060
|
+
never (2)
|
|
1061
|
+
} Default auto,
|
|
1062
|
+
mainWindow MainWindow
|
|
1063
|
+
} [ onAuthFailure, onChange ]
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
**Strengths:** Self-documenting, no ambiguity, proven at scale (2000+ line schemas), language-neutral.
|
|
1067
|
+
**Weaknesses:** Verbose keywords, lots of punctuation.
|
|
1068
|
+
|
|
1069
|
+
### Option C: Sigil Modifiers (chosen direction)
|
|
1070
|
+
|
|
1071
|
+
Replace keywords with sigils: `?` optional, `!` required, `#` unique, `= x` default.
|
|
1072
|
+
|
|
1073
|
+
```
|
|
1074
|
+
Application ::= Sequence [onAuthFailure, onChange]
|
|
1075
|
+
name? string(32)
|
|
1076
|
+
deferredLoadingMode = auto : auto(0) | always(1) | never(2)
|
|
1077
|
+
mainWindow MainWindow
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Option E: Rip-Native (@model style)
|
|
1081
|
+
|
|
1082
|
+
Schemas as Rip code, using `@model`, `@enum` directives — this is the current implementation:
|
|
1083
|
+
|
|
1084
|
+
```coffee
|
|
1085
|
+
@model Application
|
|
1086
|
+
@events onAuthFailure, onChange
|
|
1087
|
+
name? string, [0, 32]
|
|
1088
|
+
deferredLoadingMode DeferredLoadingMode = auto
|
|
1089
|
+
mainWindow MainWindow
|
|
1090
|
+
|
|
1091
|
+
@enum DeferredLoadingMode
|
|
1092
|
+
auto (0)
|
|
1093
|
+
always (1)
|
|
1094
|
+
never (2)
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
### The Fundamental Tradeoff
|
|
1098
|
+
|
|
1099
|
+
**Standalone Format** — Language-neutral, schema is data not code, clear separation, proven at scale.
|
|
1100
|
+
|
|
1101
|
+
**Computable Rip Code** — Schemas are first-class values, can extend/compose/compute at runtime, reactive schemas possible, tighter IDE integration. But: tied to Rip.
|
|
1102
|
+
|
|
1103
|
+
**Path Forward: Both?** — `.schema` files with clean SPOT-like syntax, Rip parser produces S-expressions, S-expressions can be interpreted by runtime, imported into Rip, or used to generate code for other languages.
|
|
1104
|
+
|
|
1105
|
+
---
|
|
1106
|
+
|
|
1107
|
+
## References
|
|
1108
|
+
|
|
1109
|
+
- [Sage Documentation](../demos/sage-docs/) — Original SPOT/Sparse Notation
|
|
1110
|
+
- [rip-schema](https://github.com/rip-lang/rip-packages/tree/main/packages/schema) — Current schema DSL
|
|
1111
|
+
- [Zod](https://zod.dev) — TypeScript-first schema validation
|
|
1112
|
+
- [Vue State](../demos/vue-state/) — Path-based state management
|
|
1113
|
+
- [ASN.1](https://en.wikipedia.org/wiki/ASN.1) — Abstract Syntax Notation (SPOT's ancestor)
|