Add validate util

This commit is contained in:
Eric Bailey
2025-02-06 10:43:09 -06:00
parent 59e32b3005
commit 119f56bd00
12 changed files with 68 additions and 47 deletions
+1
View File
@@ -55,6 +55,7 @@
}, },
"dependencies": { "dependencies": {
"@atproto/api": "0.14.0-next.6", "@atproto/api": "0.14.0-next.6",
"@atproto/lexicon": "0.4.6-next.5",
"@bitdrift/react-native": "^0.6.2", "@bitdrift/react-native": "^0.6.2",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+4 -6
View File
@@ -4,11 +4,7 @@ import type ViewShot from 'react-native-view-shot'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import {createAssetAsync} from 'expo-media-library' import {createAssetAsync} from 'expo-media-library'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import { import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
AppBskyGraphDefs,
AppBskyGraphStarterpack,
asPredicate,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -22,6 +18,7 @@ import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog' import {DialogControlProps} from '#/components/Dialog'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {QrCode} from '#/components/StarterPack/QrCode' import {QrCode} from '#/components/StarterPack/QrCode'
import * as bsky from '#/types/bsky'
export function QrCodeDialog({ export function QrCodeDialog({
starterPack, starterPack,
@@ -82,8 +79,9 @@ export function QrCodeDialog({
setIsProcessing(true) setIsProcessing(true)
if ( if (
!asPredicate(AppBskyGraphStarterpack.validateRecord)( !bsky.validate(
starterPack.record, starterPack.record,
AppBskyGraphStarterpack.validateRecord,
) )
) { ) {
return return
+4 -4
View File
@@ -4,9 +4,9 @@ import {
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
asPredicate,
} from '@atproto/api' } from '@atproto/api'
import * as bsky from '#/types/bsky'
import {isPostInLanguage} from '../../locale/helpers' import {isPostInLanguage} from '../../locale/helpers'
import {FALLBACK_MARKER_POST} from './feed/home' import {FALLBACK_MARKER_POST} from './feed/home'
import {ReasonFeedSource} from './feed/types' import {ReasonFeedSource} from './feed/types'
@@ -68,7 +68,7 @@ export class FeedViewPostsSlice {
} }
if ( if (
!AppBskyFeedPost.isRecord(post.record) || !AppBskyFeedPost.isRecord(post.record) ||
!asPredicate(AppBskyFeedPost.validateRecord)(post.record) !bsky.validate(post.record, AppBskyFeedPost.validateRecord)
) { ) {
return return
} }
@@ -100,7 +100,7 @@ export class FeedViewPostsSlice {
if ( if (
!AppBskyFeedDefs.isPostView(parent) || !AppBskyFeedDefs.isPostView(parent) ||
!AppBskyFeedPost.isRecord(parent.record) || !AppBskyFeedPost.isRecord(parent.record) ||
!asPredicate(AppBskyFeedPost.validateRecord)(parent.record) !bsky.validate(parent.record, AppBskyFeedPost.validateRecord)
) { ) {
this.isOrphan = true this.isOrphan = true
return return
@@ -142,7 +142,7 @@ export class FeedViewPostsSlice {
if ( if (
!AppBskyFeedDefs.isPostView(root) || !AppBskyFeedDefs.isPostView(root) ||
!AppBskyFeedPost.isRecord(root.record) || !AppBskyFeedPost.isRecord(root.record) ||
!asPredicate(AppBskyFeedPost.validateRecord)(root.record) !bsky.validate(root.record, AppBskyFeedPost.validateRecord)
) { ) {
this.isOrphan = true this.isOrphan = true
return return
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {$Typed, asPredicate,ComAtprotoLabelDefs} from '@atproto/api' import {$Typed, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -12,6 +12,7 @@ import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
export function PwiOptOut() { export function PwiOptOut() {
const t = useTheme() const t = useTheme()
@@ -33,9 +34,10 @@ export function PwiOptOut() {
profile, profile,
updates: existing => { updates: existing => {
// create labels attr if needed // create labels attr if needed
const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = asPredicate( const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate(
existing.labels,
ComAtprotoLabelDefs.validateSelfLabels, ComAtprotoLabelDefs.validateSelfLabels,
)(existing.labels) )
? existing.labels ? existing.labels
: { : {
$type: 'com.atproto.label.defs#selfLabels', $type: 'com.atproto.label.defs#selfLabels',
+2 -6
View File
@@ -1,9 +1,5 @@
import React from 'react' import React from 'react'
import { import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
AppBskyGraphDefs,
AppBskyGraphStarterpack,
asPredicate,
} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@@ -128,7 +124,7 @@ export function Provider({
const createInitialState = (): State => { const createInitialState = (): State => {
if ( if (
starterPack && starterPack &&
asPredicate(AppBskyGraphStarterpack.validateRecord)(starterPack.record) bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
) { ) {
return { return {
canNext: true, canNext: true,
+2 -2
View File
@@ -4,7 +4,6 @@ import {
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPostgate, AppBskyFeedPostgate,
asPredicate,
AtUri, AtUri,
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
@@ -22,6 +21,7 @@ import {
POSTGATE_COLLECTION, POSTGATE_COLLECTION,
} from '#/state/queries/postgate/util' } from '#/state/queries/postgate/util'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
export async function getPostgateRecord({ export async function getPostgateRecord({
agent, agent,
@@ -63,7 +63,7 @@ export async function getPostgateRecord({
if ( if (
data.value && data.value &&
asPredicate(AppBskyFeedPostgate.validateRecord)(data.value) bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)
) { ) {
return data.value return data.value
} else { } else {
+1 -2
View File
@@ -4,7 +4,6 @@ import {
AppBskyGraphGetStarterPack, AppBskyGraphGetStarterPack,
AppBskyGraphStarterpack, AppBskyGraphStarterpack,
AppBskyRichtextFacet, AppBskyRichtextFacet,
asPredicate,
AtUri, AtUri,
BskyAgent, BskyAgent,
RichText, RichText,
@@ -369,7 +368,7 @@ export async function precacheStarterPack(
starterPackView = starterPack starterPackView = starterPack
} else if ( } else if (
AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && AppBskyGraphDefs.isStarterPackViewBasic(starterPack) &&
asPredicate(AppBskyGraphStarterpack.validateRecord)(starterPack.record) bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
) { ) {
const listView: AppBskyGraphDefs.ListViewBasic = { const listView: AppBskyGraphDefs.ListViewBasic = {
uri: starterPack.record.list, uri: starterPack.record.list,
+2 -2
View File
@@ -2,7 +2,6 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedGetPostThread, AppBskyFeedGetPostThread,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
asPredicate,
AtUri, AtUri,
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
@@ -21,6 +20,7 @@ import {
} from '#/state/queries/threadgate/util' } from '#/state/queries/threadgate/util'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
import * as bsky from '#/types/bsky'
export * from '#/state/queries/threadgate/types' export * from '#/state/queries/threadgate/types'
export * from '#/state/queries/threadgate/util' export * from '#/state/queries/threadgate/util'
@@ -141,7 +141,7 @@ export async function getThreadgateRecord({
if ( if (
data.value && data.value &&
asPredicate(AppBskyFeedThreadgate.validateRecord)(data.value) bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord)
) { ) {
return data.value return data.value
} else { } else {
+3 -2
View File
@@ -1,6 +1,7 @@
import {AppBskyFeedDefs, AppBskyFeedThreadgate, asPredicate} from '@atproto/api' import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
import * as bsky from '#/types/bsky'
export function threadgateViewToAllowUISetting( export function threadgateViewToAllowUISetting(
threadgateView: AppBskyFeedDefs.ThreadgateView | undefined, threadgateView: AppBskyFeedDefs.ThreadgateView | undefined,
@@ -8,7 +9,7 @@ export function threadgateViewToAllowUISetting(
// Validate the record for clarity, since backwards compat code is a little confusing // Validate the record for clarity, since backwards compat code is a little confusing
const threadgate = const threadgate =
threadgateView && threadgateView &&
asPredicate(AppBskyFeedThreadgate.validateRecord)(threadgateView.record) bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord)
? threadgateView.record ? threadgateView.record
: undefined : undefined
return threadgateRecordToAllowUISetting(threadgate) return threadgateRecordToAllowUISetting(threadgate)
+31 -7
View File
@@ -1,19 +1,22 @@
import {asPredicate} from '@atproto/api'
import {ValidationResult} from '@atproto/lexicon'
export * as profile from '#/types/bsky/profile' export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack' export * as starterPack from '#/types/bsky/starterPack'
/** /**
* Use sparingly, and only when you know it's safe to do so. * Fast type checking without full schema validation, for use with data we
* trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
* utils do not assert the type of the entire object, only the `$type` string.
* *
* Our SDK's `is*` identity utils do not assert the type of the entire object, * For full validation of the object schema, use the `validate` export from
* and although the `isValid*` utils do, they also fully validate the object * this file.
* shape, which has a performance cost. This util allows us to prescribe the
* type we expect, while only checking the `$type` value of the record.
* *
* Usage: * Usage:
* ```ts * ```ts
* import * as atp from '#/types/atproto' * import * as bsky from '#/types/bsky'
* *
* if (atp.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) { * if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here * // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* } * }
* ``` * ```
@@ -24,3 +27,24 @@ export function dangerousIsType<R extends {$type?: string}>(
): record is R { ): record is R {
return identity(record) return identity(record)
} }
/**
* Fully validates the object schema, which as a performance cost.
*
* For faster checks with data we trust, like that from our app view, use the
* `dangerousIsType` export from this same file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
*/
export function validate<R extends {$type?: string}>(
record: unknown,
identity: (v: unknown) => ValidationResult<R>,
): record is R {
return asPredicate(identity)(record)
}
+2 -2
View File
@@ -3,7 +3,6 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
asPredicate,
AtUri, AtUri,
ModerationDecision, ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
@@ -29,6 +28,7 @@ import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover' import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
import {ContentHider} from '../../../components/moderation/ContentHider' import {ContentHider} from '../../../components/moderation/ContentHider'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts' import {PostAlerts} from '../../../components/moderation/PostAlerts'
@@ -54,7 +54,7 @@ export function Post({
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const record = useMemo<AppBskyFeedPost.Record | undefined>( const record = useMemo<AppBskyFeedPost.Record | undefined>(
() => () =>
asPredicate(AppBskyFeedPost.validateRecord)(post.record) bsky.validate(post.record, AppBskyFeedPost.validateRecord)
? post.record ? post.record
: undefined, : undefined,
[post], [post],
+11 -11
View File
@@ -282,6 +282,17 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/lexicon@0.4.6-next.5", "@atproto/lexicon@^0.4.6-next.5":
version "0.4.6-next.5"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.6-next.5.tgz#433a941cf8902afec15135f70cf2cbed37c35406"
integrity sha512-Id1atQ1rZ3dUGAUyBcauD+zs5lGHSmoAlC1Kwb2DEvkaWMv3oTXUjtdQxyb0/Kz4YaFI+nxLLRH9NBO+BipG+w==
dependencies:
"@atproto/common-web" "^0.3.2"
"@atproto/syntax" "^0.3.1"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/lexicon@^0.4.4": "@atproto/lexicon@^0.4.4":
version "0.4.4" version "0.4.4"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a"
@@ -304,17 +315,6 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/lexicon@^0.4.6-next.5":
version "0.4.6-next.5"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.6-next.5.tgz#433a941cf8902afec15135f70cf2cbed37c35406"
integrity sha512-Id1atQ1rZ3dUGAUyBcauD+zs5lGHSmoAlC1Kwb2DEvkaWMv3oTXUjtdQxyb0/Kz4YaFI+nxLLRH9NBO+BipG+w==
dependencies:
"@atproto/common-web" "^0.3.2"
"@atproto/syntax" "^0.3.1"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/oauth-provider@^0.2.10": "@atproto/oauth-provider@^0.2.10":
version "0.2.10" version "0.2.10"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901"