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 CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.13.137-blue.svg" alt="Version"></a>
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/schema](packages/schema/) | 0.3.8 | Unified schema TypeScript types, SQL DDL, validation, ORM |
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
- | [AGENTS.md](AGENTS.md) | Compiler architecture, S-expressions, component system internals |
508
- | [AGENTS.md](AGENTS.md) | AI agents — get up to speed for working on the compiler |
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
- ## @rip-lang/schema — ORM + Validation
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
- import { Model } from '@rip-lang/schema'
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
- class User extends Model
1700
- @table = 'users'
1701
- @schema
1702
- name: { type: 'string', required: true }
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 = User.find!(25)
1706
- user.name = 'Alice'
1707
- user.save!()
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