1use crate::model::{
2 CatalogMatch, Compatibility, PermissionKey, PermissionLevel, RequiredPermission,
3};
4
5use PermissionKey as PK;
6use PermissionLevel as PL;
7
8pub struct EndpointEntry {
9 pub id: &'static str,
10 pub method: &'static str,
11 pub path_template: &'static str,
12 pub category: &'static str,
13 pub required: &'static [(PermissionKey, PermissionLevel)],
14 pub classification: Compatibility,
15 pub side_effect: &'static str,
16}
17
18impl EndpointEntry {
19 pub fn to_match(&self) -> CatalogMatch {
20 CatalogMatch {
21 endpoint_id: self.id.to_owned(),
22 method: self.method.to_owned(),
23 path_template: self.path_template.to_owned(),
24 category: self.category.to_owned(),
25 required_permissions: self
26 .required
27 .iter()
28 .map(|(key, level)| RequiredPermission {
29 key: *key,
30 level: *level,
31 })
32 .collect(),
33 classification: self.classification,
34 side_effect: self.side_effect.to_owned(),
35 }
36 }
37}
38
39pub const CATALOG: &[EndpointEntry] = &[
40 EndpointEntry {
42 id: "repos.get",
43 method: "GET",
44 path_template: "/repos/{owner}/{repo}",
45 category: "repos",
46 required: &[(PK::Contents, PL::Read)],
47 classification: Compatibility::Simulated,
48 side_effect: "Read repository metadata; ci-forge serves deterministic stub.",
49 },
50 EndpointEntry {
51 id: "contents.get",
52 method: "GET",
53 path_template: "/repos/{owner}/{repo}/contents/{path}",
54 category: "contents",
55 required: &[(PK::Contents, PL::Read)],
56 classification: Compatibility::Simulated,
57 side_effect: "Read file contents; ci-forge serves from local workspace.",
58 },
59 EndpointEntry {
60 id: "contents.put",
61 method: "PUT",
62 path_template: "/repos/{owner}/{repo}/contents/{path}",
63 category: "contents",
64 required: &[(PK::Contents, PL::Write)],
65 classification: Compatibility::Simulated,
66 side_effect: "Create or update file; ci-forge mutates local workspace.",
67 },
68 EndpointEntry {
69 id: "contents.delete",
70 method: "DELETE",
71 path_template: "/repos/{owner}/{repo}/contents/{path}",
72 category: "contents",
73 required: &[(PK::Contents, PL::Write)],
74 classification: Compatibility::Simulated,
75 side_effect: "Delete file; ci-forge mutates local workspace.",
76 },
77 EndpointEntry {
78 id: "git.refs.get",
79 method: "GET",
80 path_template: "/repos/{owner}/{repo}/git/refs/{ref}",
81 category: "git-refs",
82 required: &[(PK::Contents, PL::Read)],
83 classification: Compatibility::Simulated,
84 side_effect: "Read git ref; ci-forge serves from local repository.",
85 },
86 EndpointEntry {
87 id: "git.refs.create",
88 method: "POST",
89 path_template: "/repos/{owner}/{repo}/git/refs",
90 category: "git-refs",
91 required: &[(PK::Contents, PL::Write)],
92 classification: Compatibility::Simulated,
93 side_effect: "Create git ref; ci-forge mutates local repository.",
94 },
95 EndpointEntry {
96 id: "git.refs.update",
97 method: "PATCH",
98 path_template: "/repos/{owner}/{repo}/git/refs/{ref}",
99 category: "git-refs",
100 required: &[(PK::Contents, PL::Write)],
101 classification: Compatibility::Simulated,
102 side_effect: "Update git ref; ci-forge mutates local repository.",
103 },
104 EndpointEntry {
105 id: "git.refs.delete",
106 method: "DELETE",
107 path_template: "/repos/{owner}/{repo}/git/refs/{ref}",
108 category: "git-refs",
109 required: &[(PK::Contents, PL::Write)],
110 classification: Compatibility::Simulated,
111 side_effect: "Delete git ref; ci-forge mutates local repository.",
112 },
113 EndpointEntry {
114 id: "git.tags.get",
115 method: "GET",
116 path_template: "/repos/{owner}/{repo}/git/tags/{sha}",
117 category: "git-tags",
118 required: &[(PK::Contents, PL::Read)],
119 classification: Compatibility::Simulated,
120 side_effect: "Read git tag; ci-forge serves from local repository.",
121 },
122 EndpointEntry {
123 id: "git.tags.create",
124 method: "POST",
125 path_template: "/repos/{owner}/{repo}/git/tags",
126 category: "git-tags",
127 required: &[(PK::Contents, PL::Write)],
128 classification: Compatibility::Simulated,
129 side_effect: "Create annotated tag; ci-forge mutates local repository.",
130 },
131 EndpointEntry {
133 id: "releases.list",
134 method: "GET",
135 path_template: "/repos/{owner}/{repo}/releases",
136 category: "releases",
137 required: &[(PK::Contents, PL::Read)],
138 classification: Compatibility::Simulated,
139 side_effect: "List releases; ci-forge serves from local release store.",
140 },
141 EndpointEntry {
142 id: "releases.create",
143 method: "POST",
144 path_template: "/repos/{owner}/{repo}/releases",
145 category: "releases",
146 required: &[(PK::Contents, PL::Write)],
147 classification: Compatibility::Simulated,
148 side_effect: "Create release; ci-forge persists to local release store.",
149 },
150 EndpointEntry {
151 id: "releases.get",
152 method: "GET",
153 path_template: "/repos/{owner}/{repo}/releases/{id}",
154 category: "releases",
155 required: &[(PK::Contents, PL::Read)],
156 classification: Compatibility::Simulated,
157 side_effect: "Get release by id; ci-forge serves from local release store.",
158 },
159 EndpointEntry {
160 id: "releases.update",
161 method: "PATCH",
162 path_template: "/repos/{owner}/{repo}/releases/{id}",
163 category: "releases",
164 required: &[(PK::Contents, PL::Write)],
165 classification: Compatibility::Simulated,
166 side_effect: "Edit release metadata; ci-forge mutates local release store.",
167 },
168 EndpointEntry {
169 id: "releases.delete",
170 method: "DELETE",
171 path_template: "/repos/{owner}/{repo}/releases/{id}",
172 category: "releases",
173 required: &[(PK::Contents, PL::Write)],
174 classification: Compatibility::Simulated,
175 side_effect: "Delete release; ci-forge mutates local release store.",
176 },
177 EndpointEntry {
178 id: "releases.latest",
179 method: "GET",
180 path_template: "/repos/{owner}/{repo}/releases/latest",
181 category: "releases",
182 required: &[(PK::Contents, PL::Read)],
183 classification: Compatibility::Simulated,
184 side_effect: "Get latest release; ci-forge serves from local release store.",
185 },
186 EndpointEntry {
187 id: "releases.by_tag",
188 method: "GET",
189 path_template: "/repos/{owner}/{repo}/releases/tags/{tag}",
190 category: "releases",
191 required: &[(PK::Contents, PL::Read)],
192 classification: Compatibility::Simulated,
193 side_effect: "Get release by tag; ci-forge serves from local release store.",
194 },
195 EndpointEntry {
196 id: "releases.assets.list",
197 method: "GET",
198 path_template: "/repos/{owner}/{repo}/releases/{id}/assets",
199 category: "release-assets",
200 required: &[(PK::Contents, PL::Read)],
201 classification: Compatibility::Simulated,
202 side_effect: "List release assets; ci-forge serves from local release store.",
203 },
204 EndpointEntry {
205 id: "releases.assets.upload",
206 method: "POST",
207 path_template: "/repos/{owner}/{repo}/releases/{id}/assets",
208 category: "release-assets",
209 required: &[(PK::Contents, PL::Write)],
210 classification: Compatibility::Simulated,
211 side_effect: "Upload release asset; ci-forge persists asset to local release store.",
212 },
213 EndpointEntry {
214 id: "releases.assets.delete",
215 method: "DELETE",
216 path_template: "/repos/{owner}/{repo}/releases/assets/{id}",
217 category: "release-assets",
218 required: &[(PK::Contents, PL::Write)],
219 classification: Compatibility::Simulated,
220 side_effect: "Delete release asset; ci-forge mutates local release store.",
221 },
222 EndpointEntry {
223 id: "releases.assets.get",
224 method: "GET",
225 path_template: "/repos/{owner}/{repo}/releases/assets/{id}",
226 category: "release-assets",
227 required: &[(PK::Contents, PL::Read)],
228 classification: Compatibility::Simulated,
229 side_effect: "Get release asset metadata; ci-forge serves from local release store.",
230 },
231 EndpointEntry {
233 id: "issues.list",
234 method: "GET",
235 path_template: "/repos/{owner}/{repo}/issues",
236 category: "issues",
237 required: &[(PK::Issues, PL::Read)],
238 classification: Compatibility::Simulated,
239 side_effect: "List issues; ci-forge serves empty/stub list offline.",
240 },
241 EndpointEntry {
242 id: "issues.create",
243 method: "POST",
244 path_template: "/repos/{owner}/{repo}/issues",
245 category: "issues",
246 required: &[(PK::Issues, PL::Write)],
247 classification: Compatibility::Simulated,
248 side_effect: "Create issue; ci-forge logs deterministic stub locally.",
249 },
250 EndpointEntry {
251 id: "issues.get",
252 method: "GET",
253 path_template: "/repos/{owner}/{repo}/issues/{number}",
254 category: "issues",
255 required: &[(PK::Issues, PL::Read)],
256 classification: Compatibility::Simulated,
257 side_effect: "Get issue; ci-forge serves stub offline.",
258 },
259 EndpointEntry {
260 id: "issues.update",
261 method: "PATCH",
262 path_template: "/repos/{owner}/{repo}/issues/{number}",
263 category: "issues",
264 required: &[(PK::Issues, PL::Write)],
265 classification: Compatibility::Simulated,
266 side_effect: "Update issue; ci-forge logs stub mutation locally.",
267 },
268 EndpointEntry {
269 id: "issues.comments.list",
270 method: "GET",
271 path_template: "/repos/{owner}/{repo}/issues/{number}/comments",
272 category: "issue-comments",
273 required: &[(PK::Issues, PL::Read)],
274 classification: Compatibility::Simulated,
275 side_effect: "List issue comments; ci-forge serves stub locally.",
276 },
277 EndpointEntry {
278 id: "issues.comments.create",
279 method: "POST",
280 path_template: "/repos/{owner}/{repo}/issues/{number}/comments",
281 category: "issue-comments",
282 required: &[(PK::Issues, PL::Write)],
283 classification: Compatibility::Simulated,
284 side_effect: "Create issue comment; ci-forge logs stub mutation locally.",
285 },
286 EndpointEntry {
287 id: "issues.labels.list",
288 method: "GET",
289 path_template: "/repos/{owner}/{repo}/issues/{number}/labels",
290 category: "issue-labels",
291 required: &[(PK::Issues, PL::Read)],
292 classification: Compatibility::Simulated,
293 side_effect: "List labels on issue; ci-forge serves stub locally.",
294 },
295 EndpointEntry {
296 id: "issues.labels.add",
297 method: "POST",
298 path_template: "/repos/{owner}/{repo}/issues/{number}/labels",
299 category: "issue-labels",
300 required: &[(PK::Issues, PL::Write)],
301 classification: Compatibility::Simulated,
302 side_effect: "Add labels to issue; ci-forge logs stub mutation locally.",
303 },
304 EndpointEntry {
305 id: "issues.labels.remove",
306 method: "DELETE",
307 path_template: "/repos/{owner}/{repo}/issues/{number}/labels/{name}",
308 category: "issue-labels",
309 required: &[(PK::Issues, PL::Write)],
310 classification: Compatibility::Simulated,
311 side_effect: "Remove label from issue; ci-forge logs stub mutation locally.",
312 },
313 EndpointEntry {
314 id: "repo.labels.list",
315 method: "GET",
316 path_template: "/repos/{owner}/{repo}/labels",
317 category: "labels",
318 required: &[(PK::Issues, PL::Read)],
319 classification: Compatibility::Simulated,
320 side_effect: "List repo labels; ci-forge serves stub locally.",
321 },
322 EndpointEntry {
324 id: "pulls.list",
325 method: "GET",
326 path_template: "/repos/{owner}/{repo}/pulls",
327 category: "pulls",
328 required: &[(PK::PullRequests, PL::Read)],
329 classification: Compatibility::Simulated,
330 side_effect: "List pull requests; ci-forge serves stub locally.",
331 },
332 EndpointEntry {
333 id: "pulls.create",
334 method: "POST",
335 path_template: "/repos/{owner}/{repo}/pulls",
336 category: "pulls",
337 required: &[(PK::PullRequests, PL::Write)],
338 classification: Compatibility::Simulated,
339 side_effect: "Create pull request; ci-forge logs stub mutation locally.",
340 },
341 EndpointEntry {
342 id: "pulls.get",
343 method: "GET",
344 path_template: "/repos/{owner}/{repo}/pulls/{number}",
345 category: "pulls",
346 required: &[(PK::PullRequests, PL::Read)],
347 classification: Compatibility::Simulated,
348 side_effect: "Get pull request; ci-forge serves stub locally.",
349 },
350 EndpointEntry {
351 id: "pulls.update",
352 method: "PATCH",
353 path_template: "/repos/{owner}/{repo}/pulls/{number}",
354 category: "pulls",
355 required: &[(PK::PullRequests, PL::Write)],
356 classification: Compatibility::Simulated,
357 side_effect: "Update pull request; ci-forge logs stub mutation locally.",
358 },
359 EndpointEntry {
360 id: "pulls.reviews.list",
361 method: "GET",
362 path_template: "/repos/{owner}/{repo}/pulls/{number}/reviews",
363 category: "pull-reviews",
364 required: &[(PK::PullRequests, PL::Read)],
365 classification: Compatibility::Simulated,
366 side_effect: "List PR reviews; ci-forge serves stub locally.",
367 },
368 EndpointEntry {
369 id: "pulls.reviews.create",
370 method: "POST",
371 path_template: "/repos/{owner}/{repo}/pulls/{number}/reviews",
372 category: "pull-reviews",
373 required: &[(PK::PullRequests, PL::Write)],
374 classification: Compatibility::Simulated,
375 side_effect: "Create PR review; ci-forge logs stub mutation locally.",
376 },
377 EndpointEntry {
378 id: "pulls.comments.create",
379 method: "POST",
380 path_template: "/repos/{owner}/{repo}/pulls/{number}/comments",
381 category: "pull-comments",
382 required: &[(PK::PullRequests, PL::Write)],
383 classification: Compatibility::Simulated,
384 side_effect: "Create PR review comment; ci-forge logs stub mutation locally.",
385 },
386 EndpointEntry {
388 id: "checks.runs.create",
389 method: "POST",
390 path_template: "/repos/{owner}/{repo}/check-runs",
391 category: "checks",
392 required: &[(PK::Checks, PL::Write)],
393 classification: Compatibility::Simulated,
394 side_effect: "Create check run; ci-forge persists stub check locally.",
395 },
396 EndpointEntry {
397 id: "checks.runs.update",
398 method: "PATCH",
399 path_template: "/repos/{owner}/{repo}/check-runs/{id}",
400 category: "checks",
401 required: &[(PK::Checks, PL::Write)],
402 classification: Compatibility::Simulated,
403 side_effect: "Update check run; ci-forge mutates stub check locally.",
404 },
405 EndpointEntry {
406 id: "checks.runs.get",
407 method: "GET",
408 path_template: "/repos/{owner}/{repo}/check-runs/{id}",
409 category: "checks",
410 required: &[(PK::Checks, PL::Read)],
411 classification: Compatibility::Simulated,
412 side_effect: "Get check run; ci-forge serves stub check locally.",
413 },
414 EndpointEntry {
415 id: "checks.runs.list_for_commit",
416 method: "GET",
417 path_template: "/repos/{owner}/{repo}/commits/{sha}/check-runs",
418 category: "checks",
419 required: &[(PK::Checks, PL::Read)],
420 classification: Compatibility::Simulated,
421 side_effect: "List check runs for commit; ci-forge serves stub list locally.",
422 },
423 EndpointEntry {
424 id: "checks.suites.list_for_commit",
425 method: "GET",
426 path_template: "/repos/{owner}/{repo}/commits/{sha}/check-suites",
427 category: "checks",
428 required: &[(PK::Checks, PL::Read)],
429 classification: Compatibility::Simulated,
430 side_effect: "List check suites for commit; ci-forge serves stub list locally.",
431 },
432 EndpointEntry {
434 id: "statuses.create",
435 method: "POST",
436 path_template: "/repos/{owner}/{repo}/statuses/{sha}",
437 category: "statuses",
438 required: &[(PK::Statuses, PL::Write)],
439 classification: Compatibility::Simulated,
440 side_effect: "Create commit status; ci-forge persists stub status locally.",
441 },
442 EndpointEntry {
443 id: "statuses.list_for_ref",
444 method: "GET",
445 path_template: "/repos/{owner}/{repo}/commits/{ref}/statuses",
446 category: "statuses",
447 required: &[(PK::Statuses, PL::Read)],
448 classification: Compatibility::Simulated,
449 side_effect: "List statuses for ref; ci-forge serves stub list locally.",
450 },
451 EndpointEntry {
452 id: "statuses.combined_for_ref",
453 method: "GET",
454 path_template: "/repos/{owner}/{repo}/commits/{ref}/status",
455 category: "statuses",
456 required: &[(PK::Statuses, PL::Read)],
457 classification: Compatibility::Simulated,
458 side_effect: "Get combined status for ref; ci-forge serves stub locally.",
459 },
460 EndpointEntry {
462 id: "actions.runs.list",
463 method: "GET",
464 path_template: "/repos/{owner}/{repo}/actions/runs",
465 category: "actions",
466 required: &[(PK::Actions, PL::Read)],
467 classification: Compatibility::Simulated,
468 side_effect: "List workflow runs; ci-forge serves from local run store.",
469 },
470 EndpointEntry {
471 id: "actions.runs.get",
472 method: "GET",
473 path_template: "/repos/{owner}/{repo}/actions/runs/{id}",
474 category: "actions",
475 required: &[(PK::Actions, PL::Read)],
476 classification: Compatibility::Simulated,
477 side_effect: "Get workflow run; ci-forge serves from local run store.",
478 },
479 EndpointEntry {
480 id: "actions.runs.jobs",
481 method: "GET",
482 path_template: "/repos/{owner}/{repo}/actions/runs/{id}/jobs",
483 category: "actions",
484 required: &[(PK::Actions, PL::Read)],
485 classification: Compatibility::Simulated,
486 side_effect: "List jobs for workflow run; ci-forge serves from local run store.",
487 },
488 EndpointEntry {
489 id: "actions.runs.cancel",
490 method: "POST",
491 path_template: "/repos/{owner}/{repo}/actions/runs/{id}/cancel",
492 category: "actions",
493 required: &[(PK::Actions, PL::Write)],
494 classification: Compatibility::Simulated,
495 side_effect: "Cancel workflow run; ci-forge mutates local run store.",
496 },
497 EndpointEntry {
498 id: "actions.artifacts.list",
499 method: "GET",
500 path_template: "/repos/{owner}/{repo}/actions/artifacts",
501 category: "actions",
502 required: &[(PK::Actions, PL::Read)],
503 classification: Compatibility::Simulated,
504 side_effect: "List artifacts; ci-forge serves from local artifact store.",
505 },
506 EndpointEntry {
507 id: "actions.artifacts.get",
508 method: "GET",
509 path_template: "/repos/{owner}/{repo}/actions/artifacts/{id}",
510 category: "actions",
511 required: &[(PK::Actions, PL::Read)],
512 classification: Compatibility::Simulated,
513 side_effect: "Get artifact metadata; ci-forge serves from local artifact store.",
514 },
515 EndpointEntry {
516 id: "actions.artifacts.list_for_run",
517 method: "GET",
518 path_template: "/repos/{owner}/{repo}/actions/runs/{id}/artifacts",
519 category: "actions",
520 required: &[(PK::Actions, PL::Read)],
521 classification: Compatibility::Simulated,
522 side_effect: "List run artifacts; ci-forge serves from local artifact store.",
523 },
524 EndpointEntry {
525 id: "actions.caches.list",
526 method: "GET",
527 path_template: "/repos/{owner}/{repo}/actions/caches",
528 category: "actions",
529 required: &[(PK::Actions, PL::Read)],
530 classification: Compatibility::Simulated,
531 side_effect: "List caches; ci-forge serves from local cache store.",
532 },
533 EndpointEntry {
534 id: "actions.workflows.list",
535 method: "GET",
536 path_template: "/repos/{owner}/{repo}/actions/workflows",
537 category: "actions",
538 required: &[(PK::Actions, PL::Read)],
539 classification: Compatibility::Simulated,
540 side_effect: "List workflows; ci-forge serves from local workflow store.",
541 },
542 EndpointEntry {
543 id: "actions.workflows.dispatch",
544 method: "POST",
545 path_template: "/repos/{owner}/{repo}/actions/workflows/{id}/dispatches",
546 category: "actions",
547 required: &[(PK::Actions, PL::Write)],
548 classification: Compatibility::Simulated,
549 side_effect: "Trigger workflow dispatch; ci-forge enqueues stub run locally.",
550 },
551 EndpointEntry {
553 id: "deployments.list",
554 method: "GET",
555 path_template: "/repos/{owner}/{repo}/deployments",
556 category: "deployments",
557 required: &[(PK::Deployments, PL::Read)],
558 classification: Compatibility::Simulated,
559 side_effect: "List deployments; ci-forge serves stub list locally.",
560 },
561 EndpointEntry {
562 id: "deployments.create",
563 method: "POST",
564 path_template: "/repos/{owner}/{repo}/deployments",
565 category: "deployments",
566 required: &[(PK::Deployments, PL::Write)],
567 classification: Compatibility::Simulated,
568 side_effect: "Create deployment; ci-forge persists stub deployment locally.",
569 },
570 EndpointEntry {
571 id: "deployments.get",
572 method: "GET",
573 path_template: "/repos/{owner}/{repo}/deployments/{id}",
574 category: "deployments",
575 required: &[(PK::Deployments, PL::Read)],
576 classification: Compatibility::Simulated,
577 side_effect: "Get deployment; ci-forge serves stub deployment locally.",
578 },
579 EndpointEntry {
580 id: "deployments.statuses.create",
581 method: "POST",
582 path_template: "/repos/{owner}/{repo}/deployments/{id}/statuses",
583 category: "deployment-statuses",
584 required: &[(PK::Deployments, PL::Write)],
585 classification: Compatibility::Simulated,
586 side_effect: "Create deployment status; ci-forge persists stub status locally.",
587 },
588 EndpointEntry {
589 id: "deployments.statuses.list",
590 method: "GET",
591 path_template: "/repos/{owner}/{repo}/deployments/{id}/statuses",
592 category: "deployment-statuses",
593 required: &[(PK::Deployments, PL::Read)],
594 classification: Compatibility::Simulated,
595 side_effect: "List deployment statuses; ci-forge serves stub list locally.",
596 },
597 EndpointEntry {
599 id: "packages.user.list",
600 method: "GET",
601 path_template: "/user/packages",
602 category: "packages",
603 required: &[(PK::Packages, PL::Read)],
604 classification: Compatibility::Simulated,
605 side_effect: "List user packages; ci-forge serves stub list locally.",
606 },
607 EndpointEntry {
608 id: "packages.org.list",
609 method: "GET",
610 path_template: "/orgs/{org}/packages",
611 category: "packages",
612 required: &[(PK::Packages, PL::Read)],
613 classification: Compatibility::Simulated,
614 side_effect: "List org packages; ci-forge serves stub list locally.",
615 },
616 EndpointEntry {
617 id: "packages.user.get",
618 method: "GET",
619 path_template: "/user/packages/{package_type}/{name}",
620 category: "packages",
621 required: &[(PK::Packages, PL::Read)],
622 classification: Compatibility::Simulated,
623 side_effect: "Get user package; ci-forge serves stub locally.",
624 },
625 EndpointEntry {
626 id: "packages.user.delete",
627 method: "DELETE",
628 path_template: "/user/packages/{package_type}/{name}",
629 category: "packages",
630 required: &[(PK::Packages, PL::Write)],
631 classification: Compatibility::Simulated,
632 side_effect: "Delete user package; ci-forge mutates local package store.",
633 },
634 EndpointEntry {
636 id: "meta.rate_limit",
637 method: "GET",
638 path_template: "/rate_limit",
639 category: "meta",
640 required: &[],
641 classification: Compatibility::Exact,
642 side_effect: "Read rate limit; deterministic stub matches GitHub response shape exactly.",
643 },
644 EndpointEntry {
645 id: "meta.user",
646 method: "GET",
647 path_template: "/user",
648 category: "meta",
649 required: &[],
650 classification: Compatibility::Exact,
651 side_effect: "Authenticated user identity; deterministic stub.",
652 },
653 EndpointEntry {
654 id: "meta.meta",
655 method: "GET",
656 path_template: "/meta",
657 category: "meta",
658 required: &[],
659 classification: Compatibility::Exact,
660 side_effect: "GitHub meta info; deterministic stub.",
661 },
662 EndpointEntry {
663 id: "meta.installation",
664 method: "GET",
665 path_template: "/repos/{owner}/{repo}/installation",
666 category: "meta",
667 required: &[],
668 classification: Compatibility::Exact,
669 side_effect: "App installation metadata; deterministic stub.",
670 },
671];
672
673pub fn lookup(method: &str, path: &str) -> Option<CatalogMatch> {
674 let normalized_method = method.trim().to_uppercase();
675 let stripped = strip_query(path);
676 for entry in CATALOG {
677 if entry.method.eq_ignore_ascii_case(&normalized_method)
678 && path_matches(entry.path_template, stripped)
679 {
680 return Some(entry.to_match());
681 }
682 }
683 None
684}
685
686pub fn strip_query(path: &str) -> &str {
687 match path.find('?') {
688 Some(index) => &path[..index],
689 None => path,
690 }
691}
692
693pub fn path_matches(template: &str, path: &str) -> bool {
694 let template_segs: Vec<&str> = trim_slashes(template).split('/').collect();
695 let path_segs: Vec<&str> = trim_slashes(path).split('/').collect();
696 if template_segs.len() != path_segs.len() {
697 return false;
698 }
699 for (t, p) in template_segs.iter().zip(path_segs.iter()) {
700 if t.is_empty() && p.is_empty() {
701 continue;
702 }
703 if is_placeholder(t) {
704 if p.is_empty() {
705 return false;
706 }
707 continue;
708 }
709 if !t.eq_ignore_ascii_case(p) {
710 return false;
711 }
712 }
713 true
714}
715
716fn trim_slashes(value: &str) -> &str {
717 value.trim_start_matches('/').trim_end_matches('/')
718}
719
720fn is_placeholder(segment: &str) -> bool {
721 segment.starts_with('{') && segment.ends_with('}') && segment.len() >= 2
722}
723
724pub fn is_graphql_path(path: &str) -> bool {
725 let stripped = strip_query(path);
726 let trimmed = trim_slashes(stripped);
727 trimmed.eq_ignore_ascii_case("graphql")
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 #[test]
735 fn path_matches_template_with_placeholders() {
736 assert!(path_matches(
737 "/repos/{owner}/{repo}/releases/{id}",
738 "/repos/wildmason/mortar/releases/123",
739 ));
740 }
741
742 #[test]
743 fn path_matches_rejects_mismatched_segment_count() {
744 assert!(!path_matches(
745 "/repos/{owner}/{repo}/releases",
746 "/repos/wildmason/mortar/releases/123",
747 ));
748 }
749
750 #[test]
751 fn path_matches_rejects_mismatched_literal() {
752 assert!(!path_matches(
753 "/repos/{owner}/{repo}/issues",
754 "/repos/wildmason/mortar/releases",
755 ));
756 }
757
758 #[test]
759 fn lookup_returns_catalog_match_for_release_create() {
760 let result = lookup("POST", "/repos/wildmason/mortar/releases").unwrap();
761 assert_eq!(result.endpoint_id, "releases.create");
762 assert_eq!(result.category, "releases");
763 assert_eq!(result.required_permissions.len(), 1);
764 assert_eq!(result.required_permissions[0].key, PermissionKey::Contents);
765 assert_eq!(result.required_permissions[0].level, PermissionLevel::Write);
766 }
767
768 #[test]
769 fn lookup_returns_none_for_off_catalog() {
770 assert!(lookup("POST", "/repos/wildmason/mortar/branches/main/protection").is_none());
771 }
772
773 #[test]
774 fn lookup_ignores_query_string() {
775 let result = lookup("GET", "/repos/wildmason/mortar/issues?state=open").unwrap();
776 assert_eq!(result.endpoint_id, "issues.list");
777 }
778
779 #[test]
780 fn graphql_path_detection() {
781 assert!(is_graphql_path("/graphql"));
782 assert!(is_graphql_path("/graphql?foo=bar"));
783 assert!(!is_graphql_path("/repos/x/y"));
784 }
785}