Skip to content

Commit

Permalink
add submodule docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad committed Jan 3, 2025
1 parent 744c66d commit a2a3a76
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 5 deletions.
12 changes: 12 additions & 0 deletions ark/docs/components/snippets/contentsById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
betterErrors:
'import { type, type ArkErrors } from "arktype"\n\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"versions?": "(number | string)[]"\n})\n\ninterface RuntimeErrors extends ArkErrors {\n\t/**platform must be "android" or "ios" (was "enigma")\nversions[2] must be a number or a string (was bigint)*/\n\tsummary: string\n}\n\nconst narrowMessage = (e: ArkErrors): e is RuntimeErrors => true\n\n// ---cut---\nconst out = user({\n\tname: "Alan Turing",\n\tplatform: "enigma",\n\tversions: [0, "1", 0n]\n})\n\nif (out instanceof type.errors) {\n\t// ---cut-start---\n\tif (!narrowMessage(out)) throw new Error()\n\t// ---cut-end---\n\t// hover summary to see validation errors\n\tconsole.error(out.summary)\n}\n',
clarityAndConcision:
'// @errors: 2322\nimport { type } from "arktype"\n// this file is written in JS so that it can include a syntax error\n// without creating a type error while still displaying the error in twoslash\n// ---cut---\n// hover me\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"versions?": "number | string)[]"\n})\n',
deepIntrospectability:
'import { type } from "arktype"\n\nconst user = type({\n\tname: "string",\n\tdevice: {\n\t\tplatform: "\'android\' | \'ios\'",\n\t\t"version?": "number | string"\n\t}\n})\n\n// ---cut---\nuser.extends("object") // true\nuser.extends("string") // false\n// true (string is narrower than unknown)\nuser.extends({\n\tname: "unknown"\n})\n// false (string is wider than "Alan")\nuser.extends({\n\tname: "\'Alan\'"\n})\n',
intrinsicOptimization:
'import { type } from "arktype"\n// prettier-ignore\n// ---cut---\n// all unions are optimally discriminated\n// even if multiple/nested paths are needed\nconst account = type({\n\tkind: "\'admin\'",\n\t"powers?": "string[]"\n}).or({\n\tkind: "\'superadmin\'",\n\t"superpowers?": "string[]"\n}).or({\n\tkind: "\'pleb\'"\n})\n',
unparalleledDx:
'// @noErrors\nimport { type } from "arktype"\n// prettier-ignore\n// ---cut---\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"version?": "number | s"\n\t// ^|\n})\n'
}
77 changes: 76 additions & 1 deletion ark/docs/content/docs/scopes/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Scopes
---

<a id="intro" />

Scopes are the foundation of ArkType, and one of the most powerful features for users wanting full control over configuration and to make their own keywords available fluidly within string definition syntax.

A scope is just like a scope in code- a resolution space where you can define types, generics, or other scopes. The `type` export is a actually just a method on our default `Scope`!
Expand Down Expand Up @@ -201,7 +203,80 @@ Luckily, despite its appearance, the type otherwise behaves as you'd expect- Typ

### submodules

🚧 Coming soon ™️🚧
If you've used keywords like `string.email` or `number.integer`, you may wonder if aliases can be grouped in your own Scopes. Recall from [the introduction to Scopes](#intro) that `type` is actually just a method on ArkType's default `Scope`, meaning all of its functionality is available externally, including alias groups called _Submodules_.

Submodules are groups of aliases with a shared prefix. To define one, just assign the value of the prefix to a `Module` with the names you want:

```ts
const subAliases = type.module({ alias: "number" })

const rootScope = scope({
a: "string",
b: "sub.alias",
sub: subAliases
})

const myType = rootScope.type({
someKey: "sub.alias[]"
})
```

Submodules are parsed bottom-up. This means subaliases can be referenced directly in the root scope,
but root aliases can't be referenced from the submodule, even if it's inlined.

#### nested

Submodules can be nested to arbitrary depth:

```ts
const subAliases = type.module({ alias: "number" })

const rootScope = scope({
a: "string",
b: "sub.alias",
sub: subAliases
})
// ---cut---

const rootScopeSquared = scope({
// reference rootScope from our previous example
newRoot: rootScope.export()
})

const myNewType = rootScopeSquared.type({
someOtherKey: "newRoot.sub.alias | boolean"
})
```

