import * as Cause from "effect/Cause"; import * as DateTime from "effect/DateTime"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; export interface NormalizedGitLabMergeRequestRecord { readonly number: number; readonly title: string; readonly url: string; readonly baseRefName: string; readonly headRefName: string; readonly state: "open" | "closed" | "merged"; readonly isDraft?: boolean; readonly updatedAt: Option.Option; readonly isCrossRepository?: boolean; readonly headRepositoryNameWithOwner?: string | null; readonly headRepositoryOwnerLogin?: string | null; } const GitLabProjectReferenceSchema = Schema.Struct({ path_with_namespace: Schema.optional(Schema.String), pathWithNamespace: Schema.optional(Schema.String), namespace: Schema.optional( Schema.NullOr( Schema.Struct({ path: Schema.optional(Schema.String), full_path: Schema.optional(Schema.String), fullPath: Schema.optional(Schema.String), }), ), ), }); const GitLabMergeRequestSchema = Schema.Struct({ iid: PositiveInt, title: TrimmedNonEmptyString, web_url: TrimmedNonEmptyString, source_branch: TrimmedNonEmptyString, target_branch: TrimmedNonEmptyString, state: Schema.optional(Schema.NullOr(Schema.String)), draft: Schema.optional(Schema.Boolean), work_in_progress: Schema.optional(Schema.Boolean), updated_at: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)), source_project_id: Schema.optional(Schema.NullOr(Schema.Number)), target_project_id: Schema.optional(Schema.NullOr(Schema.Number)), source_project: Schema.optional(Schema.NullOr(GitLabProjectReferenceSchema)), target_project: Schema.optional(Schema.NullOr(GitLabProjectReferenceSchema)), }); function trimOptionalString(value: string | null | undefined): string | null { const trimmed = value?.trim() ?? ""; return trimmed.length > 0 ? trimmed : null; } function normalizeGitLabMergeRequestState( state: string | null | undefined, ): "open" | "closed" | "merged" { const normalized = state?.trim().toLowerCase(); if (normalized === "merged") { return "merged"; } if (normalized === "closed") { return "closed"; } return "open"; } function projectPathWithNamespace( project: Schema.Schema.Type | null | undefined, ): string | null { const explicit = trimOptionalString(project?.path_with_namespace) ?? trimOptionalString(project?.pathWithNamespace); if (explicit) { return explicit; } const namespacePath = trimOptionalString(project?.namespace?.full_path) ?? trimOptionalString(project?.namespace?.fullPath) ?? trimOptionalString(project?.namespace?.path); return namespacePath; } function ownerLoginFromPathWithNamespace(pathWithNamespace: string | null): string | null { const [owner] = pathWithNamespace?.split("/") ?? []; return trimOptionalString(owner); } function normalizeGitLabMergeRequestRecord( raw: Schema.Schema.Type, ): NormalizedGitLabMergeRequestRecord { const sourceProjectPath = projectPathWithNamespace(raw.source_project); const targetProjectPath = projectPathWithNamespace(raw.target_project); const isCrossRepository = typeof raw.source_project_id === "number" && typeof raw.target_project_id === "number" ? raw.source_project_id !== raw.target_project_id : sourceProjectPath !== null && targetProjectPath !== null ? sourceProjectPath.toLowerCase() !== targetProjectPath.toLowerCase() : undefined; const headRepositoryOwnerLogin = ownerLoginFromPathWithNamespace(sourceProjectPath); return { number: raw.iid, title: raw.title, url: raw.web_url, baseRefName: raw.target_branch, headRefName: raw.source_branch, state: normalizeGitLabMergeRequestState(raw.state), ...(raw.draft === true || raw.work_in_progress === true ? { isDraft: true } : {}), updatedAt: raw.updated_at ?? Option.none(), ...(typeof isCrossRepository === "boolean" ? { isCrossRepository } : {}), ...(sourceProjectPath ? { headRepositoryNameWithOwner: sourceProjectPath } : {}), ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}), }; } const decodeGitLabMergeRequestList = decodeJsonResult(Schema.Array(Schema.Unknown)); const decodeGitLabMergeRequest = decodeJsonResult(GitLabMergeRequestSchema); const decodeGitLabMergeRequestEntry = Schema.decodeUnknownExit(GitLabMergeRequestSchema); export const formatGitLabJsonDecodeError = formatSchemaError; export function decodeGitLabMergeRequestListJson( raw: string, ): Result.Result< ReadonlyArray, Cause.Cause > { const result = decodeGitLabMergeRequestList(raw); if (Result.isSuccess(result)) { const mergeRequests: NormalizedGitLabMergeRequestRecord[] = []; for (const entry of result.success) { const decodedEntry = decodeGitLabMergeRequestEntry(entry); if (Exit.isFailure(decodedEntry)) { continue; } mergeRequests.push(normalizeGitLabMergeRequestRecord(decodedEntry.value)); } return Result.succeed(mergeRequests); } return Result.fail(result.failure); } export function decodeGitLabMergeRequestJson( raw: string, ): Result.Result> { const result = decodeGitLabMergeRequest(raw); if (Result.isSuccess(result)) { return Result.succeed(normalizeGitLabMergeRequestRecord(result.success)); } return Result.fail(result.failure); }