Skip to content

Commit 3613ef7

Browse files
committed
refactor: Introduce plugin API module
1 parent 9816d02 commit 3613ef7

File tree

2 files changed

+366
-0
lines changed

2 files changed

+366
-0
lines changed

src/api/errors.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum PluginApiError {
2+
ImportFailed = "Import failed",
3+
ProcessingFailed = "Processing failed",
4+
}

src/api/index.ts

+362
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
import type { VaultError } from "@/errors";
2+
import indexPageByDaysTemplate from "@/templates/indexPageByDays.hbs";
3+
import indexPageByLanguagesTemplate from "@/templates/indexPageByLanguages.hbs";
4+
import indexPageByOwnersTemplate from "@/templates/indexPageByOwners.hbs";
5+
import repoPageTemplate from "@/templates/repoPage.hbs";
6+
import type { GitHub } from "@/types";
7+
import { getOrCreateFile, getOrCreateFolder } from "@/utils";
8+
import { Stream, set } from "itertools-ts";
9+
import { DateTime } from "luxon";
10+
import { ResultAsync, ok } from "neverthrow";
11+
import type { DataWriteOptions, FileManager, TFile, Vault } from "obsidian";
12+
import { PluginApiError } from "./errors";
13+
14+
export const emptyPage = `<!-- GITHUB-STARS-START -->
15+
16+
<!-- GITHUB-STARS-END -->
17+
18+
---
19+
`;
20+
21+
export class GithubStarsPluginApi {
22+
private vault: Vault;
23+
private fileManager: FileManager;
24+
private placeHolderRegex =
25+
/(<!-- GITHUB-STARS-START -->\s)([\s\S]*?)(\s<!-- GITHUB-STARS-END -->)/gm;
26+
27+
constructor(vault: Vault, fileManager: FileManager) {
28+
this.vault = vault;
29+
this.fileManager = fileManager;
30+
}
31+
32+
private processContentAndFrontMatter(
33+
file: TFile,
34+
contentFn: (data: string) => string,
35+
frontMatterFn: (frontmatter: unknown) => void,
36+
options?: DataWriteOptions,
37+
) {
38+
const processContent = ResultAsync.fromPromise(
39+
this.vault.process(file, contentFn, options),
40+
() => PluginApiError.ProcessingFailed,
41+
);
42+
const processFrontMatter = ResultAsync.fromPromise(
43+
this.fileManager.processFrontMatter(file, frontMatterFn, options),
44+
() => PluginApiError.ProcessingFailed,
45+
);
46+
return ResultAsync.combine([
47+
processContent,
48+
processFrontMatter,
49+
]).andThen(([result]) => ok(result));
50+
}
51+
52+
private updateDataWithNewContent(data: string, newContent: string): string {
53+
return data.replace(
54+
this.placeHolderRegex,
55+
(_match, p1, _p2, p3) => `${p1}${newContent}${p3}`,
56+
);
57+
}
58+
59+
public createOrUpdateIndexPageByDays(
60+
repos: GitHub.Repository[],
61+
destinationFolder: string,
62+
repostioriesFolder: string,
63+
filename: string,
64+
): ResultAsync<string, PluginApiError | VaultError> {
65+
const reposByDates = new Map<
66+
number,
67+
Map<number, Map<number, GitHub.Repository[]>>
68+
>();
69+
70+
for (const repo of repos) {
71+
const yearKey = repo.starredAt
72+
?.startOf("year")
73+
.toMillis() as number;
74+
const monthKey = repo.starredAt
75+
?.startOf("month")
76+
.toMillis() as number;
77+
const dayKey = repo.starredAt?.startOf("day").toMillis() as number;
78+
79+
let yearGroup = reposByDates.get(yearKey);
80+
if (!yearGroup) {
81+
yearGroup = new Map();
82+
reposByDates.set(yearKey, yearGroup);
83+
}
84+
85+
let monthGroup = yearGroup.get(monthKey);
86+
if (!monthGroup) {
87+
monthGroup = new Map();
88+
yearGroup.set(monthKey, monthGroup);
89+
}
90+
91+
let dayGroup = monthGroup.get(dayKey);
92+
if (!dayGroup) {
93+
dayGroup = [];
94+
monthGroup.set(dayKey, dayGroup);
95+
}
96+
97+
dayGroup.push(repo);
98+
}
99+
100+
const starredCount = Stream.of(repos)
101+
.filter((r) => typeof r.unstarredAt === "undefined")
102+
.toCount();
103+
const unstarredCount = Stream.of(repos)
104+
.filter((r) => !!r.unstarredAt)
105+
.toCount();
106+
107+
const processContent = (data: string) =>
108+
this.updateDataWithNewContent(
109+
data,
110+
indexPageByDaysTemplate({
111+
reposByDates,
112+
repostioriesFolder,
113+
}),
114+
);
115+
// biome-ignore lint/suspicious/noExplicitAny: TODO type of FrontMatter on index page
116+
const processFrontMatter = (frontmatter: any) => {
117+
frontmatter.updatedAt = DateTime.utc().toISODate();
118+
frontmatter.total = repos.length;
119+
frontmatter.starredCount = starredCount;
120+
frontmatter.unstarredCount = unstarredCount;
121+
};
122+
123+
return getOrCreateFolder(this.vault, destinationFolder)
124+
.andThen((folder) =>
125+
getOrCreateFile(
126+
this.vault,
127+
`${folder.path}/${filename}`,
128+
emptyPage,
129+
),
130+
)
131+
.andThen(({ file }) =>
132+
this.processContentAndFrontMatter(
133+
file,
134+
processContent,
135+
processFrontMatter,
136+
),
137+
);
138+
}
139+
140+
public createOrUpdateIndexPageByLanguages(
141+
repos: GitHub.Repository[],
142+
destinationFolder: string,
143+
repostioriesFolder: string,
144+
filename: string,
145+
): ResultAsync<string, PluginApiError | VaultError> {
146+
const sortedMainLanguages = set.distinct(
147+
repos.map((r) => r.mainLanguage).sort(),
148+
);
149+
const reposPerLanguage = Stream.of(repos)
150+
.groupBy((r) => r.mainLanguage)
151+
.sort();
152+
const starredCount = Stream.of(repos)
153+
.filter((r) => typeof r.unstarredAt === "undefined")
154+
.toCount();
155+
const unstarredCount = Stream.of(repos)
156+
.filter((r) => !!r.unstarredAt)
157+
.toCount();
158+
159+
const processContent = (data: string) =>
160+
this.updateDataWithNewContent(
161+
data,
162+
indexPageByLanguagesTemplate({
163+
reposPerLanguage,
164+
languages: sortedMainLanguages,
165+
repostioriesFolder,
166+
}),
167+
);
168+
// biome-ignore lint/suspicious/noExplicitAny: TODO type of FrontMatter on index page
169+
const processFrontMatter = (frontmatter: any) => {
170+
frontmatter.updatedAt = DateTime.utc().toISODate();
171+
frontmatter.total = repos.length;
172+
frontmatter.starredCount = starredCount;
173+
frontmatter.unstarredCount = unstarredCount;
174+
};
175+
176+
return getOrCreateFolder(this.vault, destinationFolder)
177+
.andThen((folder) =>
178+
getOrCreateFile(
179+
this.vault,
180+
`${folder.path}/${filename}`,
181+
emptyPage,
182+
),
183+
)
184+
.andThen(({ file }) =>
185+
this.processContentAndFrontMatter(
186+
file,
187+
processContent,
188+
processFrontMatter,
189+
),
190+
);
191+
}
192+
193+
public createOrUpdateIndexPageByOwners(
194+
repos: GitHub.Repository[],
195+
destinationFolder: string,
196+
repostioriesFolder: string,
197+
filename: string,
198+
): ResultAsync<string, PluginApiError | VaultError> {
199+
const owners = repos.map((r) => r.owner);
200+
const alphaSort = (a: string, b: string) =>
201+
a.toLowerCase().localeCompare(b.toLowerCase());
202+
203+
const sortedOrgs = set.distinct(
204+
owners
205+
.filter((r) => r.isOrganization)
206+
.map((o) => o.login)
207+
.sort(alphaSort),
208+
);
209+
const sortedUsers = set.distinct(
210+
owners
211+
.filter((r) => !r.isOrganization)
212+
.map((o) => o.login)
213+
.sort(alphaSort),
214+
);
215+
const reposOfOrgs = Stream.of(repos)
216+
.filter((r) => r.owner.isOrganization)
217+
.groupBy((r) => r.owner.login)
218+
.sort();
219+
const reposOfUsers = Stream.of(repos)
220+
.filter((r) => !r.owner.isOrganization)
221+
.groupBy((r) => r.owner.login)
222+
.sort();
223+
const starredCount = Stream.of(repos)
224+
.filter((r) => typeof r.unstarredAt === "undefined")
225+
.toCount();
226+
const unstarredCount = Stream.of(repos)
227+
.filter((r) => !!r.unstarredAt)
228+
.toCount();
229+
230+
const processContent = (data: string) =>
231+
this.updateDataWithNewContent(
232+
data,
233+
indexPageByOwnersTemplate({
234+
sortedOrgs,
235+
sortedUsers,
236+
reposOfOrgs,
237+
reposOfUsers,
238+
repostioriesFolder,
239+
}),
240+
);
241+
// biome-ignore lint/suspicious/noExplicitAny: TODO type of FrontMatter on index page
242+
const processFrontMatter = (frontmatter: any) => {
243+
frontmatter.updatedAt = DateTime.utc().toISODate();
244+
frontmatter.total = repos.length;
245+
frontmatter.starredCount = starredCount;
246+
frontmatter.unstarredCount = unstarredCount;
247+
};
248+
249+
return getOrCreateFolder(this.vault, destinationFolder)
250+
.andThen((folder) =>
251+
getOrCreateFile(
252+
this.vault,
253+
`${folder.path}/${filename}`,
254+
emptyPage,
255+
),
256+
)
257+
.andThen(({ file }) =>
258+
this.processContentAndFrontMatter(
259+
file,
260+
processContent,
261+
processFrontMatter,
262+
),
263+
);
264+
}
265+
266+
public createOrUpdateRepositoriesPages(
267+
repos: GitHub.Repository[],
268+
repostioriesFolder: string,
269+
progressCallback: (createdPages: number, updatedPages: number) => void,
270+
): ResultAsync<
271+
{ createdPages: number; updatedPages: number },
272+
PluginApiError | VaultError
273+
> {
274+
let createdPages = 0;
275+
let updatedPages = 0;
276+
const results: ResultAsync<string, PluginApiError | VaultError>[] = [];
277+
278+
for (const repo of repos) {
279+
const processContent = (data: string) =>
280+
this.updateDataWithNewContent(
281+
data,
282+
repoPageTemplate(
283+
{
284+
repo,
285+
},
286+
{
287+
allowedProtoProperties: {
288+
latestReleaseName: true,
289+
},
290+
},
291+
),
292+
);
293+
// biome-ignore lint/suspicious/noExplicitAny: TODO type of FrontMatter on repository page
294+
const processFrontMatter = (frontmatter: any) => {
295+
frontmatter.url = repo.url.toString();
296+
if (repo.homepageUrl) {
297+
frontmatter.homepageUrl = repo.homepageUrl;
298+
} else {
299+
// biome-ignore lint/performance/noDelete: FrontMatter mutation
300+
delete frontmatter.homepageUrl;
301+
}
302+
frontmatter.isArchived = repo.isArchived;
303+
frontmatter.isFork = repo.isFork;
304+
frontmatter.isPrivate = repo.isPrivate;
305+
frontmatter.isTemplate = repo.isTemplate;
306+
frontmatter.stargazerCount = repo.stargazerCount;
307+
frontmatter.forkCount = repo.forkCount;
308+
frontmatter.createdAt = repo.createdAt?.toISODate();
309+
frontmatter.importedAt = repo.importedAt?.toISODate();
310+
frontmatter.pushedAt = repo.pushedAt?.toISODate();
311+
frontmatter.starredAt = repo.starredAt?.toISODate();
312+
frontmatter.updatedAt = repo.updatedAt?.toISODate();
313+
if (repo.unstarredAt) {
314+
frontmatter.unstarredAt = repo.unstarredAt.toISODate();
315+
} else {
316+
// biome-ignore lint/performance/noDelete: FrontMatter mutation
317+
delete frontmatter.unstarredAt;
318+
}
319+
if (repo.repositoryTopics.length) {
320+
frontmatter.topics = repo.repositoryTopics.map(
321+
(t) => t.name,
322+
);
323+
} else {
324+
// biome-ignore lint/performance/noDelete: FrontMatter mutation
325+
delete frontmatter.topics;
326+
}
327+
};
328+
329+
const result = getOrCreateFolder(
330+
this.vault,
331+
`${repostioriesFolder}/${repo.owner.login}`,
332+
)
333+
.andThen((folder) =>
334+
getOrCreateFile(
335+
this.vault,
336+
`${folder.path}/${repo.name}.md`,
337+
emptyPage,
338+
),
339+
)
340+
.andTee(({ isCreated }) => {
341+
if (isCreated) {
342+
createdPages++;
343+
} else {
344+
updatedPages++;
345+
}
346+
progressCallback(createdPages, updatedPages);
347+
})
348+
.andThen(({ file }) =>
349+
this.processContentAndFrontMatter(
350+
file,
351+
processContent,
352+
processFrontMatter,
353+
),
354+
);
355+
results.push(result);
356+
}
357+
358+
return ResultAsync.combine(results).andThen(() =>
359+
ok({ createdPages, updatedPages }),
360+
);
361+
}
362+
}

0 commit comments

Comments
 (0)