rip-lang 3.13.137 → 3.14.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 +46 -4
- package/docs/RIP-LANG.md +116 -11
- package/docs/RIP-SCHEMA.md +2390 -0
- package/docs/RIP-TYPES.md +21 -14
- package/docs/assets/rip-schema-logo-960w.png +0 -0
- package/docs/assets/rip-schema-social.png +0 -0
- package/docs/dist/rip.js +6839 -3670
- package/docs/dist/rip.min.js +1475 -211
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +10 -3
- package/src/AGENTS.md +130 -0
- package/src/compiler.js +65 -2
- package/src/components.js +19 -5
- package/src/grammar/grammar.rip +20 -1
- package/src/lexer.js +42 -0
- package/src/parser.js +222 -220
- package/src/schema.js +3327 -0
- package/src/typecheck.js +914 -59
- package/src/types.js +25 -0
- package/src/ui.rip +203 -45
- package/CHANGELOG.md +0 -1500
- package/src/sourcemap-utils.js +0 -366
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="
|
|
12
|
+
<a href="https://github.com/shreeve/rip-lang/commits/main"><img src="https://img.shields.io/badge/version-3.14.1-blue.svg" alt="Version"></a>
|
|
13
13
|
<a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
|
|
14
14
|
<a href="#"><img src="https://img.shields.io/badge/tests-1%2C436%2F1%2C436-brightgreen.svg" alt="Tests"></a>
|
|
15
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
@@ -239,6 +239,7 @@ All use `globalThis` with `??=` — override any by redeclaring locally.
|
|
|
239
239
|
| `*>` (merge assign) | `*>obj = {a: 1}` | `Object.assign(obj, {a: 1})` |
|
|
240
240
|
| `or return` | `x = get() or return err` | Guard clause (Ruby-style) |
|
|
241
241
|
| `?? throw` | `x = get() ?? throw err` | Nullish guard |
|
|
242
|
+
| `:name` (symbol) | `:redo`, `:active` | Ruby-style interned symbol (`Symbol.for`) |
|
|
242
243
|
|
|
243
244
|
### Heredoc & Heregex
|
|
244
245
|
|
|
@@ -283,6 +284,47 @@ pattern = ///
|
|
|
283
284
|
|
|
284
285
|
---
|
|
285
286
|
|
|
287
|
+
## Schema
|
|
288
|
+
|
|
289
|
+
**Rip Schema** is a first-class language construct for declaring data inline. One keyword — `schema` — covers what would otherwise take three libraries: a validator (Zod-style), an ORM (Prisma/ActiveRecord-style), and a migration tool. Schemas live in `.rip` source, compile alongside the rest of your code, and are real runtime values you can export, pass around, and derive from. Unlike Rip's compile-time `type` / `interface` system (which is erased from JS output), schemas exist at runtime because they validate, construct class instances, run ORM queries, and emit SQL — all from a single declaration that your editor also type-checks via automatic shadow TypeScript.
|
|
290
|
+
|
|
291
|
+
A schema has one of **five kinds**, selected by a `:symbol` after the keyword. `:input` (the default) is a field validator. `:shape` adds methods and computed getters — validators with behavior, like a `Money` or `Address` value. `:enum` declares a closed set of members using `:symbol` literals (`:draft`, `:active 1`) and exposes `.parse()` that accepts either the member name or its value. `:mixin` declares a reusable field group — non-instantiable, consumed by other schemas via `@mixin Name` with diamond-dedup and cycle detection. `:model` is the big one: DB-backed, with a full async ORM (`find`, `where`, `create`, `save`, `destroy`), migration-grade DDL emission (`toSQL`), Rails-ordered lifecycle hooks (ten recognized names from `beforeValidation` through `afterDestroy`), and `@belongs_to` / `@has_many` / `@has_one` relations that resolve lazily through a process-global registry.
|
|
292
|
+
|
|
293
|
+
```coffee
|
|
294
|
+
# Validator
|
|
295
|
+
SignupInput = schema
|
|
296
|
+
email! email
|
|
297
|
+
password! string, 8..100
|
|
298
|
+
|
|
299
|
+
# Shape with behavior
|
|
300
|
+
Address = schema :shape
|
|
301
|
+
street! string
|
|
302
|
+
city! string
|
|
303
|
+
full: ~> "#{@street}, #{@city}"
|
|
304
|
+
|
|
305
|
+
# Enumeration
|
|
306
|
+
Status = schema
|
|
307
|
+
:pending 0
|
|
308
|
+
:active 1
|
|
309
|
+
:done 2
|
|
310
|
+
|
|
311
|
+
# DB-backed model
|
|
312
|
+
User = schema :model
|
|
313
|
+
name! string
|
|
314
|
+
email!# email
|
|
315
|
+
@timestamps
|
|
316
|
+
@has_many Order
|
|
317
|
+
beforeValidation: -> @email = @email.toLowerCase()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The **body syntax is declarative**, not general Rip code. Five line forms are legal: fields (`name! type, min..max`, with inline transforms via `name! type, -> fn(it)` where `it` is the whole raw input), directives (`@timestamps`, `@mixin Name`, `@belongs_to User?`), methods (`name: -> body`), computed getters (`name: ~> body`), and eager-derived fields (`name: !> body` — computed once at parse/hydrate, stored as an own property, distinct from `~>` which re-evaluates on every access). Modifiers `!`, `#`, `?` mark required, unique, and optional; the type slot is optional and defaults to `string`. Constraints are self-identifying by shape: `min..max` for ranges, `[value]` for defaults, `/regex/` for patterns, `{key: val}` for attrs, and the terminal `-> body` for transforms. Literal-union types (`"M" | "F" | "U"`) in the type slot cover enum-style value sets. Cross-field invariants — "passwords must match", "end after start", "id OR full-object" — attach as `@ensure "message", (u) -> predicate` (or an array of such pairs), run after field validation, and collect all failures in declaration order. Every instantiable schema exposes the same three-method runtime API: `.parse(data)` returns a cleaned value or throws `SchemaError` with structured `.issues`; `.safe(data)` returns `{ok, value, errors}` without throwing; `.ok(data)` is a boolean fast path that allocates no error arrays. All three have async dammit variants — `User.find! 1`, `user.save!` — that are the idiomatic form in Rip source.
|
|
321
|
+
|
|
322
|
+
**`:model` is where the pieces converge.** One declaration gives you a validator, a class with fields as enumerable own properties and methods/getters on the prototype, a chainable async query builder (`User.where(active: true).order("last_name").all!`), migration DDL that works standalone (`User.toSQL()` never touches the database), belongs-to/has-many accessors that resolve cross-module through the registry, and full shadow TypeScript with `ModelSchema<Instance, Data>` typing that propagates through schema algebra. Hydrated instances carry both snake_case and camelCase aliases on DB-derived columns (`order.user_id` and `order.userId` read the same slot), so raw SQL helpers and ORM access coexist cleanly. A single-function adapter interface (`adapter.query(sql, params)`) routes all database I/O, so tests use in-memory mocks and production uses rip-db without the ORM caring.
|
|
323
|
+
|
|
324
|
+
**Schema algebra** — `.pick`, `.omit`, `.partial`, `.required`, `.extend` — always returns a new `:shape`. Field semantics (type, literal unions, constraints, inline transforms) carry through to the derived shape; instance behavior (methods, computed `~>`, eager-derived `!>`, hooks, and `@ensure` refinements) does not. `User.omit "password"` produces a validator for `User` minus the password field; it won't have `.find()` or the `beforeSave` hook, but field-level transforms (`email, -> it.email.toLowerCase()`) continue to fire on the derived shape exactly as they did on the original. This invariant is enforced both at runtime (ORM methods throw on derived shapes with a targeted diagnostic pointing at query projection) and at the TypeScript level (algebra generics are parameterized over `Data`, not `Instance`, so the derived types correctly omit methods and ORM surface). Internally, the whole feature is a compiler sidecar — 54% of the implementation lives in `src/schema.js` and touches the core compiler in under 100 lines of wiring. A four-layer lazy runtime (raw descriptor → normalized metadata → validator plan → ORM plan / DDL plan) means module load is cheap, migration scripts never build the ORM plan, and validator-only consumers never build the class machinery. The full reference is in [docs/RIP-SCHEMA.md](docs/RIP-SCHEMA.md).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
286
328
|
## vs React / Vue / Solid
|
|
287
329
|
|
|
288
330
|
| Concept | React | Vue | Solid | Rip |
|
|
@@ -434,7 +476,7 @@ Rip includes optional packages for full-stack development:
|
|
|
434
476
|
| [@rip-lang/ui](packages/ui/) | — | Unified UI system — browser widgets, email components, shared helpers, Tailwind integration |
|
|
435
477
|
| [@rip-lang/swarm](packages/swarm/) | 1.2.18 | Parallel job runner with worker pool |
|
|
436
478
|
| [@rip-lang/csv](packages/csv/) | 1.3.6 | CSV parser + writer |
|
|
437
|
-
| [@rip-lang/
|
|
479
|
+
| [@rip-lang/time](packages/time/) | 1.0.0 | Immutable date/time with IANA timezones + `Duration` (US-English, zero runtime deps) |
|
|
438
480
|
| [VS Code Extension](packages/vscode/) | 0.5.7 | Syntax highlighting, type intelligence, source maps |
|
|
439
481
|
|
|
440
482
|
```bash
|
|
@@ -504,8 +546,8 @@ bun run bump major
|
|
|
504
546
|
|-------|-------------|
|
|
505
547
|
| [docs/RIP-LANG.md](docs/RIP-LANG.md) | Full language reference (syntax, operators, reactivity, types, components) |
|
|
506
548
|
| [docs/RIP-TYPES.md](docs/RIP-TYPES.md) | Type system specification |
|
|
507
|
-
| [
|
|
508
|
-
| [AGENTS.md](AGENTS.md) | AI agents —
|
|
549
|
+
| [docs/RIP-SCHEMA.md](docs/RIP-SCHEMA.md) | Schema keyword — validators, models, ORM, DDL, algebra |
|
|
550
|
+
| [AGENTS.md](AGENTS.md) | AI agents — compiler architecture, subsystems, conventions |
|
|
509
551
|
|
|
510
552
|
---
|
|
511
553
|
|
package/docs/RIP-LANG.md
CHANGED
|
@@ -116,6 +116,12 @@ table = *{
|
|
|
116
116
|
null: "nothing"
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
# Symbols — Ruby-style interned symbols via Symbol.for()
|
|
120
|
+
status = :active # Symbol.for("active")
|
|
121
|
+
state = :ready # Symbol.for("ready")
|
|
122
|
+
:redo is :redo # true (globally interned)
|
|
123
|
+
typeof :hello # "symbol"
|
|
124
|
+
|
|
119
125
|
# Ranges
|
|
120
126
|
nums = [1..5] # [1, 2, 3, 4, 5]
|
|
121
127
|
exclusive = [1...5] # [1, 2, 3, 4]
|
|
@@ -337,6 +343,7 @@ Multiple lines
|
|
|
337
343
|
| `not of` | Not of | `k not of obj` | Negated key existence |
|
|
338
344
|
| `<=>` | Two-way bind | `value <=> name` | Bidirectional reactive binding (render blocks) |
|
|
339
345
|
| `*{ }` | Map literal | `*{/pat/: val}` | `new Map([[/pat/, val]])` |
|
|
346
|
+
| `:name` | Symbol literal | `:redo` | `Symbol.for("redo")` — Ruby-style interned symbol |
|
|
340
347
|
|
|
341
348
|
## Assignment Operators
|
|
342
349
|
|
|
@@ -1295,7 +1302,6 @@ Rip includes optional packages for full-stack development. All are written in Ri
|
|
|
1295
1302
|
```bash
|
|
1296
1303
|
bun add @rip-lang/server # Web framework + production server
|
|
1297
1304
|
bun add @rip-lang/db # DuckDB server + client
|
|
1298
|
-
bun add @rip-lang/schema # ORM + validation
|
|
1299
1305
|
bun add @rip-lang/swarm # Parallel job runner
|
|
1300
1306
|
bun add @rip-lang/csv # CSV parser + writer
|
|
1301
1307
|
# Widgets are included in packages/ui/ (not a separate npm package)
|
|
@@ -1691,22 +1697,65 @@ rows = CSV.read "name,age\nAlice,30\nBob,25\n", headers: true
|
|
|
1691
1697
|
CSV.save! 'output.csv', rows
|
|
1692
1698
|
```
|
|
1693
1699
|
|
|
1694
|
-
##
|
|
1700
|
+
## schema — Inline validators, models, and DDL
|
|
1701
|
+
|
|
1702
|
+
The `schema` keyword declares a runtime data description inline — a
|
|
1703
|
+
validator, a domain shape, an enumeration, a reusable field group, or a
|
|
1704
|
+
full DB-backed model. One keyword covers input validation, class
|
|
1705
|
+
generation, persistence, migration DDL, and shadow TypeScript.
|
|
1695
1706
|
|
|
1696
1707
|
```coffee
|
|
1697
|
-
|
|
1708
|
+
# Validator
|
|
1709
|
+
SignupInput = schema
|
|
1710
|
+
email! email
|
|
1711
|
+
password! 8..100
|
|
1712
|
+
password2! 8..100
|
|
1713
|
+
|
|
1714
|
+
@ensure "passwords must match", (u) -> u.password is u.password2
|
|
1715
|
+
|
|
1716
|
+
# Shape with behavior
|
|
1717
|
+
Address = schema :shape
|
|
1718
|
+
street! string
|
|
1719
|
+
city! string
|
|
1720
|
+
full: ~> "#{@street}, #{@city}"
|
|
1721
|
+
|
|
1722
|
+
# Enumeration
|
|
1723
|
+
Status = schema
|
|
1724
|
+
:pending 0
|
|
1725
|
+
:active 1
|
|
1726
|
+
:done 2
|
|
1727
|
+
|
|
1728
|
+
# DB-backed model with ORM and migrations
|
|
1729
|
+
User = schema :model
|
|
1730
|
+
name! string
|
|
1731
|
+
email!# email
|
|
1732
|
+
@timestamps
|
|
1733
|
+
@has_many Order
|
|
1734
|
+
beforeValidation: -> @email = @email.toLowerCase()
|
|
1698
1735
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
email: { type: 'email', unique: true }
|
|
1736
|
+
# Usage
|
|
1737
|
+
input = SignupInput.parse rawJson # throws SchemaError on invalid
|
|
1738
|
+
result = SignupInput.safe rawJson # {ok, value, errors}
|
|
1739
|
+
valid = SignupInput.ok rawJson # boolean
|
|
1704
1740
|
|
|
1705
|
-
user
|
|
1706
|
-
user.
|
|
1707
|
-
|
|
1741
|
+
user = User.create! name: "Alice", email: "a@b.c"
|
|
1742
|
+
orders = user.orders! # Promise<OrderInstance[]>
|
|
1743
|
+
sql = User.toSQL()
|
|
1744
|
+
|
|
1745
|
+
UserPublic = User.omit "password" # algebra returns :shape
|
|
1708
1746
|
```
|
|
1709
1747
|
|
|
1748
|
+
Body forms: fields (`name! type`, with optional range/regex/default/attrs
|
|
1749
|
+
and terminal `-> transform`), methods (`name: -> body`), computed getters
|
|
1750
|
+
(`name: ~> body`), eager-derived fields (`name: !> body`), directives
|
|
1751
|
+
(`@mixin`, `@timestamps`, `@has_many`, `@belongs_to`, `@index`,
|
|
1752
|
+
`@softDelete`), and `@ensure "msg", (x) -> predicate` cross-field
|
|
1753
|
+
refinements. Inline and array forms are both accepted for `@ensure`.
|
|
1754
|
+
|
|
1755
|
+
**See [docs/RIP-SCHEMA.md](./RIP-SCHEMA.md) for the comprehensive guide** —
|
|
1756
|
+
all five kinds, every body form, the ORM and DDL contract, hooks, mixins,
|
|
1757
|
+
algebra, shadow TypeScript, and the architecture.
|
|
1758
|
+
|
|
1710
1759
|
## Full-Stack Example
|
|
1711
1760
|
|
|
1712
1761
|
A complete API server in Rip:
|
|
@@ -1985,6 +2034,58 @@ class EventEmitter
|
|
|
1985
2034
|
@
|
|
1986
2035
|
```
|
|
1987
2036
|
|
|
2037
|
+
## Conversion Method Naming
|
|
2038
|
+
|
|
2039
|
+
When a type exposes methods that convert between representations, Rip
|
|
2040
|
+
follows a convention borrowed from Rust, Swift, and .NET for naming
|
|
2041
|
+
them. The prefix signals what kind of thing you get back:
|
|
2042
|
+
|
|
2043
|
+
| Prefix | Meaning | What you get |
|
|
2044
|
+
| --------- | ----------------------------------------- | ----------------------------------------------- |
|
|
2045
|
+
| `toX()` | Convert — produce a new independent value | New object; mutating it doesn't affect `self` |
|
|
2046
|
+
| `asX()` | View / cast — reinterpret `self` as an `X` | Wrapper or lens; may share state with `self` |
|
|
2047
|
+
| `fromX()` | Static constructor from an `X` | New instance built from external data |
|
|
2048
|
+
| `parseX` | Parse an `X`-formatted representation | Validated, typed value |
|
|
2049
|
+
|
|
2050
|
+
The test when naming your own method: **if mutating the returned value
|
|
2051
|
+
can affect the original, it's `as`; otherwise it's `to`.** Equivalently:
|
|
2052
|
+
did real work happen (allocation, copy, serialization)? Then `to`.
|
|
2053
|
+
Zero-cost reinterpretation? Then `as`.
|
|
2054
|
+
|
|
2055
|
+
```coffee
|
|
2056
|
+
# toX — converts, produces an independent value
|
|
2057
|
+
user.toJSON() # plain object; mutating it doesn't change user
|
|
2058
|
+
user.toPublic() # filtered plain object for wire responses
|
|
2059
|
+
User.toSQL() # CREATE TABLE DDL string
|
|
2060
|
+
events.toArray() # materialized Array from an iterator / Set
|
|
2061
|
+
|
|
2062
|
+
# asX — reinterprets, may share state
|
|
2063
|
+
# No Rip idioms today; reserved for zero-cost views. A future
|
|
2064
|
+
# buffer.asUint8Array() would wrap the same memory, not copy it.
|
|
2065
|
+
|
|
2066
|
+
# fromX — static construction from another type
|
|
2067
|
+
Array.from(iter)
|
|
2068
|
+
String.fromCharCode(65)
|
|
2069
|
+
|
|
2070
|
+
# parseX — typed parse from a wire / string format
|
|
2071
|
+
Schema.parse(data) # validates, returns a typed instance
|
|
2072
|
+
parseInt("42")
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
`parse` and `to` are duals: `Schema.parse(data)` takes a wire
|
|
2076
|
+
representation in, `instance.toJSON()` produces one out. Picking the
|
|
2077
|
+
right prefix ahead of time keeps the pair discoverable without
|
|
2078
|
+
remembering which method name you chose last time.
|
|
2079
|
+
|
|
2080
|
+
Related prefixes worth recognizing from other ecosystems, even though
|
|
2081
|
+
Rip doesn't have idiomatic uses today:
|
|
2082
|
+
|
|
2083
|
+
- `intoX` — consume `self`, return `X` (Rust's ownership transfer).
|
|
2084
|
+
JS garbage collection makes ownership invisible, so Rip just uses
|
|
2085
|
+
`toX` for the same cases.
|
|
2086
|
+
- `withX` — immutable update returning a modified copy of `self` with
|
|
2087
|
+
one field changed. Useful for record types with many optional fields.
|
|
2088
|
+
|
|
1988
2089
|
---
|
|
1989
2090
|
|
|
1990
2091
|
# 16. Quick Reference
|
|
@@ -2042,6 +2143,10 @@ a ?? b # nullish coalescing
|
|
|
2042
2143
|
# Word arrays
|
|
2043
2144
|
%w[foo bar baz] # ["foo", "bar", "baz"] — Ruby-style word literal
|
|
2044
2145
|
|
|
2146
|
+
# Symbol literals — Ruby-style interned symbols
|
|
2147
|
+
:redo # Symbol.for("redo")
|
|
2148
|
+
:active # Symbol.for("active")
|
|
2149
|
+
|
|
2045
2150
|
# Map literals — real Map with any key type
|
|
2046
2151
|
*{ /regex/: val, "key": val, 42: val }
|
|
2047
2152
|
|