#### rooted

The Submodules from our previous examples group `Type`s together, but cannot be referenced as `Type`s themselves the way `string` and `number` can. To define a _Rooted Submodule_, just use an alias called `root`:

```ts
const userModule = type.module({
root: {
name: "string"
},
// subaliases can extend a base type by referencing 'root'
// like any other alias
admin: {
"...": "root",
isAdmin: "true"
},
saiyan: {
"...": "root",
powerLevel: "number > 9000"
}
})

const rootModule = type.module({
user: userModule,
// user can now be referenced directly in a definition
group: "user[]",
// or used as a prefix to access subaliases
elevatedUser: "user.admin | user.saiyan"
})
```

### thunks

Expand Down
12 changes: 10 additions & 2 deletions ark/schema/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export abstract class BaseScope<$ extends {} = {}> {
readonly resolutions: {
[alias: string]: CachedResolution | undefined
} = {}
readonly json: JsonStructure = {}

exportedNames: string[] = []
readonly aliases: Record<string, unknown> = {}
protected resolved = false
Expand Down Expand Up @@ -218,6 +218,14 @@ export abstract class BaseScope<$ extends {} = {}> {
return this
}

// json is populated when the scope is exported, so ensure it is populated
// before allowing external access
private _json: JsonStructure | undefined
get json(): JsonStructure {
if (!this._json) this.export()
return this._json!
}

defineSchema<def extends RootSchema>(def: def): def {
return def
}
Expand Down Expand Up @@ -485,7 +493,7 @@ export abstract class BaseScope<$ extends {} = {}> {

this._exportedResolutions = resolutionsOfModule(this, this._exports)

Object.assign(this.json, resolutionsToJson(this._exportedResolutions))
this._json = resolutionsToJson(this._exportedResolutions)
Object.assign(this.resolutions, this._exportedResolutions)

this.references = Object.values(this.referencesById)
Expand Down
84 changes: 82 additions & 2 deletions ark/type/__tests__/submodule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ contextualize.each(
)

contextualize.each(
"subtypes",
"rooted submodules",
() => {
const foo = scope({ root: "'foo'", bar: "'bar'" }).export()
const foo = type.module({ root: "'foo'", bar: "'bar'" })

const $ = scope({
foo,
Expand Down Expand Up @@ -193,6 +193,86 @@ contextualize.each(
// @ts-expect-error
attest(() => $.type("foo.")).completions({ "foo.": ["foo.bar"] })
})

it("docs example", () => {
const userModule = type.module({
root: {
name: "string"
},
// subaliases can extend a base type by referencing 'root'
// like any other alias
admin: {
"...": "root",
isAdmin: "true"
},
saiyan: {
"...": "root",
powerLevel: "number > 9000"
}
})

const rootScope = type.scope({
user: userModule,
// user can now be referenced directly in a definition
group: "user[]",
// or used as a prefix to access subaliases
elevatedUser: "user.admin | user.saiyan"
})

attest(rootScope).type.toString.snap()
attest(rootScope.json).snap({
"user.root": {
required: [{ key: "name", value: "string" }],
domain: "object"
},
"user.admin": {
required: [
{ key: "isAdmin", value: { unit: true } },
{ key: "name", value: "string" }
],
domain: "object"
},
"user.saiyan": {
required: [
{ key: "name", value: "string" },
{
key: "powerLevel",
value: { domain: "number", min: { exclusive: true, rule: 9000 } }
}
],
domain: "object"
},
group: {
sequence: {
required: [{ key: "name", value: "string" }],
domain: "object"
},
proto: "Array"
},
elevatedUser: [
{
required: [
{ key: "isAdmin", value: { unit: true } },
{ key: "name", value: "string" }
],
domain: "object"
},
{
required: [
{ key: "name", value: "string" },
{
key: "powerLevel",
value: {
domain: "number",
min: { exclusive: true, rule: 9000 }
}
}
],
domain: "object"
}
]
})
})
}
)

Expand Down

0 comments on commit a2a3a76

Please sign in to comment.