[SDK] Widen #/types/bsky views to dual-world unions (#11353)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
type $Typed as $TypedApi,
|
||||
type AppBskyEmbedRecord,
|
||||
type AppBskyFeedDefs,
|
||||
} from '@atproto/api'
|
||||
import {type $Typed} from '@atproto/lex'
|
||||
|
||||
import {type app} from '#/lexicons'
|
||||
import {
|
||||
type Embed,
|
||||
type EmbedType,
|
||||
parseEmbed,
|
||||
parseEmbedRecordView,
|
||||
} from '#/types/bsky/post'
|
||||
|
||||
const now = () => new Date().toISOString()
|
||||
|
||||
const imagesView = {
|
||||
$type: 'app.bsky.embed.images#view',
|
||||
images: [
|
||||
{
|
||||
$type: 'app.bsky.embed.images#viewImage',
|
||||
thumb: 'https://example.com/thumb.jpg',
|
||||
fullsize: 'https://example.com/full.jpg',
|
||||
alt: 'alt text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const galleryView = {
|
||||
$type: 'app.bsky.embed.gallery#view',
|
||||
items: [
|
||||
{
|
||||
$type: 'app.bsky.embed.gallery#viewImage',
|
||||
thumbnail: 'https://example.com/thumb.jpg',
|
||||
fullsize: 'https://example.com/full.jpg',
|
||||
alt: 'alt text',
|
||||
aspectRatio: {width: 1, height: 1},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const externalView = {
|
||||
$type: 'app.bsky.embed.external#view',
|
||||
external: {
|
||||
$type: 'app.bsky.embed.external#viewExternal',
|
||||
uri: 'https://example.com',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
}
|
||||
|
||||
const videoView = {
|
||||
$type: 'app.bsky.embed.video#view',
|
||||
cid: 'bafyvideo',
|
||||
playlist: 'https://example.com/playlist.m3u8',
|
||||
}
|
||||
|
||||
const viewRecord = {
|
||||
$type: 'app.bsky.embed.record#viewRecord',
|
||||
uri: 'at://did:plc:abc/app.bsky.feed.post/123',
|
||||
cid: 'bafypost',
|
||||
author: {
|
||||
$type: 'app.bsky.actor.defs#profileViewBasic',
|
||||
did: 'did:plc:abc',
|
||||
handle: 'alice.test',
|
||||
},
|
||||
value: {$type: 'app.bsky.feed.post', text: 'hello', createdAt: now()},
|
||||
indexedAt: now(),
|
||||
}
|
||||
|
||||
const recordView = {
|
||||
$type: 'app.bsky.embed.record#view',
|
||||
record: viewRecord,
|
||||
}
|
||||
|
||||
const generatorView = {
|
||||
$type: 'app.bsky.feed.defs#generatorView',
|
||||
uri: 'at://did:plc:abc/app.bsky.feed.generator/feed',
|
||||
cid: 'bafyfeed',
|
||||
did: 'did:web:example.com',
|
||||
creator: {
|
||||
$type: 'app.bsky.actor.defs#profileView',
|
||||
did: 'did:plc:abc',
|
||||
handle: 'alice.test',
|
||||
},
|
||||
displayName: 'Cool feed',
|
||||
indexedAt: now(),
|
||||
}
|
||||
|
||||
const starterPackViewBasic = {
|
||||
$type: 'app.bsky.graph.defs#starterPackViewBasic',
|
||||
uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123',
|
||||
cid: 'bafypack',
|
||||
record: {},
|
||||
creator: {
|
||||
$type: 'app.bsky.actor.defs#profileViewBasic',
|
||||
did: 'did:plc:abc',
|
||||
handle: 'alice.test',
|
||||
},
|
||||
indexedAt: now(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a fixture into the `parseEmbed` input position. The fixtures are plain
|
||||
* objects standing in for app view responses; the guards under test only read
|
||||
* `$type`, so the structural detail beyond that is not load-bearing.
|
||||
*/
|
||||
const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed']
|
||||
|
||||
/*
|
||||
* Type-level assertions for the dual-world widening. These are compile-time
|
||||
* only: each widened arm must accept both the `#/lexicons` view and the
|
||||
* `@atproto/api` view, and `parseEmbed` must accept a `PostView.embed` from
|
||||
* either world, because both worlds have live producers.
|
||||
*/
|
||||
type Assignable<From, To> = From extends To ? true : false
|
||||
type Expect<T extends true> = T
|
||||
|
||||
type _PostArmAcceptsNewWorld = Expect<
|
||||
Assignable<
|
||||
{type: 'post'; view: $Typed<app.bsky.embed.record.ViewRecord>},
|
||||
EmbedType<'post'>
|
||||
>
|
||||
>
|
||||
type _PostArmAcceptsOldWorld = Expect<
|
||||
Assignable<
|
||||
{type: 'post'; view: $TypedApi<AppBskyEmbedRecord.ViewRecord>},
|
||||
EmbedType<'post'>
|
||||
>
|
||||
>
|
||||
type _ParseEmbedAcceptsNewWorld = Expect<
|
||||
Assignable<
|
||||
app.bsky.feed.defs.PostView['embed'],
|
||||
Parameters<typeof parseEmbed>[0]
|
||||
>
|
||||
>
|
||||
type _ParseEmbedAcceptsOldWorld = Expect<
|
||||
Assignable<
|
||||
AppBskyFeedDefs.PostView['embed'],
|
||||
Parameters<typeof parseEmbed>[0]
|
||||
>
|
||||
>
|
||||
type _ParseEmbedReturnsEmbed = Expect<
|
||||
Assignable<ReturnType<typeof parseEmbed>, Embed>
|
||||
>
|
||||
|
||||
describe('types/bsky/post parseEmbed', () => {
|
||||
it('parses an images view', () => {
|
||||
const embed = parseEmbed(asEmbed(imagesView))
|
||||
expect(embed.type).toBe('images')
|
||||
if (embed.type === 'images') {
|
||||
expect(embed.view.images).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('parses a gallery view', () => {
|
||||
const embed = parseEmbed(asEmbed(galleryView))
|
||||
expect(embed.type).toBe('gallery')
|
||||
if (embed.type === 'gallery') {
|
||||
expect(embed.view.items).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('parses an external view', () => {
|
||||
const embed = parseEmbed(asEmbed(externalView))
|
||||
expect(embed.type).toBe('link')
|
||||
if (embed.type === 'link') {
|
||||
expect(embed.view.external.uri).toBe('https://example.com')
|
||||
}
|
||||
})
|
||||
|
||||
it('parses a video view', () => {
|
||||
const embed = parseEmbed(asEmbed(videoView))
|
||||
expect(embed.type).toBe('video')
|
||||
if (embed.type === 'video') {
|
||||
expect(embed.view.playlist).toBe('https://example.com/playlist.m3u8')
|
||||
}
|
||||
})
|
||||
|
||||
it('parses a record view through to its inner record', () => {
|
||||
const embed = parseEmbed(asEmbed(recordView))
|
||||
expect(embed.type).toBe('post')
|
||||
if (embed.type === 'post') {
|
||||
expect(embed.view.uri).toBe('at://did:plc:abc/app.bsky.feed.post/123')
|
||||
}
|
||||
})
|
||||
|
||||
it('parses a recordWithMedia view into both halves', () => {
|
||||
const embed = parseEmbed(
|
||||
asEmbed({
|
||||
$type: 'app.bsky.embed.recordWithMedia#view',
|
||||
record: recordView,
|
||||
media: imagesView,
|
||||
}),
|
||||
)
|
||||
expect(embed.type).toBe('post_with_media')
|
||||
if (embed.type === 'post_with_media') {
|
||||
expect(embed.view.type).toBe('post')
|
||||
expect(embed.media.type).toBe('images')
|
||||
}
|
||||
})
|
||||
|
||||
it('returns the unknown arm for an unrecognised $type', () => {
|
||||
const embed = parseEmbed(asEmbed({$type: 'com.example.someEmbed#view'}))
|
||||
expect(embed).toEqual({type: 'unknown', view: null})
|
||||
})
|
||||
|
||||
it('returns the unknown arm for undefined', () => {
|
||||
expect(parseEmbed(undefined)).toEqual({type: 'unknown', view: null})
|
||||
})
|
||||
|
||||
it('does not match an embed with no $type', () => {
|
||||
expect(parseEmbed(asEmbed({images: []}))).toEqual({
|
||||
type: 'unknown',
|
||||
view: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('types/bsky/post parseEmbedRecordView', () => {
|
||||
const asRecordView = (record: unknown) =>
|
||||
({record}) as app.bsky.embed.record.View
|
||||
|
||||
it('parses each known record variant', () => {
|
||||
expect(parseEmbedRecordView(asRecordView(viewRecord)).type).toBe('post')
|
||||
expect(
|
||||
parseEmbedRecordView(
|
||||
asRecordView({$type: 'app.bsky.embed.record#viewNotFound'}),
|
||||
).type,
|
||||
).toBe('post_not_found')
|
||||
expect(
|
||||
parseEmbedRecordView(
|
||||
asRecordView({$type: 'app.bsky.embed.record#viewBlocked'}),
|
||||
).type,
|
||||
).toBe('post_blocked')
|
||||
expect(
|
||||
parseEmbedRecordView(
|
||||
asRecordView({$type: 'app.bsky.embed.record#viewDetached'}),
|
||||
).type,
|
||||
).toBe('post_detached')
|
||||
expect(parseEmbedRecordView(asRecordView(generatorView)).type).toBe('feed')
|
||||
expect(
|
||||
parseEmbedRecordView(
|
||||
asRecordView({$type: 'app.bsky.graph.defs#listView'}),
|
||||
).type,
|
||||
).toBe('list')
|
||||
expect(
|
||||
parseEmbedRecordView(
|
||||
asRecordView({$type: 'app.bsky.labeler.defs#labelerView'}),
|
||||
).type,
|
||||
).toBe('labeler')
|
||||
expect(parseEmbedRecordView(asRecordView(starterPackViewBasic)).type).toBe(
|
||||
'starter_pack',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the unknown arm for an unrecognised record', () => {
|
||||
expect(
|
||||
parseEmbedRecordView(asRecordView({$type: 'com.example.thing'})),
|
||||
).toEqual({type: 'unknown', view: null})
|
||||
})
|
||||
})
|
||||
|
||||
describe('types/bsky/post Embed dual-world types', () => {
|
||||
it('has the compile-time dual-world assertions above satisfied', () => {
|
||||
/*
|
||||
* The assertions are the `_*` types declared at module scope; a failure
|
||||
* surfaces as a typecheck error, not a test failure. This case exists so the
|
||||
* intent is visible when reading the suite.
|
||||
*/
|
||||
const parsedNewWorld: Embed = parseEmbed(
|
||||
asEmbed({$type: 'app.bsky.embed.images#view', images: []}),
|
||||
)
|
||||
expect(parsedNewWorld.type).toBe('images')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
|
||||
import {type app} from '#/lexicons'
|
||||
import {
|
||||
type AnyStarterPackView,
|
||||
isBasicView,
|
||||
isView,
|
||||
} from '#/types/bsky/starterPack'
|
||||
|
||||
const now = () => new Date().toISOString()
|
||||
|
||||
const creator = {
|
||||
$type: 'app.bsky.actor.defs#profileViewBasic',
|
||||
did: 'did:plc:abc',
|
||||
handle: 'alice.test',
|
||||
}
|
||||
|
||||
const basicView = {
|
||||
$type: 'app.bsky.graph.defs#starterPackViewBasic',
|
||||
uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123',
|
||||
cid: 'bafypack',
|
||||
record: {},
|
||||
creator,
|
||||
indexedAt: now(),
|
||||
}
|
||||
|
||||
const fullView = {
|
||||
...basicView,
|
||||
$type: 'app.bsky.graph.defs#starterPackView',
|
||||
}
|
||||
|
||||
/*
|
||||
* Type-level assertions for the dual-world widening: the alias must accept a
|
||||
* starter pack view from either world, because both have live producers. These
|
||||
* are compile-time only - a failure surfaces as a typecheck error.
|
||||
*/
|
||||
type Assignable<From, To> = From extends To ? true : false
|
||||
type Expect<T extends true> = T
|
||||
|
||||
type _AcceptsNewBasicView = Expect<
|
||||
Assignable<app.bsky.graph.defs.StarterPackViewBasic, AnyStarterPackView>
|
||||
>
|
||||
type _AcceptsNewFullView = Expect<
|
||||
Assignable<app.bsky.graph.defs.StarterPackView, AnyStarterPackView>
|
||||
>
|
||||
type _AcceptsOldBasicView = Expect<
|
||||
Assignable<AppBskyGraphDefs.StarterPackViewBasic, AnyStarterPackView>
|
||||
>
|
||||
type _AcceptsOldFullView = Expect<
|
||||
Assignable<AppBskyGraphDefs.StarterPackView, AnyStarterPackView>
|
||||
>
|
||||
|
||||
describe('types/bsky/starterPack guards', () => {
|
||||
describe('isBasicView', () => {
|
||||
it('accepts a basic view', () => {
|
||||
expect(isBasicView(basicView)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects the full view', () => {
|
||||
expect(isBasicView(fullView)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a missing or absent $type', () => {
|
||||
expect(isBasicView({uri: 'at://x', cid: 'y'})).toBe(false)
|
||||
expect(isBasicView(null)).toBe(false)
|
||||
expect(isBasicView(undefined)).toBe(false)
|
||||
expect(isBasicView('string')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isView', () => {
|
||||
it('accepts the full view', () => {
|
||||
expect(isView(fullView)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects the basic view', () => {
|
||||
expect(isView(basicView)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a missing or absent $type', () => {
|
||||
expect(isView({uri: 'at://x', cid: 'y'})).toBe(false)
|
||||
expect(isView(null)).toBe(false)
|
||||
expect(isView(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('narrows a view from either world to a readable shape', () => {
|
||||
/*
|
||||
* The `$type` string is world-independent, so one guard narrows values from
|
||||
* both producers; the narrowed union stays structurally readable.
|
||||
*/
|
||||
for (const view of [basicView, fullView]) {
|
||||
if (isBasicView(view) || isView(view)) {
|
||||
expect(typeof view.uri).toBe('string')
|
||||
expect(typeof view.creator.did).toBe('string')
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
+94
-38
@@ -1,64 +1,110 @@
|
||||
import {
|
||||
type $Typed,
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedGallery,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyLabelerDefs,
|
||||
type $Typed as $TypedApi,
|
||||
type AppBskyEmbedExternal,
|
||||
type AppBskyEmbedGallery,
|
||||
type AppBskyEmbedImages,
|
||||
type AppBskyEmbedRecord,
|
||||
type AppBskyEmbedVideo,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyGraphDefs,
|
||||
type AppBskyLabelerDefs,
|
||||
} from '@atproto/api'
|
||||
import {type $Typed} from '@atproto/lex'
|
||||
|
||||
import {app} from '#/lexicons'
|
||||
import {isType} from '#/types/bsky'
|
||||
|
||||
/*
|
||||
* Each `view` slot below accepts both the generated `#/lexicons` view and the
|
||||
* `@atproto/api` view of the same def, because both worlds have live producers:
|
||||
* `parseEmbed` narrows with the `#/lexicons` schemas and so returns new-world
|
||||
* views, while call sites that build an `Embed` by hand still pass views
|
||||
* produced through the `@atproto/api` agent.
|
||||
*
|
||||
* The two worlds share the same `$type` strings, so the guards in this file
|
||||
* narrow a value from either producer; only the static type differs.
|
||||
*
|
||||
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
|
||||
*/
|
||||
export type Embed =
|
||||
| {
|
||||
type: 'post'
|
||||
view: $Typed<AppBskyEmbedRecord.ViewRecord>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.record.ViewRecord>
|
||||
| $TypedApi<AppBskyEmbedRecord.ViewRecord>
|
||||
}
|
||||
| {
|
||||
type: 'post_not_found'
|
||||
view: $Typed<AppBskyEmbedRecord.ViewNotFound>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.record.ViewNotFound>
|
||||
| $TypedApi<AppBskyEmbedRecord.ViewNotFound>
|
||||
}
|
||||
| {
|
||||
type: 'post_blocked'
|
||||
view: $Typed<AppBskyEmbedRecord.ViewBlocked>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.record.ViewBlocked>
|
||||
| $TypedApi<AppBskyEmbedRecord.ViewBlocked>
|
||||
}
|
||||
| {
|
||||
type: 'post_detached'
|
||||
view: $Typed<AppBskyEmbedRecord.ViewDetached>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.record.ViewDetached>
|
||||
| $TypedApi<AppBskyEmbedRecord.ViewDetached>
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
view: $Typed<AppBskyFeedDefs.GeneratorView>
|
||||
view:
|
||||
| $Typed<app.bsky.feed.defs.GeneratorView>
|
||||
| $TypedApi<AppBskyFeedDefs.GeneratorView>
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
view: $Typed<AppBskyGraphDefs.ListView>
|
||||
view:
|
||||
| $Typed<app.bsky.graph.defs.ListView>
|
||||
| $TypedApi<AppBskyGraphDefs.ListView>
|
||||
}
|
||||
| {
|
||||
type: 'labeler'
|
||||
view: $Typed<AppBskyLabelerDefs.LabelerView>
|
||||
view:
|
||||
| $Typed<app.bsky.labeler.defs.LabelerView>
|
||||
| $TypedApi<AppBskyLabelerDefs.LabelerView>
|
||||
}
|
||||
| {
|
||||
type: 'starter_pack'
|
||||
view: $Typed<AppBskyGraphDefs.StarterPackViewBasic>
|
||||
view:
|
||||
| $Typed<app.bsky.graph.defs.StarterPackViewBasic>
|
||||
| $TypedApi<AppBskyGraphDefs.StarterPackViewBasic>
|
||||
}
|
||||
| {
|
||||
type: 'images'
|
||||
view: $Typed<AppBskyEmbedImages.View>
|
||||
/*
|
||||
* Only the `@atproto/api` view, unlike the other arms: the ImageEmbed
|
||||
* consumer reads `view.images` directly, and the `#/lexicons` view is
|
||||
* assignable to this slot, so `parseEmbed`'s new-world value flows in
|
||||
* while the consumer keeps a single structural shape to read from.
|
||||
*/
|
||||
view: $TypedApi<AppBskyEmbedImages.View>
|
||||
}
|
||||
| {
|
||||
type: 'gallery'
|
||||
view: $Typed<AppBskyEmbedGallery.View>
|
||||
/*
|
||||
* Old-world only for the same reason as the `images` arm above: the
|
||||
* consumer narrows `view.items` with `AppBskyEmbedGallery.isViewImage`,
|
||||
* which cannot narrow the new view's `Unknown$TypedObject` arm.
|
||||
*/
|
||||
view: $TypedApi<AppBskyEmbedGallery.View>
|
||||
}
|
||||
| {
|
||||
type: 'link'
|
||||
view: $Typed<AppBskyEmbedExternal.View>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.external.View>
|
||||
| $TypedApi<AppBskyEmbedExternal.View>
|
||||
}
|
||||
| {
|
||||
type: 'video'
|
||||
view: $Typed<AppBskyEmbedVideo.View>
|
||||
view:
|
||||
| $Typed<app.bsky.embed.video.View>
|
||||
| $TypedApi<AppBskyEmbedVideo.View>
|
||||
}
|
||||
| {
|
||||
type: 'post_with_media'
|
||||
@@ -72,43 +118,45 @@ export type Embed =
|
||||
|
||||
export type EmbedType<T extends Embed['type']> = Extract<Embed, {type: T}>
|
||||
|
||||
export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
|
||||
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||
export function parseEmbedRecordView({
|
||||
record,
|
||||
}: app.bsky.embed.record.View): Embed {
|
||||
if (isType(app.bsky.embed.record.viewRecord, record)) {
|
||||
return {
|
||||
type: 'post',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(record)) {
|
||||
} else if (isType(app.bsky.embed.record.viewNotFound, record)) {
|
||||
return {
|
||||
type: 'post_not_found',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(record)) {
|
||||
} else if (isType(app.bsky.embed.record.viewBlocked, record)) {
|
||||
return {
|
||||
type: 'post_blocked',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewDetached(record)) {
|
||||
} else if (isType(app.bsky.embed.record.viewDetached, record)) {
|
||||
return {
|
||||
type: 'post_detached',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isGeneratorView(record)) {
|
||||
} else if (isType(app.bsky.feed.defs.generatorView, record)) {
|
||||
return {
|
||||
type: 'feed',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isListView(record)) {
|
||||
} else if (isType(app.bsky.graph.defs.listView, record)) {
|
||||
return {
|
||||
type: 'list',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyLabelerDefs.isLabelerView(record)) {
|
||||
} else if (isType(app.bsky.labeler.defs.labelerView, record)) {
|
||||
return {
|
||||
type: 'labeler',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
|
||||
} else if (isType(app.bsky.graph.defs.starterPackViewBasic, record)) {
|
||||
return {
|
||||
type: 'starter_pack',
|
||||
view: record,
|
||||
@@ -121,30 +169,38 @@ export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
export function parseEmbed(
|
||||
/*
|
||||
* Accepts a `PostView.embed` from either world; the `#/lexicons` guards below
|
||||
* narrow on `$type`, which is world-independent.
|
||||
*/
|
||||
embed:
|
||||
| app.bsky.feed.defs.PostView['embed']
|
||||
| AppBskyFeedDefs.PostView['embed'],
|
||||
): Embed {
|
||||
if (isType(app.bsky.embed.images.view, embed)) {
|
||||
return {
|
||||
type: 'images',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedGallery.isView(embed)) {
|
||||
} else if (isType(app.bsky.embed.gallery.view, embed)) {
|
||||
return {
|
||||
type: 'gallery',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedExternal.isView(embed)) {
|
||||
} else if (isType(app.bsky.embed.external.view, embed)) {
|
||||
return {
|
||||
type: 'link',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedVideo.isView(embed)) {
|
||||
} else if (isType(app.bsky.embed.video.view, embed)) {
|
||||
return {
|
||||
type: 'video',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isView(embed)) {
|
||||
} else if (isType(app.bsky.embed.record.view, embed)) {
|
||||
return parseEmbedRecordView(embed)
|
||||
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
} else if (isType(app.bsky.embed.recordWithMedia.view, embed)) {
|
||||
return {
|
||||
type: 'post_with_media',
|
||||
view: parseEmbedRecordView(embed.record),
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {type app, type chat} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* Matches any profile view exported by our SDK
|
||||
* Matches any profile view exported by our SDK, in either world.
|
||||
*
|
||||
* Both the generated `#/lexicons` views and the `@atproto/api` views are
|
||||
* accepted because both are live producers: queries migrated to the lexicon
|
||||
* client emit the former, unmigrated ones emit the latter, and consumers of
|
||||
* this alias take profiles from both.
|
||||
*
|
||||
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
|
||||
*/
|
||||
export type AnyProfileView =
|
||||
| app.bsky.actor.defs.ProfileViewBasic
|
||||
| app.bsky.actor.defs.ProfileView
|
||||
| app.bsky.actor.defs.ProfileViewDetailed
|
||||
| chat.bsky.actor.defs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileView
|
||||
| AppBskyActorDefs.ProfileViewDetailed
|
||||
|
||||
@@ -1,11 +1,52 @@
|
||||
import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
|
||||
export const isBasicView = AppBskyGraphDefs.isStarterPackViewBasic
|
||||
export const isView = AppBskyGraphDefs.isStarterPackView
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
/*
|
||||
* `$type`-only guards for starter pack views. They compare against the
|
||||
* `#/lexicons` schema's `$type` string rather than delegating to the schema's
|
||||
* `isTypeOf` (which treats a missing `$type` as a match), matching the
|
||||
* present-and-equal semantics of the old `@atproto/api`
|
||||
* `AppBskyGraphDefs.isStarterPackView*` helpers.
|
||||
*
|
||||
* The `$type` string is identical in both worlds, so a single check narrows a
|
||||
* value from either producer; the narrowed type is the union of both worlds'
|
||||
* views for the same reason {@link AnyStarterPackView} is.
|
||||
*/
|
||||
export function isBasicView(
|
||||
v: unknown,
|
||||
): v is
|
||||
| app.bsky.graph.defs.StarterPackViewBasic
|
||||
| AppBskyGraphDefs.StarterPackViewBasic {
|
||||
return (
|
||||
v != null &&
|
||||
typeof v === 'object' &&
|
||||
(v as {$type?: unknown}).$type ===
|
||||
app.bsky.graph.defs.starterPackViewBasic.$type
|
||||
)
|
||||
}
|
||||
|
||||
export function isView(
|
||||
v: unknown,
|
||||
): v is app.bsky.graph.defs.StarterPackView | AppBskyGraphDefs.StarterPackView {
|
||||
return (
|
||||
v != null &&
|
||||
typeof v === 'object' &&
|
||||
(v as {$type?: unknown}).$type === app.bsky.graph.defs.starterPackView.$type
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches any starter pack view exported by our SDK
|
||||
* Matches any starter pack view exported by our SDK, in either world.
|
||||
*
|
||||
* Both the generated `#/lexicons` views and the `@atproto/api` views are
|
||||
* accepted because both are live producers: queries migrated to the lexicon
|
||||
* client emit the former, unmigrated ones emit the latter.
|
||||
*
|
||||
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
|
||||
*/
|
||||
export type AnyStarterPackView =
|
||||
| app.bsky.graph.defs.StarterPackViewBasic
|
||||
| app.bsky.graph.defs.StarterPackView
|
||||
| AppBskyGraphDefs.StarterPackViewBasic
|
||||
| AppBskyGraphDefs.StarterPackView
|
||||
|
||||
Reference in New Issue
Block a user