Skip to content

Commit 0c00aab

Browse files
committed
updating README with usage examples
1 parent 9c285cf commit 0c00aab

File tree

3 files changed

+230
-109
lines changed

3 files changed

+230
-109
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,44 @@ codeowners for-team Payroll
190190
```
191191

192192
- Please update `CHANGELOG.md` and this `README.md` when making changes.
193+
194+
### Module layout (for library users)
195+
196+
- `src/runner.rs`: public façade re-exporting the API and types.
197+
- `src/runner/api.rs`: externally available functions used by the CLI and other crates.
198+
- `src/runner/types.rs`: `RunConfig`, `RunResult`, and runner `Error`.
199+
- `src/ownership/`: all ownership logic (parsing, mapping, validation, generation).
200+
- `src/ownership/codeowners_query.rs`: CODEOWNERS-only queries consumed by the façade.
201+
202+
Import public APIs from `codeowners::runner::*`.
203+
204+
### Library usage example
205+
206+
```rust
207+
use codeowners::runner::{RunConfig, for_file, teams_for_files_from_codeowners};
208+
209+
fn main() {
210+
let run_config = RunConfig {
211+
project_root: std::path::PathBuf::from("."),
212+
codeowners_file_path: std::path::PathBuf::from(".github/CODEOWNERS"),
213+
config_path: std::path::PathBuf::from("config/code_ownership.yml"),
214+
no_cache: true, // set false to enable on-disk caching
215+
};
216+
217+
// Find owner for a single file using the optimized path (not just CODEOWNERS)
218+
let result = for_file(&run_config, "app/models/user.rb", false);
219+
for msg in result.info_messages { println!("{}", msg); }
220+
for err in result.io_errors { eprintln!("io: {}", err); }
221+
for err in result.validation_errors { eprintln!("validation: {}", err); }
222+
223+
// Map multiple files to teams using CODEOWNERS rules only
224+
let files = vec![
225+
"app/models/user.rb".to_string(),
226+
"config/teams/payroll.yml".to_string(),
227+
];
228+
match teams_for_files_from_codeowners(&run_config, &files) {
229+
Ok(map) => println!("{:?}", map),
230+
Err(e) => eprintln!("error: {}", e),
231+
}
232+
}
233+
```

src/runner.rs

Lines changed: 0 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -242,119 +242,10 @@ impl Runner {
242242

243243
#[cfg(test)]
244244
mod tests {
245-
use std::path::Path;
246-
use tempfile::tempdir;
247-
248245
use super::*;
249-
use crate::{common_test, ownership::mapper::Source};
250246

251247
#[test]
252248
fn test_version() {
253249
assert_eq!(version(), env!("CARGO_PKG_VERSION").to_string());
254250
}
255-
fn write_file(temp_dir: &Path, file_path: &str, content: &str) {
256-
let file_path = temp_dir.join(file_path);
257-
let _ = std::fs::create_dir_all(file_path.parent().unwrap());
258-
std::fs::write(file_path, content).unwrap();
259-
}
260-
261-
#[test]
262-
fn test_file_owners_for_file() {
263-
let temp_dir = tempdir().unwrap();
264-
write_file(
265-
temp_dir.path(),
266-
"config/code_ownership.yml",
267-
common_test::tests::DEFAULT_CODE_OWNERSHIP_YML,
268-
);
269-
["a", "b", "c"].iter().for_each(|name| {
270-
let team_yml = format!("name: {}\ngithub:\n team: \"@{}\"\n members:\n - {}member\n", name, name, name);
271-
write_file(temp_dir.path(), &format!("config/teams/{}.yml", name), &team_yml);
272-
});
273-
write_file(
274-
temp_dir.path(),
275-
"app/consumers/deep/nesting/nestdir/deep_file.rb",
276-
"# @team b\nclass DeepFile end;",
277-
);
278-
279-
let run_config = RunConfig {
280-
project_root: temp_dir.path().to_path_buf(),
281-
codeowners_file_path: temp_dir.path().join(".github/CODEOWNERS").to_path_buf(),
282-
config_path: temp_dir.path().join("config/code_ownership.yml").to_path_buf(),
283-
no_cache: false,
284-
};
285-
286-
let file_owner = file_owner_for_file(&run_config, "app/consumers/deep/nesting/nestdir/deep_file.rb")
287-
.unwrap()
288-
.unwrap();
289-
assert_eq!(file_owner.team.name, "b");
290-
assert_eq!(file_owner.team.github_team, "@b");
291-
assert!(file_owner.team.path.to_string_lossy().ends_with("config/teams/b.yml"));
292-
assert_eq!(file_owner.sources.len(), 1);
293-
assert_eq!(file_owner.sources, vec![Source::AnnotatedFile]);
294-
295-
let team = team_for_file(&run_config, "app/consumers/deep/nesting/nestdir/deep_file.rb")
296-
.unwrap()
297-
.unwrap();
298-
assert_eq!(team.name, "b");
299-
assert_eq!(team.github_team, "@b");
300-
assert!(team.path.to_string_lossy().ends_with("config/teams/b.yml"));
301-
}
302-
303-
#[test]
304-
fn test_teams_for_files_from_codeowners() {
305-
let project_root = Path::new("tests/fixtures/valid_project");
306-
let file_paths = [
307-
"javascript/packages/items/item.ts",
308-
"config/teams/payroll.yml",
309-
"ruby/app/models/bank_account.rb",
310-
"made/up/file.rb",
311-
"ruby/ignored_files/git_ignored.rb",
312-
];
313-
let run_config = RunConfig {
314-
project_root: project_root.to_path_buf(),
315-
codeowners_file_path: project_root.join(".github/CODEOWNERS").to_path_buf(),
316-
config_path: project_root.join("config/code_ownership.yml").to_path_buf(),
317-
no_cache: false,
318-
};
319-
let teams =
320-
teams_for_files_from_codeowners(&run_config, &file_paths.iter().map(|s| s.to_string()).collect::<Vec<String>>()).unwrap();
321-
assert_eq!(teams.len(), 5);
322-
assert_eq!(
323-
teams
324-
.get("javascript/packages/items/item.ts")
325-
.unwrap()
326-
.as_ref()
327-
.map(|t| t.name.as_str()),
328-
Some("Payroll")
329-
);
330-
assert_eq!(
331-
teams.get("config/teams/payroll.yml").unwrap().as_ref().map(|t| t.name.as_str()),
332-
Some("Payroll")
333-
);
334-
assert_eq!(
335-
teams
336-
.get("ruby/app/models/bank_account.rb")
337-
.unwrap()
338-
.as_ref()
339-
.map(|t| t.name.as_str()),
340-
Some("Payments")
341-
);
342-
assert_eq!(teams.get("made/up/file.rb").unwrap().as_ref().map(|t| t.name.as_str()), None);
343-
assert_eq!(
344-
teams
345-
.get("ruby/ignored_files/git_ignored.rb")
346-
.unwrap()
347-
.as_ref()
348-
.map(|t| t.name.as_str()),
349-
None
350-
);
351-
assert_eq!(
352-
teams
353-
.get("ruby/ignored_files/git_ignored.rb")
354-
.unwrap()
355-
.as_ref()
356-
.map(|t| t.name.as_str()),
357-
None
358-
);
359-
}
360251
}

tests/runner_api.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::path::Path;
2+
3+
use codeowners::runner::{self, RunConfig};
4+
5+
fn write_file(temp_dir: &Path, file_path: &str, content: &str) {
6+
let file_path = temp_dir.join(file_path);
7+
let _ = std::fs::create_dir_all(file_path.parent().unwrap());
8+
std::fs::write(file_path, content).unwrap();
9+
}
10+
11+
#[test]
12+
fn test_file_owners_for_file() {
13+
let temp_dir = tempfile::tempdir().unwrap();
14+
const DEFAULT_CODE_OWNERSHIP_YML: &str = r#"---
15+
owned_globs:
16+
- "{app,components,config,frontend,lib,packs,spec,ruby}/**/*.{rb,rake,js,jsx,ts,tsx,json,yml,erb}"
17+
unowned_globs:
18+
- config/code_ownership.yml
19+
javascript_package_paths:
20+
- javascript/packages/**
21+
vendored_gems_path: gems
22+
team_file_glob:
23+
- config/teams/**/*.yml
24+
"#;
25+
write_file(temp_dir.path(), "config/code_ownership.yml", DEFAULT_CODE_OWNERSHIP_YML);
26+
["a", "b", "c"].iter().for_each(|name| {
27+
let team_yml = format!(
28+
"name: {}\ngithub:\n team: \"@{}\"\n members:\n - {}member\n",
29+
name, name, name
30+
);
31+
write_file(temp_dir.path(), &format!("config/teams/{}.yml", name), &team_yml);
32+
});
33+
write_file(
34+
temp_dir.path(),
35+
"app/consumers/deep/nesting/nestdir/deep_file.rb",
36+
"# @team b\nclass DeepFile end;",
37+
);
38+
39+
let run_config = RunConfig {
40+
project_root: temp_dir.path().to_path_buf(),
41+
codeowners_file_path: temp_dir.path().join(".github/CODEOWNERS").to_path_buf(),
42+
config_path: temp_dir.path().join("config/code_ownership.yml").to_path_buf(),
43+
no_cache: true,
44+
};
45+
46+
let file_owner = runner::file_owner_for_file(&run_config, "app/consumers/deep/nesting/nestdir/deep_file.rb")
47+
.unwrap()
48+
.unwrap();
49+
assert_eq!(file_owner.team.name, "b");
50+
assert_eq!(file_owner.team.github_team, "@b");
51+
assert!(file_owner.team.path.to_string_lossy().ends_with("config/teams/b.yml"));
52+
}
53+
54+
#[test]
55+
fn test_teams_for_files_from_codeowners() {
56+
let project_root = Path::new("tests/fixtures/valid_project");
57+
let file_paths = [
58+
"javascript/packages/items/item.ts",
59+
"config/teams/payroll.yml",
60+
"ruby/app/models/bank_account.rb",
61+
"made/up/file.rb",
62+
"ruby/ignored_files/git_ignored.rb",
63+
];
64+
let run_config = RunConfig {
65+
project_root: project_root.to_path_buf(),
66+
codeowners_file_path: project_root.join(".github/CODEOWNERS").to_path_buf(),
67+
config_path: project_root.join("config/code_ownership.yml").to_path_buf(),
68+
no_cache: true,
69+
};
70+
let teams = runner::teams_for_files_from_codeowners(
71+
&run_config,
72+
&file_paths.iter().map(|s| s.to_string()).collect::<Vec<String>>(),
73+
)
74+
.unwrap();
75+
assert_eq!(teams.len(), 5);
76+
assert_eq!(
77+
teams
78+
.get("javascript/packages/items/item.ts")
79+
.unwrap()
80+
.as_ref()
81+
.map(|t| t.name.as_str()),
82+
Some("Payroll")
83+
);
84+
assert_eq!(
85+
teams
86+
.get("config/teams/payroll.yml")
87+
.unwrap()
88+
.as_ref()
89+
.map(|t| t.name.as_str()),
90+
Some("Payroll")
91+
);
92+
assert_eq!(
93+
teams
94+
.get("ruby/app/models/bank_account.rb")
95+
.unwrap()
96+
.as_ref()
97+
.map(|t| t.name.as_str()),
98+
Some("Payments")
99+
);
100+
assert_eq!(teams.get("made/up/file.rb").unwrap().as_ref().map(|t| t.name.as_str()), None);
101+
assert_eq!(
102+
teams
103+
.get("ruby/ignored_files/git_ignored.rb")
104+
.unwrap()
105+
.as_ref()
106+
.map(|t| t.name.as_str()),
107+
None
108+
);
109+
}
110+
111+
#[test]
112+
fn test_for_team_reads_codeowners() {
113+
let td = tempfile::tempdir().unwrap();
114+
// minimal config
115+
const DEFAULT_CODE_OWNERSHIP_YML: &str = r#"---
116+
owned_globs:
117+
- "app/**/*"
118+
unowned_globs:
119+
- config/code_ownership.yml
120+
team_file_glob:
121+
- config/teams/**/*.yml
122+
vendored_gems_path: gems
123+
javascript_package_paths:
124+
- javascript/packages/**
125+
"#;
126+
write_file(td.path(), "config/code_ownership.yml", DEFAULT_CODE_OWNERSHIP_YML);
127+
128+
// team file for Foo
129+
write_file(
130+
td.path(),
131+
"config/teams/foo.yml",
132+
"name: Foo\ngithub:\n team: \"@Foo\"\n members:\n - user\n",
133+
);
134+
// provide a CODEOWNERS file referencing @Foo
135+
write_file(td.path(), ".github/CODEOWNERS", "/app/** @Foo\n");
136+
137+
let rc = RunConfig {
138+
project_root: td.path().to_path_buf(),
139+
codeowners_file_path: td.path().join(".github/CODEOWNERS"),
140+
config_path: td.path().join("config/code_ownership.yml"),
141+
no_cache: true,
142+
};
143+
144+
// Ensure CODEOWNERS file matches generator output to avoid out-of-date errors
145+
let _ = runner::generate(&rc, false);
146+
let res = runner::for_team(&rc, "Foo");
147+
assert!(res.io_errors.is_empty(), "unexpected io errors: {:?}", res.io_errors);
148+
assert!(res.validation_errors.is_empty());
149+
assert!(res.info_messages.iter().any(|m| m.contains("# Code Ownership Report for `Foo` Team")));
150+
}
151+
152+
#[test]
153+
fn test_validate_and_generate_and_validate() {
154+
let td = tempfile::tempdir().unwrap();
155+
// config and team so generation has inputs
156+
const DEFAULT_CODE_OWNERSHIP_YML: &str = r#"---
157+
owned_globs:
158+
- "**/*"
159+
team_file_glob:
160+
- config/teams/**/*.yml
161+
vendored_gems_path: gems
162+
javascript_package_paths:
163+
- javascript/packages/**
164+
"#;
165+
write_file(td.path(), "config/code_ownership.yml", DEFAULT_CODE_OWNERSHIP_YML);
166+
write_file(
167+
td.path(),
168+
"config/teams/foo.yml",
169+
"name: Foo\ngithub:\n team: \"@Foo\"\n members:\n - user\nowned_globs:\n - \"app/**\"\n - \"config/code_ownership.yml\"\n",
170+
);
171+
// create a file to be matched (no annotation to avoid multi-source ownership)
172+
write_file(td.path(), "app/x.rb", "puts :x\n");
173+
174+
let rc = RunConfig {
175+
project_root: td.path().to_path_buf(),
176+
codeowners_file_path: td.path().join(".github/CODEOWNERS"),
177+
config_path: td.path().join("config/code_ownership.yml"),
178+
no_cache: true,
179+
};
180+
181+
let gv = runner::generate_and_validate(&rc, vec![], true);
182+
assert!(gv.io_errors.is_empty(), "io: {:?}", gv.io_errors);
183+
assert!(gv.validation_errors.is_empty(), "val: {:?}", gv.validation_errors);
184+
// file should exist after generate
185+
let content = std::fs::read_to_string(td.path().join(".github/CODEOWNERS")).unwrap();
186+
assert!(!content.is_empty());
187+
}
188+
189+

0 commit comments

Comments
 (0)