import { Cause, DateTime, Exit, Option, Result, Schema } from "effect"; import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; export interface NormalizedGitHubPullRequestRecord { readonly number: number; readonly title: string; readonly url: string; readonly baseRefName: string; readonly headRefName: string; readonly state: "open" | "closed" | "merged"; readonly updatedAt: Option.Option; readonly isCrossRepository?: boolean; readonly headRepositoryNameWithOwner?: string | null; readonly headRepositoryOwnerLogin?: string | null; } const GitHubPullRequestSchema = Schema.Struct({ number: PositiveInt, title: TrimmedNonEmptyString, url: TrimmedNonEmptyString, baseRefName: TrimmedNonEmptyString, headRefName: TrimmedNonEmptyString, state: Schema.optional(Schema.NullOr(Schema.String)), mergedAt: Schema.optional(Schema.NullOr(Schema.String)), updatedAt: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)), isCrossRepository: Schema.optional(Schema.Boolean), headRepository: Schema.optional( Schema.NullOr( Schema.Struct({ nameWithOwner: Schema.String, }), ), ), headRepositoryOwner: Schema.optional( Schema.NullOr( Schema.Struct({ login: Schema.String, }), ), ), }); function trimOptionalString(value: string | null | undefined): string | null { const trimmed = value?.trim() ?? ""; return trimmed.length > 0 ? trimmed : null; } function normalizeGitHubPullRequestState(input: { state?: string | null | undefined; mergedAt?: string | null | undefined; }): "open" | "closed" | "merged" { const normalizedState = input.state?.trim().toUpperCase(); if ( (typeof input.mergedAt === "string" && input.mergedAt.trim().length > 0) || normalizedState === "MERGED" ) { return "merged"; } if (normalizedState === "CLOSED") { return "closed"; } return "open"; } function normalizeGitHubPullRequestRecord( raw: Schema.Schema.Type, ): NormalizedGitHubPullRequestRecord { const headRepositoryNameWithOwner = trimOptionalString(raw.headRepository?.nameWithOwner); const headRepositoryOwnerLogin = trimOptionalString(raw.headRepositoryOwner?.login) ?? (typeof headRepositoryNameWithOwner === "string" && headRepositoryNameWithOwner.includes("/") ? (headRepositoryNameWithOwner.split("/")[0] ?? null) : null); return { number: raw.number, title: raw.title, url: raw.url, baseRefName: raw.baseRefName, headRefName: raw.headRefName, state: normalizeGitHubPullRequestState(raw), updatedAt: raw.updatedAt ?? Option.none(), ...(typeof raw.isCrossRepository === "boolean" ? { isCrossRepository: raw.isCrossRepository } : {}), ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}), ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}), }; } const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown)); const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema); const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema); export const formatGitHubJsonDecodeError = formatSchemaError; export function decodeGitHubPullRequestListJson( raw: string, ): Result.Result< ReadonlyArray, Cause.Cause > { const result = decodeGitHubPullRequestList(raw); if (Result.isSuccess(result)) { const pullRequests: NormalizedGitHubPullRequestRecord[] = []; for (const entry of result.success) { const decodedEntry = decodeGitHubPullRequestEntry(entry); if (Exit.isFailure(decodedEntry)) { continue; } pullRequests.push(normalizeGitHubPullRequestRecord(decodedEntry.value)); } return Result.succeed(pullRequests); } return Result.fail(result.failure); } export function decodeGitHubPullRequestJson( raw: string, ): Result.Result> { const result = decodeGitHubPullRequest(raw); if (Result.isSuccess(result)) { return Result.succeed(normalizeGitHubPullRequestRecord(result.success)); } return Result.fail(result.failure); }