@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/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)