1use serde::{Deserialize, Serialize};
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug)]
9pub struct SourceBuildError(String);
10
11impl fmt::Display for SourceBuildError {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "Source build error: {}", self.0)
14 }
15}
16
17impl Error for SourceBuildError {}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Branch {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 display_name: Option<String>,
25}
26
27impl Branch {
28 #[must_use]
30 pub fn new(display_name: impl Into<String>) -> Self {
31 Self {
32 display_name: Some(display_name.into()),
33 }
34 }
35
36 #[must_use]
38 pub fn display_name(&self) -> Option<&str> {
39 self.display_name.as_deref()
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
45#[serde(rename_all = "camelCase")]
46pub struct GithubRepo {
47 #[serde(skip_serializing_if = "Option::is_none")]
48 owner: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 repo: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 is_private: Option<bool>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 default_branch: Option<Branch>,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 branches: Vec<Branch>,
57}
58
59impl GithubRepo {
60 #[must_use]
62 pub fn new(owner: impl Into<String>, repo: impl Into<String>) -> Self {
63 Self {
64 owner: Some(owner.into()),
65 repo: Some(repo.into()),
66 ..Self::default()
67 }
68 }
69
70 #[must_use]
72 pub fn with_is_private(mut self, is_private: bool) -> Self {
73 self.is_private = Some(is_private);
74 self
75 }
76
77 #[must_use]
79 pub fn with_default_branch(mut self, default_branch: Branch) -> Self {
80 self.default_branch = Some(default_branch);
81 self
82 }
83
84 #[must_use]
86 pub fn with_branches(mut self, branches: Vec<Branch>) -> Self {
87 self.branches = branches;
88 self
89 }
90
91 #[must_use]
93 pub fn owner(&self) -> Option<&str> {
94 self.owner.as_deref()
95 }
96
97 #[must_use]
99 pub fn repo(&self) -> Option<&str> {
100 self.repo.as_deref()
101 }
102
103 #[must_use]
105 pub fn is_private(&self) -> Option<bool> {
106 self.is_private
107 }
108
109 #[must_use]
111 pub fn default_branch(&self) -> Option<&Branch> {
112 self.default_branch.as_ref()
113 }
114
115 #[must_use]
117 pub fn branches(&self) -> &[Branch] {
118 &self.branches
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct Source {
129 #[serde(skip_serializing_if = "Option::is_none")]
130 id: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 name: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 github_repo: Option<GithubRepo>,
135}
136
137impl Source {
138 #[must_use]
140 pub fn builder() -> SourceBuilder {
141 SourceBuilder::default()
142 }
143
144 #[must_use]
146 pub fn id(&self) -> Option<&str> {
147 self.id.as_deref()
148 }
149
150 #[must_use]
152 pub fn name(&self) -> Option<&str> {
153 self.name.as_deref()
154 }
155
156 #[must_use]
158 pub fn github_repo(&self) -> Option<&GithubRepo> {
159 self.github_repo.as_ref()
160 }
161}
162
163#[derive(Debug, Default)]
165pub struct SourceBuilder {
166 id: Option<String>,
167 name: Option<String>,
168 github_repo: Option<GithubRepo>,
169}
170
171impl SourceBuilder {
172 #[must_use]
174 pub fn id(mut self, id: impl Into<String>) -> Self {
175 self.id = Some(id.into());
176 self
177 }
178
179 #[must_use]
181 pub fn name(mut self, name: impl Into<String>) -> Self {
182 self.name = Some(name.into());
183 self
184 }
185
186 #[must_use]
188 pub fn github_repo(mut self, github_repo: GithubRepo) -> Self {
189 self.github_repo = Some(github_repo);
190 self
191 }
192
193 pub fn build(self) -> Result<Source, SourceBuildError> {
199 Ok(Source {
200 id: self.id,
201 name: self.name,
202 github_repo: self.github_repo,
203 })
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_source_builder_with_fields() {
213 let source = Source::builder()
214 .id("src-1")
215 .name("My Source")
216 .build()
217 .unwrap();
218 assert_eq!(source.id(), Some("src-1"));
219 assert_eq!(source.name(), Some("My Source"));
220 }
221
222 #[test]
223 fn test_source_builder_without_fields() {
224 let source = Source::builder().build().unwrap();
225 assert_eq!(source.id(), None);
226 assert_eq!(source.name(), None);
227 }
228
229 #[test]
230 fn test_source_builder_with_github_repo() {
231 let source = Source::builder()
232 .id("src-1")
233 .name("sources/github/example-owner/example-repo")
234 .github_repo(GithubRepo::new("example-owner", "example-repo").with_is_private(true))
235 .build()
236 .unwrap();
237
238 assert_eq!(
239 source.github_repo().and_then(GithubRepo::owner),
240 Some("example-owner")
241 );
242 }
243
244 #[test]
247 fn test_source_deserializes_real_api_shape() {
248 let json = r#"{
249 "name": "sources/github/example-owner/example-repo",
250 "githubRepo": {
251 "owner": "example-owner",
252 "repo": "example-repo",
253 "isPrivate": true,
254 "defaultBranch": {
255 "displayName": "main"
256 },
257 "branches": [
258 {"displayName": "main"},
259 {"displayName": "feature/x"}
260 ]
261 },
262 "id": "github/example-owner/example-repo"
263 }"#;
264
265 let source: Source = serde_json::from_str(json).unwrap();
266 assert_eq!(
267 source.name(),
268 Some("sources/github/example-owner/example-repo")
269 );
270 let repo = source.github_repo().unwrap();
271 assert_eq!(repo.owner(), Some("example-owner"));
272 assert_eq!(repo.is_private(), Some(true));
273 assert_eq!(
274 repo.default_branch().and_then(Branch::display_name),
275 Some("main")
276 );
277 assert_eq!(repo.branches().len(), 2);
278 }
279}