1use crate::manifest::{self, Event, PackId, SCHEMA_VERSION};
4use chrono::Utc;
5use std::path::Path;
6use thiserror::Error;
7
8#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AddRequest {
12 pub url: String,
13 pub path: String,
14 pub pack_type: String,
15}
16
17impl AddRequest {
18 pub fn new(
19 url: impl Into<String>,
20 path: impl Into<String>,
21 pack_type: impl Into<String>,
22 ) -> Self {
23 Self { url: url.into(), path: path.into(), pack_type: pack_type.into() }
24 }
25}
26
27#[non_exhaustive]
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct AddOpts {
31 pub dry_run: bool,
32}
33
34impl AddOpts {
35 pub fn new(dry_run: bool) -> Self {
36 Self { dry_run }
37 }
38}
39
40#[non_exhaustive]
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AddReport {
44 pub id: PackId,
45 pub url: String,
46 pub path: String,
47 pub pack_type: String,
48 pub dry_run: bool,
49 pub appended: bool,
50}
51
52#[non_exhaustive]
53#[derive(Debug, Error)]
54pub enum AddError {
55 #[error("manifest write failed: {0}")]
56 Manifest(#[from] manifest::ManifestError),
57}
58
59pub fn add_pack(
61 manifest_path: &Path,
62 request: AddRequest,
63 opts: AddOpts,
64) -> Result<AddReport, AddError> {
65 let id = PackId::from(request.path.clone());
66 if !opts.dry_run {
67 let ev = Event::Add {
68 ts: Utc::now(),
69 id: id.clone(),
70 url: request.url.clone(),
71 path: request.path.clone(),
72 pack_type: request.pack_type.clone(),
73 schema_version: SCHEMA_VERSION.to_string(),
74 };
75 manifest::append_event(manifest_path, &ev)?;
76 }
77
78 Ok(AddReport {
79 id,
80 url: request.url,
81 path: request.path,
82 pack_type: request.pack_type,
83 dry_run: opts.dry_run,
84 appended: !opts.dry_run,
85 })
86}
87
88pub fn infer_path_from_url(url: &str) -> String {
90 let trimmed = url.trim_end_matches(['/', '\\']);
91 let tail = trimmed.rsplit(['/', '\\', ':']).next().unwrap_or(trimmed);
92 tail.strip_suffix(".git").unwrap_or(tail).to_string()
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn add_pack_appends_add_event() {
101 let dir = tempfile::tempdir().unwrap();
102 let manifest = dir.path().join(".grex/events.jsonl");
103 let report = add_pack(
104 &manifest,
105 AddRequest {
106 url: "https://example.com/repo.git".into(),
107 path: "repo".into(),
108 pack_type: "scripted".into(),
109 },
110 AddOpts { dry_run: false },
111 )
112 .unwrap();
113
114 assert!(report.appended);
115 assert_eq!(report.id, "repo");
116 let events = manifest::read_all(&manifest).unwrap();
117 assert_eq!(events.len(), 1);
118 match &events[0] {
119 Event::Add { id, url, path, pack_type, schema_version, .. } => {
120 assert_eq!(id, "repo");
121 assert_eq!(url, "https://example.com/repo.git");
122 assert_eq!(path, "repo");
123 assert_eq!(pack_type, "scripted");
124 assert_eq!(schema_version, SCHEMA_VERSION);
125 }
126 _ => panic!("expected add event"),
127 }
128 }
129
130 #[test]
131 fn add_pack_dry_run_does_not_write_manifest() {
132 let dir = tempfile::tempdir().unwrap();
133 let manifest = dir.path().join(".grex/events.jsonl");
134 let report = add_pack(
135 &manifest,
136 AddRequest { url: "".into(), path: "local".into(), pack_type: "declarative".into() },
137 AddOpts { dry_run: true },
138 )
139 .unwrap();
140
141 assert!(!report.appended);
142 assert!(!manifest.exists());
143 }
144
145 #[test]
146 fn infer_path_from_https_git_url() {
147 assert_eq!(infer_path_from_url("https://example.com/org/repo.git"), "repo");
148 }
149
150 #[test]
151 fn infer_path_from_scp_like_url() {
152 assert_eq!(infer_path_from_url("git@example.com:org/repo.git"), "repo");
153 }
154
155 #[test]
156 fn infer_path_from_trailing_slash() {
157 assert_eq!(infer_path_from_url("https://example.com/org/repo/"), "repo");
158 }
159
160 #[test]
161 fn infer_path_from_empty_url_is_empty() {
162 assert_eq!(infer_path_from_url(""), "");
163 }
164}