Skip to main content

gha_github_service_proof/
catalog.rs

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    // -- repos / contents --
41    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    // -- releases / assets --
132    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    // -- issues / comments / labels --
232    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    // -- pull requests --
323    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    // -- checks --
387    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    // -- statuses --
433    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    // -- actions metadata --
461    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    // -- deployments --
552    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    // -- packages --
598    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    // -- metadata / probes --
635    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}