1import * as DateTime from "effect/DateTime";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
5
6export interface NormalizedBitbucketPullRequestRecord {
7 readonly number: number;
8 readonly title: string;
9 readonly url: string;
10 readonly baseRefName: string;
11 readonly headRefName: string;
12 readonly state: "open" | "closed" | "merged";
13 readonly isDraft?: boolean;
14 readonly updatedAt: Option.Option<DateTime.Utc>;
15 readonly isCrossRepository?: boolean;
16 readonly headRepositoryNameWithOwner?: string | null;
17 readonly headRepositoryOwnerLogin?: string | null;
18}
19
20export const BitbucketRepositoryRefSchema = Schema.Struct({
21 full_name: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
22 workspace: Schema.optional(
23 Schema.NullOr(
24 Schema.Struct({
25 slug: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
26 }),
27 ),
28 ),
29});
30
31export const BitbucketPullRequestBranchSchema = Schema.Struct({
32 repository: Schema.optional(Schema.NullOr(BitbucketRepositoryRefSchema)),
33 branch: Schema.Struct({
34 name: TrimmedNonEmptyString,
35 }),
36});
37
38export const BitbucketPullRequestSchema = Schema.Struct({
39 id: PositiveInt,
40 title: TrimmedNonEmptyString,
41 state: Schema.optional(Schema.NullOr(Schema.String)),
42 draft: Schema.optional(Schema.Boolean),
43 updated_on: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)),
44 links: Schema.Struct({
45 html: Schema.Struct({
46 href: TrimmedNonEmptyString,
47 }),
48 }),
49 source: BitbucketPullRequestBranchSchema,
50 destination: BitbucketPullRequestBranchSchema,
51});
52
53export const BitbucketPullRequestListSchema = Schema.Struct({
54 values: Schema.Array(BitbucketPullRequestSchema),
55 next: Schema.optional(TrimmedNonEmptyString),
56});
57
58function trimOptionalString(value: string | null | undefined): string | null {
59 const trimmed = value?.trim() ?? "";
60 return trimmed.length > 0 ? trimmed : null;
61}
62
63function repositoryOwner(repository: Schema.Schema.Type<typeof BitbucketRepositoryRefSchema>) {
64 return (
65 trimOptionalString(repository.workspace?.slug) ??
66 (repository.full_name?.includes("/") ? (repository.full_name.split("/")[0] ?? null) : null)
67 );
68}
69
70function normalizeBitbucketPullRequestState(state: string | null | undefined) {
71 switch (state?.trim().toUpperCase()) {
72 case "MERGED":
73 return "merged" as const;
74 case "DECLINED":
75 case "SUPERSEDED":
76 return "closed" as const;
77 case "OPEN":
78 default:
79 return "open" as const;
80 }
81}
82
83export function normalizeBitbucketPullRequestRecord(
84 raw: Schema.Schema.Type<typeof BitbucketPullRequestSchema>,
85): NormalizedBitbucketPullRequestRecord {
86 const headRepositoryNameWithOwner = trimOptionalString(raw.source.repository?.full_name);
87 const baseRepositoryNameWithOwner = trimOptionalString(raw.destination.repository?.full_name);
88 const headRepositoryOwnerLogin = raw.source.repository
89 ? repositoryOwner(raw.source.repository)
90 : null;
91 const isCrossRepository =
92 headRepositoryNameWithOwner !== null &&
93 baseRepositoryNameWithOwner !== null &&
94 headRepositoryNameWithOwner !== baseRepositoryNameWithOwner;
95
96 return {
97 number: raw.id,
98 title: raw.title,
99 url: raw.links.html.href,
100 baseRefName: raw.destination.branch.name,
101 headRefName: raw.source.branch.name,
102 state: normalizeBitbucketPullRequestState(raw.state),
103 ...(raw.draft === true ? { isDraft: true } : {}),
104 updatedAt: raw.updated_on ?? Option.none(),
105 ...(isCrossRepository ? { isCrossRepository: true } : {}),
106 ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}),
107 ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),
108 };
109}