add lex client seam, types/bsky schema helpers, pilot trending migration
This commit is contained in:
+1
-1
@@ -62,7 +62,7 @@
|
||||
"lint": "oxlint --quiet src modules",
|
||||
"lint-native": "swiftlint ./modules && ktlint ./modules",
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"lexicons:generate": "lex build --clear --index-file",
|
||||
"lexicons:generate": "lex build --clear --index-file --import-ext ''",
|
||||
"lexicons:update": "lex install --update",
|
||||
"typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web",
|
||||
"typecheck:ios": "tsc --project ./tsconfig.check.ios.json",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {type AppBskyUnspeccedGetTrends, hasMutedWord} from '@atproto/api'
|
||||
import {hasMutedWord} from '@atproto/api'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {
|
||||
@@ -10,7 +10,8 @@ import {logger} from '#/logger'
|
||||
import {getContentLanguages} from '#/state/preferences/languages'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useLexClient} from '#/state/session'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
export const DEFAULT_LIMIT = 5
|
||||
|
||||
@@ -32,7 +33,7 @@ export const createGetTrendsQueryKey = (limit?: number) =>
|
||||
limit === undefined ? ['trends'] : ['trends', {limit}]
|
||||
|
||||
export function useGetTrendsQuery(props: QueryProps = {}) {
|
||||
const agent = useAgent()
|
||||
const client = useLexClient()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const limit = props.limit ?? DEFAULT_LIMIT
|
||||
const mutedWords = useMemo(() => {
|
||||
@@ -46,7 +47,8 @@ export function useGetTrendsQuery(props: QueryProps = {}) {
|
||||
queryKey: createGetTrendsQueryKey(limit),
|
||||
queryFn: async () => {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const {data} = await agent.app.bsky.unspecced.getTrends(
|
||||
const data = await client.call(
|
||||
app.bsky.unspecced.getTrends,
|
||||
{
|
||||
limit,
|
||||
},
|
||||
@@ -63,7 +65,7 @@ export function useGetTrendsQuery(props: QueryProps = {}) {
|
||||
return data
|
||||
},
|
||||
select: useCallback(
|
||||
(data: AppBskyUnspeccedGetTrends.OutputSchema) => {
|
||||
(data: app.bsky.unspecced.getTrends.$OutputBody) => {
|
||||
return {
|
||||
recId: data.recIdStr,
|
||||
trends: dedupe(
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {type AtpAgent} from '@atproto/api'
|
||||
import {Client} from '@atproto/lex-client'
|
||||
import {describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
import {app} from '#/lexicons'
|
||||
import {agentToLexClient} from '../clients'
|
||||
|
||||
/**
|
||||
* Minimal stand-in for the parts of AtpAgent that `agentToLexClient` reads: a
|
||||
* `did` and a `fetchHandler`. Returned as `AtpAgent` via a cast since we only
|
||||
* exercise those two members.
|
||||
*/
|
||||
function makeFakeAgent(did: string | undefined) {
|
||||
const fetchHandler = jest.fn(
|
||||
(_path: string, _init: RequestInit): Promise<Response> =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
did: 'did:plc:fake',
|
||||
handle: 'fake.bsky.social',
|
||||
}),
|
||||
{status: 200, headers: {'content-type': 'application/json'}},
|
||||
),
|
||||
),
|
||||
)
|
||||
const agent = {did, fetchHandler}
|
||||
return {agent: agent as unknown as AtpAgent, fetchHandler}
|
||||
}
|
||||
|
||||
describe('agentToLexClient', () => {
|
||||
it('routes client.call through the agent fetchHandler', async () => {
|
||||
const {agent, fetchHandler} = makeFakeAgent('did:plc:fake')
|
||||
const client = agentToLexClient(agent)
|
||||
|
||||
const result = await client.call(app.bsky.actor.getProfile.main, {
|
||||
actor: 'fake.bsky.social',
|
||||
})
|
||||
|
||||
expect(fetchHandler).toHaveBeenCalledTimes(1)
|
||||
const [path] = fetchHandler.mock.calls[0]
|
||||
expect(path).toContain('/xrpc/app.bsky.actor.getProfile')
|
||||
expect(path).toContain('actor=fake.bsky.social')
|
||||
expect(result.handle).toBe('fake.bsky.social')
|
||||
})
|
||||
|
||||
it('passes through the agent did', () => {
|
||||
const {agent} = makeFakeAgent('did:plc:fake')
|
||||
const client = agentToLexClient(agent)
|
||||
expect(client.did).toBe('did:plc:fake')
|
||||
})
|
||||
|
||||
it('reflects an undefined did (unauthenticated agent)', () => {
|
||||
const {agent} = makeFakeAgent(undefined)
|
||||
const client = agentToLexClient(agent)
|
||||
expect(client.did).toBeUndefined()
|
||||
})
|
||||
|
||||
it('memoizes one client per agent', () => {
|
||||
const {agent: agentA} = makeFakeAgent('did:plc:a')
|
||||
const {agent: agentB} = makeFakeAgent('did:plc:b')
|
||||
|
||||
const clientA1 = agentToLexClient(agentA)
|
||||
const clientA2 = agentToLexClient(agentA)
|
||||
const clientB = agentToLexClient(agentB)
|
||||
|
||||
expect(clientA1).toBeInstanceOf(Client)
|
||||
expect(clientA1).toBe(clientA2)
|
||||
expect(clientA1).not.toBe(clientB)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import {type AtpAgent} from '@atproto/api'
|
||||
import {Client} from '@atproto/lex-client'
|
||||
|
||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||
|
||||
/**
|
||||
* Stable per-agent cache of lex `Client` instances. We never reuse an
|
||||
* `AtpAgent` (the session provider disposes the previous one on switch), so a
|
||||
* `WeakMap` keyed on the agent gives us a client whose identity is stable for
|
||||
* the lifetime of that agent. This keeps React Query keys and hook consumers
|
||||
* from churning on every render.
|
||||
*/
|
||||
const clientForAgent = new WeakMap<AtpAgent, Client>()
|
||||
|
||||
/**
|
||||
* Wrap a live {@link AtpAgent} as a lex {@link Client}, bridging the old session
|
||||
* source of truth to the new SDK so features can migrate incrementally.
|
||||
*
|
||||
* The client talks to the agent through the minimal lex `Agent` interface
|
||||
* (`{ did, fetchHandler }`). We deliberately route `fetchHandler` through
|
||||
* `agent.fetchHandler` (the agent's XRPC dispatch layer) rather than
|
||||
* `agent.sessionManager.fetchHandler`:
|
||||
*
|
||||
* - `agent.fetchHandler` (see @atproto/api `agent.js`) is where the agent
|
||||
* applies its configured `atproto-proxy` header (set via `configureProxy` to
|
||||
* the appview) and its `atproto-accept-labelers` header, before delegating to
|
||||
* `sessionManager.fetchHandler` for authorization + token refresh.
|
||||
* - `sessionManager.fetchHandler` (`CredentialSession`) only adds the auth
|
||||
* token and handles refresh - it does NOT proxy or attach labelers. Wrapping
|
||||
* it directly would silently drop appview proxying and moderation labelers.
|
||||
*
|
||||
* Because the wrapped agent already applies proxy + labeler headers, we do NOT
|
||||
* pass a `service` option to the `Client` (lex-client only sets `atproto-proxy`
|
||||
* when `service` is provided) and we leave `Client.appLabelers` at its default
|
||||
* empty set. This avoids double-setting either header.
|
||||
*
|
||||
* Results are memoized per-agent so the returned client is referentially stable.
|
||||
*/
|
||||
export function agentToLexClient(agent: AtpAgent): Client {
|
||||
const cached = clientForAgent.get(agent)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const client = new Client({
|
||||
get did() {
|
||||
return agent.did
|
||||
},
|
||||
fetchHandler: (path, init) => agent.fetchHandler(path, init),
|
||||
})
|
||||
clientForAgent.set(agent, client)
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily-constructed unauthenticated client pointed at the public appview. It
|
||||
* hits {@link PUBLIC_BSKY_SERVICE} directly, mirroring `createPublicAgent`'s
|
||||
* service URL, so no proxying is required.
|
||||
*/
|
||||
let publicClient: Client | undefined
|
||||
|
||||
function getPublicLexClient(): Client {
|
||||
publicClient ??= new Client(PUBLIC_BSKY_SERVICE)
|
||||
return publicClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthenticated lex {@link Client} for public appview reads. A process-wide
|
||||
* singleton, so its identity is stable across renders.
|
||||
*/
|
||||
export function usePublicLexClient(): Client {
|
||||
return getPublicLexClient()
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useSyncExternalStore,
|
||||
} from 'react'
|
||||
import {type AtpAgent, type AtpSessionEvent} from '@atproto/api'
|
||||
import {type Client} from '@atproto/lex-client'
|
||||
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from './agent'
|
||||
import {type Action, getInitialState, reducer, type State} from './reducer'
|
||||
export {isSignupQueued} from './util'
|
||||
import {agentToLexClient} from './clients'
|
||||
import {addSessionDebugLog} from './logging'
|
||||
export type {SessionAccount} from '#/state/session/types'
|
||||
|
||||
@@ -460,3 +462,14 @@ export function useAgent(): AtpAgent {
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated lex {@link Client} wrapping the current session agent. Stable
|
||||
* per-agent, so it only changes identity when the active account changes.
|
||||
*
|
||||
* @see agentToLexClient for how the AtpAgent is bridged to the lex Client.
|
||||
*/
|
||||
export function useLexClient(): Client {
|
||||
const agent = useAgent()
|
||||
return agentToLexClient(agent)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import {AppBskyFeedPost} from '@atproto/api'
|
||||
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const now = () => new Date().toISOString()
|
||||
|
||||
/**
|
||||
* A structurally valid `app.bsky.feed.post` record.
|
||||
*/
|
||||
const validPost = {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: 'hello world',
|
||||
createdAt: now(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Right `$type`, but the body is invalid (`text` is not a string, `createdAt`
|
||||
* is not a datetime). Passes a `$type`-only guard, fails full validation.
|
||||
*/
|
||||
const invalidPost = {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: 123,
|
||||
createdAt: 'not-a-datetime',
|
||||
}
|
||||
|
||||
/**
|
||||
* A different record type entirely - should fail even the fast guard.
|
||||
*/
|
||||
const wrongType = {
|
||||
$type: 'app.bsky.feed.like',
|
||||
subject: {uri: 'at://x', cid: 'y'},
|
||||
createdAt: now(),
|
||||
}
|
||||
|
||||
describe('types/bsky new-world helpers (#/lexicons)', () => {
|
||||
describe('isType (fast, $type-only)', () => {
|
||||
it('accepts a valid record', () => {
|
||||
expect(bsky.isType(app.bsky.feed.post, validPost)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a $type mismatch', () => {
|
||||
expect(bsky.isType(app.bsky.feed.post, wrongType)).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts an invalid body that has the right $type (dangerous semantics)', () => {
|
||||
// Mirrors the old `dangerousIsType`: only the `$type` is checked, so a
|
||||
// structurally invalid record still passes.
|
||||
expect(bsky.isType(app.bsky.feed.post, invalidPost)).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a schema passed as its namespace module or its main schema', () => {
|
||||
expect(bsky.isType(app.bsky.feed.post, validPost)).toBe(true)
|
||||
expect(bsky.isType(app.bsky.feed.post.main, validPost)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false (does not throw) for null and undefined', () => {
|
||||
// Mirrors the old `dangerousIsType`/`is$typed` behavior - call sites
|
||||
// pass e.g. `post.record` which may be undefined.
|
||||
expect(bsky.isType(app.bsky.feed.post, null)).toBe(false)
|
||||
expect(bsky.isType(app.bsky.feed.post, undefined)).toBe(false)
|
||||
expect(bsky.isType(app.bsky.feed.post, 'string')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires a present $type for typed-object def schemas', () => {
|
||||
/*
|
||||
* The generated TypedObjectSchema.isTypeOf treats a missing $type as a
|
||||
* match (maybe-typed semantics). Our helper must NOT: when
|
||||
* discriminating unions by $type, an object without $type would
|
||||
* otherwise satisfy every branch.
|
||||
*/
|
||||
expect(bsky.isType(app.bsky.feed.defs.postView, {foo: 1})).toBe(false)
|
||||
expect(bsky.isType(app.bsky.feed.defs.postView, {})).toBe(false)
|
||||
expect(
|
||||
bsky.isType(app.bsky.feed.defs.postView, {
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('matches (full validation guard)', () => {
|
||||
it('accepts a valid record', () => {
|
||||
expect(bsky.matches(app.bsky.feed.post, validPost)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a $type mismatch', () => {
|
||||
expect(bsky.matches(app.bsky.feed.post, wrongType)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an invalid body even though the $type matches', () => {
|
||||
expect(bsky.matches(app.bsky.feed.post, invalidPost)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('safeParse (full validation, no throw)', () => {
|
||||
it('succeeds and returns the value for a valid record', () => {
|
||||
const result = bsky.safeParse(app.bsky.feed.post, validPost)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.value.text).toBe('hello world')
|
||||
}
|
||||
})
|
||||
|
||||
it('fails with a reason for an invalid record', () => {
|
||||
const result = bsky.safeParse(app.bsky.feed.post, invalidPost)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.reason).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('parse (full validation, throws)', () => {
|
||||
it('returns the typed value for a valid record', () => {
|
||||
const record = bsky.parse(app.bsky.feed.post, validPost)
|
||||
expect(record.text).toBe('hello world')
|
||||
})
|
||||
|
||||
it('throws for an invalid record', () => {
|
||||
expect(() => bsky.parse(app.bsky.feed.post, invalidPost)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('types/bsky legacy helpers (@atproto/api) still work', () => {
|
||||
it('dangerousIsType accepts the right $type without validating the body', () => {
|
||||
expect(
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
validPost,
|
||||
AppBskyFeedPost.isRecord,
|
||||
),
|
||||
).toBe(true)
|
||||
// Right $type, invalid body - still passes the fast guard.
|
||||
expect(
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
invalidPost,
|
||||
AppBskyFeedPost.isRecord,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
wrongType,
|
||||
AppBskyFeedPost.isRecord,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('validate fully validates the body', () => {
|
||||
expect(
|
||||
bsky.validate<AppBskyFeedPost.Record>(
|
||||
validPost,
|
||||
AppBskyFeedPost.validateRecord,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
bsky.validate<AppBskyFeedPost.Record>(
|
||||
invalidPost,
|
||||
AppBskyFeedPost.validateRecord,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
+164
-2
@@ -1,4 +1,16 @@
|
||||
import {type ValidationResult} from '@atproto/lexicon'
|
||||
import {
|
||||
getMain,
|
||||
type InferInput,
|
||||
type InferOutput,
|
||||
type Main,
|
||||
type ParseOptions,
|
||||
type RecordSchema,
|
||||
type Schema,
|
||||
type TypedObjectSchema,
|
||||
type ValidateOptions,
|
||||
type ValidationResult,
|
||||
} from '@atproto/lex'
|
||||
import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon'
|
||||
|
||||
export * as post from '#/types/bsky/post'
|
||||
export * as profile from '#/types/bsky/profile'
|
||||
@@ -45,7 +57,157 @@ export function dangerousIsType<R extends {$type?: string}>(
|
||||
*/
|
||||
export function validate<R extends {$type?: string}>(
|
||||
record: unknown,
|
||||
validator: (v: unknown) => ValidationResult<R>,
|
||||
validator: (v: unknown) => LegacyValidationResult<R>,
|
||||
): record is R {
|
||||
return validator(record).success
|
||||
}
|
||||
|
||||
/*
|
||||
* New-world helpers (below) operate on the generated lexicon schema objects
|
||||
* from '#/lexicons', replacing the `@atproto/api`-based helpers above.
|
||||
*
|
||||
* The old helpers take a record plus a standalone `is*`/`validate*` function
|
||||
* (e.g. `AppBskyFeedPost.isRecord`). The new codegen instead attaches the
|
||||
* validation surface directly to each schema, so these helpers take the schema
|
||||
* object itself:
|
||||
*
|
||||
* ```ts
|
||||
* import {app} from '#/lexicons'
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* // old: bsky.dangerousIsType(v, AppBskyFeedPost.isRecord)
|
||||
* bsky.isType(app.bsky.feed.post, v)
|
||||
*
|
||||
* // old: bsky.validate(v, AppBskyFeedPost.validateRecord)
|
||||
* bsky.matches(app.bsky.feed.post, v)
|
||||
* ```
|
||||
*
|
||||
* Every generated namespace module (e.g. `app.bsky.feed.post`) re-exports its
|
||||
* `main` schema, and bare defs (e.g. `app.bsky.feed.defs.postView`) are schema
|
||||
* objects directly. Both forms are accepted here - `getMain` unwraps a module
|
||||
* to its `main` schema and passes a bare schema through unchanged - so call
|
||||
* sites can pass whichever is in scope.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generated lexicon schema that carries a `$type` and therefore supports the
|
||||
* fast, `$type`-only {@link isType} check: a record schema (`app.bsky.feed.post`)
|
||||
* or a typed-object def schema (`app.bsky.feed.defs.postView`).
|
||||
*/
|
||||
type TypedSchema = RecordSchema | TypedObjectSchema
|
||||
|
||||
/**
|
||||
* Fast type checking without full schema validation, for use with data we
|
||||
* trust, or for non-critical path use cases. This only compares the `$type`
|
||||
* string; it does NOT assert the rest of the object matches the schema. An
|
||||
* invalid record with the right `$type` will pass.
|
||||
*
|
||||
* This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full
|
||||
* validation of the object schema, use {@link matches}, {@link parse}, or
|
||||
* {@link safeParse} from this same file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import {app} from '#/lexicons'
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.isType(app.bsky.feed.post, item)) {
|
||||
* // `item` is narrowed to the post record type here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function isType<S extends TypedSchema>(
|
||||
schema: Main<S>,
|
||||
value: unknown,
|
||||
): value is InferInput<S> {
|
||||
/*
|
||||
* Deliberately NOT delegating to the schema's `isTypeOf`: the generated
|
||||
* `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match
|
||||
* (maybe-typed semantics), which would let any plain object satisfy any
|
||||
* def-schema check and break `$type`-discriminated unions. The old
|
||||
* `dangerousIsType` required a present, matching `$type`, and so do we.
|
||||
* The nullish/object guard also mirrors the old `is$typed` behavior of
|
||||
* returning false (not throwing) for null/undefined input.
|
||||
*/
|
||||
return (
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
(value as {$type?: unknown}).$type === getMain(schema).$type
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully validates the object against the schema (strict, no coercion), which
|
||||
* has a performance cost, and narrows the value on success.
|
||||
*
|
||||
* This is the '#/lexicons' equivalent of {@link validate}. For faster checks
|
||||
* with data we trust, like that from our app view, use {@link isType} from this
|
||||
* same file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import {app} from '#/lexicons'
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.matches(app.bsky.feed.post, item)) {
|
||||
* // `item` is narrowed to the post record type here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function matches<S extends Schema>(
|
||||
schema: Main<S>,
|
||||
value: unknown,
|
||||
options?: ValidateOptions,
|
||||
): value is InferInput<S> {
|
||||
return getMain(schema).matches(value, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully validates and parses the value against the schema, returning the typed
|
||||
* value or throwing an `LexValidationError` on failure. Parsing may apply
|
||||
* schema transformations such as default values.
|
||||
*
|
||||
* Prefer {@link safeParse} where you want to branch on failure without a
|
||||
* try/catch, or {@link matches} where you only need a type guard.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import {app} from '#/lexicons'
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* const record = bsky.parse(app.bsky.feed.post, item) // typed, or throws
|
||||
* ```
|
||||
*/
|
||||
export function parse<S extends Schema>(
|
||||
schema: Main<S>,
|
||||
value: unknown,
|
||||
options?: ParseOptions,
|
||||
): InferOutput<S> {
|
||||
return getMain(schema).parse(value, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully validates and parses the value against the schema, returning a
|
||||
* discriminated `ValidationResult` instead of throwing. Parsing may apply
|
||||
* schema transformations such as default values.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import {app} from '#/lexicons'
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* const result = bsky.safeParse(app.bsky.feed.post, item)
|
||||
* if (result.success) {
|
||||
* // `result.value` is the typed post record
|
||||
* } else {
|
||||
* // `result.reason` is the LexValidationError
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function safeParse<S extends Schema>(
|
||||
schema: Main<S>,
|
||||
value: unknown,
|
||||
options?: ParseOptions,
|
||||
): ValidationResult<InferOutput<S>> {
|
||||
return getMain(schema).safeParse(value, options)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user