diff --git a/src/lib/tangled/repo/getIssue.ts b/src/lib/tangled/repo/getIssue.ts new file mode 100644 index 0000000..882c1b2 --- /dev/null +++ b/src/lib/tangled/repo/getIssue.ts @@ -0,0 +1,15 @@ +import { ok } from '@atcute/client' +import type { ResourceUri } from '@atcute/lexicons' +import { rpc } from '../client' +import type { IssueRecord } from './types' +import { validateIssueRecord } from './validators' + +export async function getIssue(issue: ResourceUri): Promise { + const response = await ok( + rpc.get('sh.tangled.repo.getIssue', { + params: { issue }, + }), + ) + + return validateIssueRecord(response) +} diff --git a/src/lib/tangled/repo/getRepo.ts b/src/lib/tangled/repo/getRepo.ts index fe66755..94448c2 100644 --- a/src/lib/tangled/repo/getRepo.ts +++ b/src/lib/tangled/repo/getRepo.ts @@ -1,7 +1,8 @@ import { ok } from '@atcute/client' import type { ResourceUri } from '@atcute/lexicons' import { rpc } from '../client' -import { validateRepo, type Repo } from './types' +import type { Repo } from './types' +import { validateRepo } from './validators' export async function getRepo(atUri: ResourceUri): Promise { const repo = await ok( diff --git a/src/lib/tangled/repo/getRepoByRepoDid.ts b/src/lib/tangled/repo/getRepoByRepoDid.ts index cb502c3..444862e 100644 --- a/src/lib/tangled/repo/getRepoByRepoDid.ts +++ b/src/lib/tangled/repo/getRepoByRepoDid.ts @@ -1,6 +1,7 @@ import type { Did } from '@atcute/lexicons' import { rpc } from '../client' -import { validateRepo, type Repo } from './types' +import type { Repo } from './types' +import { validateRepo } from './validators' export async function getRepoByRepoDid(did: Did): Promise { const response = await rpc.get('sh.tangled.repo.getRepoByRepoDid', { diff --git a/src/lib/tangled/repo/index.ts b/src/lib/tangled/repo/index.ts index 4df9535..0181fbd 100644 --- a/src/lib/tangled/repo/index.ts +++ b/src/lib/tangled/repo/index.ts @@ -1,4 +1,5 @@ export { getDefaultBranch } from './getDefaultBranch' +export { getIssue } from './getIssue' export { isDirectoryMode } from './isDirectoryMode' export { getRecentCommits } from './getRecentCommits' export type { RecentCommitsOptions, RepoCommit } from './getRecentCommits' @@ -8,9 +9,19 @@ export { getRepoLog } from './getRepoLog' export type { RepoLogOptions } from './getRepoLog' export { getRepoTree } from './getRepoTree' export { listIssues } from './listIssues' -export type { Issue, IssueList, ListIssuesOptions } from './listIssues' +export type { ListIssuesOptions } from './listIssues' export { listPulls } from './listPulls' -export type { ListPullsOptions, Pull, PullList } from './listPulls' +export type { ListPullsOptions } from './listPulls' export { listRepos } from './listRepos' -export { getRepoName, getRepoRkey } from './types' -export type { Repo, RepoList } from './types' +export { getRepoName, getRepoRkey } from './repoIdentity' +export type { + Issue, + IssueList, + IssueRecord, + Pull, + PullList, + PullRecord, + Repo, + RepoList, + TangledRecord, +} from './types' diff --git a/src/lib/tangled/repo/listIssues.ts b/src/lib/tangled/repo/listIssues.ts index 6b9f9ec..52ffae3 100644 --- a/src/lib/tangled/repo/listIssues.ts +++ b/src/lib/tangled/repo/listIssues.ts @@ -1,21 +1,11 @@ import { ok } from '@atcute/client' import type { Did } from '@atcute/lexicons' import { safeParse } from '@atcute/lexicons' -import type { Main as TangledIssue } from '@atcute/tangled/types/repo/issue' -import { mainSchema as issueSchema } from '@atcute/tangled/types/repo/issue' -import type { $output as IssueListResponse } from '@atcute/tangled/types/repo/listIssues' import { mainSchema as issueListSchema } from '@atcute/tangled/types/repo/listIssues' import { rpc } from '../client' import { removeNullCursor } from '../utils' - -export type Issue = Omit & { - value: TangledIssue -} - -export type IssueList = { - items: Issue[] - cursor?: string -} +import type { IssueList } from './types' +import { validateIssue } from './validators' export type ListIssuesOptions = { cursor?: string @@ -45,16 +35,7 @@ export async function listIssues( throw new Error(`Bobbin returned an invalid issue list: ${listValidation.message}`) } - const items = listValidation.value.items.map((item, index) => { - const valueValidation = safeParse(issueSchema, item.value) - if (!valueValidation.ok) { - throw new Error( - `Bobbin returned an invalid issue record at index ${index}: ${valueValidation.message}`, - ) - } - - return { ...item, value: valueValidation.value } - }) + const items = listValidation.value.items.map(validateIssue) return { items, cursor: listValidation.value.cursor } } diff --git a/src/lib/tangled/repo/listPulls.ts b/src/lib/tangled/repo/listPulls.ts index f65d1f0..a19cbb6 100644 --- a/src/lib/tangled/repo/listPulls.ts +++ b/src/lib/tangled/repo/listPulls.ts @@ -1,21 +1,11 @@ import { ok } from '@atcute/client' import type { Did } from '@atcute/lexicons' import { safeParse } from '@atcute/lexicons' -import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull' -import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull' -import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls' import { mainSchema as pullListSchema } from '@atcute/tangled/types/repo/listPulls' import { rpc } from '../client' import { removeNullCursor } from '../utils' - -export type Pull = Omit & { - value: TangledPull -} - -export type PullList = { - items: Pull[] - cursor?: string -} +import type { PullList } from './types' +import { validatePull } from './validators' export type ListPullsOptions = { cursor?: string @@ -42,16 +32,7 @@ export async function listPulls(repoDid: Did, options: ListPullsOptions = {}): P throw new Error(`Bobbin returned an invalid pull list: ${listValidation.message}`) } - const items = listValidation.value.items.map((item, index) => { - const valueValidation = safeParse(pullSchema, item.value) - if (!valueValidation.ok) { - throw new Error( - `Bobbin returned an invalid pull record at index ${index}: ${valueValidation.message}`, - ) - } - - return { ...item, value: valueValidation.value } - }) + const items = listValidation.value.items.map(validatePull) return { items, cursor: listValidation.value.cursor } } diff --git a/src/lib/tangled/repo/listRepos.ts b/src/lib/tangled/repo/listRepos.ts index 73134c4..ddd0e89 100644 --- a/src/lib/tangled/repo/listRepos.ts +++ b/src/lib/tangled/repo/listRepos.ts @@ -4,7 +4,9 @@ import { safeParse } from '@atcute/lexicons' import { mainSchema as reposListSchema } from '@atcute/tangled/types/repo/listRepos' import { rpc } from '../client' import { removeNullCursor } from '../utils' -import { getRepoRkey, validateRepo, type Repo, type RepoList } from './types' +import { getRepoRkey } from './repoIdentity' +import type { Repo, RepoList } from './types' +import { validateRepo } from './validators' export async function listRepos(did: Did): Promise { const response = await ok( diff --git a/src/lib/tangled/repo/repoIdentity.ts b/src/lib/tangled/repo/repoIdentity.ts new file mode 100644 index 0000000..06fccd3 --- /dev/null +++ b/src/lib/tangled/repo/repoIdentity.ts @@ -0,0 +1,15 @@ +import { parseResourceUri } from '@atcute/lexicons' +import type { Repo } from './types' + +export function getRepoRkey(repo: Repo): string { + const { rkey } = parseResourceUri(repo.uri) + if (rkey === undefined) { + throw new Error(`Repository URI has no record key: ${repo.uri}`) + } + + return rkey +} + +export function getRepoName(repo: Repo): string { + return repo.value.name ?? getRepoRkey(repo) +} diff --git a/src/lib/tangled/repo/types.ts b/src/lib/tangled/repo/types.ts index d254049..c02d0f9 100644 --- a/src/lib/tangled/repo/types.ts +++ b/src/lib/tangled/repo/types.ts @@ -1,61 +1,41 @@ import type { Cid, ResourceUri } from '@atcute/lexicons' -import { parseResourceUri, safeParse } from '@atcute/lexicons' import type { Main as TangledRepo } from '@atcute/tangled/types/repo' -import { mainSchema as repoSchema } from '@atcute/tangled/types/repo' +import type { Main as TangledIssue } from '@atcute/tangled/types/repo/issue' +import type { $output as IssueListResponse } from '@atcute/tangled/types/repo/listIssues' +import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull' +import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls' -export type Repo = { +export type TangledRecord = { cid?: Cid uri: ResourceUri - value: TangledRepo + value: TValue } -export type RepoList = { - items: Repo[] - cursor?: string -} +export type Repo = TangledRecord -export function getRepoRkey(repo: Repo): string { - const { rkey } = parseResourceUri(repo.uri) - if (rkey === undefined) { - throw new Error(`Repository URI has no record key: ${repo.uri}`) - } +export type IssueRecord = TangledRecord - return rkey +export type Issue = Omit & { + value: TangledIssue } -export function getRepoName(repo: Repo): string { - return repo.value.name ?? getRepoRkey(repo) -} - -export function validateRepo(repo: { cid?: Cid; uri: ResourceUri; value: unknown }): Repo { - const validation = safeParse(repoSchema, normalizeRepoRecord(repo.value)) - if (!validation.ok) { - throw new Error(`Bobbin returned an invalid repo record: ${repo.uri}: ${validation.message}`) - } +export type PullRecord = TangledRecord - return { ...repo, value: validation.value } +export type Pull = Omit & { + value: TangledPull } -function normalizeRepoRecord(value: unknown): unknown { - if (!isRecord(value)) return value - - const normalized = { ...value } - if (typeof normalized.description === 'string') { - const graphemes = Array.from( - new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(normalized.description), - ({ segment }) => segment, - ) - - if (graphemes.length === 0) { - delete normalized.description - } else if (graphemes.length > 140) { - normalized.description = graphemes.slice(0, 140).join('') - } - } +export type RepoList = { + items: Repo[] + cursor?: string +} - return normalized +export type IssueList = { + items: Issue[] + cursor?: string } -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) +export type PullList = { + items: Pull[] + cursor?: string } diff --git a/src/lib/tangled/repo/validators.ts b/src/lib/tangled/repo/validators.ts new file mode 100644 index 0000000..4ef466a --- /dev/null +++ b/src/lib/tangled/repo/validators.ts @@ -0,0 +1,64 @@ +import { safeParse } from '@atcute/lexicons' +import { mainSchema as repoSchema } from '@atcute/tangled/types/repo' +import { mainSchema as issueSchema } from '@atcute/tangled/types/repo/issue' +import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull' +import type { Issue, IssueRecord, Pull, PullRecord, Repo, TangledRecord } from './types' + +export function validateRepo(record: TangledRecord): Repo { + const validation = safeParse(repoSchema, normalizeRepoRecord(record.value)) + if (!validation.ok) { + throw new Error(`Bobbin returned an invalid repo record: ${record.uri}: ${validation.message}`) + } + + return { ...record, value: validation.value } +} + +export function validateIssueRecord(record: TangledRecord): IssueRecord { + const validation = safeParse(issueSchema, record.value) + if (!validation.ok) { + throw new Error(`Bobbin returned an invalid issue record: ${record.uri}: ${validation.message}`) + } + + return { ...record, value: validation.value } +} + +export function validateIssue(item: Omit & TangledRecord): Issue { + return { ...item, value: validateIssueRecord(item).value } +} + +export function validatePullRecord(record: TangledRecord): PullRecord { + const validation = safeParse(pullSchema, record.value) + if (!validation.ok) { + throw new Error(`Bobbin returned an invalid pull record: ${record.uri}: ${validation.message}`) + } + + return { ...record, value: validation.value } +} + +export function validatePull(item: Omit & TangledRecord): Pull { + return { ...item, value: validatePullRecord(item).value } +} + +function normalizeRepoRecord(value: unknown): unknown { + if (!isRecord(value)) return value + + const normalized = { ...value } + if (typeof normalized.description === 'string') { + const graphemes = Array.from( + new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(normalized.description), + ({ segment }) => segment, + ) + + if (graphemes.length === 0) { + delete normalized.description + } else if (graphemes.length > 140) { + normalized.description = graphemes.slice(0, 140).join('') + } + } + + return normalized +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +}