Skip to content

Commit 3bd106c

Browse files
committed
feat: expose library crate for programmatic API access
Extract `config_dir()` into `src/config.rs` and Model Armor sanitization types into `src/sanitize.rs` so they can be shared between the binary and library targets without pulling in CLI-only code. Add `src/lib.rs` with public module re-exports and `tests/lib_integration.rs` with offline tests. Also moves `parse_service_and_version()` from `main.rs` to `services.rs` so it is accessible from both the lib and bin crate roots. Zero behavior changes to the binary.
1 parent 24a25a6 commit 3bd106c

17 files changed

Lines changed: 411 additions & 205 deletions

.changeset/add-library-crate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@googleworkspace/cli": minor
3+
---
4+
5+
Expose library crate (`lib.rs`) for programmatic API access. Extracts `config_dir()` and Model Armor sanitization types into standalone modules so they can be shared between the binary and library targets without pulling in CLI-only code.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ authors = ["Justin Poehnelt"]
2525
keywords = ["cli", "google-workspace", "google", "drive", "gmail"]
2626
categories = ["command-line-utilities", "web-programming"]
2727

28+
[lib]
29+
name = "gws"
30+
path = "src/lib.rs"
31+
2832
[[bin]]
2933
name = "gws"
3034
path = "src/main.rs"

src/accounts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub fn email_to_b64(email: &str) -> String {
6868

6969
/// Path to `accounts.json` inside the config directory.
7070
pub fn accounts_path() -> PathBuf {
71-
crate::auth_commands::config_dir().join("accounts.json")
71+
crate::config::config_dir().join("accounts.json")
7272
}
7373

7474
/// Load the accounts registry from disk. Returns `None` if the file does not

src/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub async fn get_token(scopes: &[&str], account: Option<&str>) -> anyhow::Result
6868

6969
let creds_file = std::env::var("GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE").ok();
7070
let impersonated_user = std::env::var("GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER").ok();
71-
let config_dir = crate::auth_commands::config_dir();
71+
let config_dir = crate::config::config_dir();
7272

7373
// If env var credentials are specified, skip account resolution entirely
7474
if creds_file.is_some() {

src/auth_commands.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -92,30 +92,7 @@ const READONLY_SCOPES: &[&str] = &[
9292
];
9393

9494
pub fn config_dir() -> PathBuf {
95-
#[cfg(test)]
96-
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
97-
return PathBuf::from(dir);
98-
}
99-
100-
// Use ~/.config/gws on all platforms for a consistent, user-friendly path.
101-
let primary = dirs::home_dir()
102-
.unwrap_or_else(|| PathBuf::from("."))
103-
.join(".config")
104-
.join("gws");
105-
if primary.exists() {
106-
return primary;
107-
}
108-
109-
// Backward compat: fall back to OS-specific config dir for existing installs
110-
// (e.g. ~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows).
111-
let legacy = dirs::config_dir()
112-
.unwrap_or_else(|| PathBuf::from("."))
113-
.join("gws");
114-
if legacy.exists() {
115-
return legacy;
116-
}
117-
118-
primary
95+
crate::config::config_dir()
11996
}
12097

12198
fn plain_credentials_path() -> PathBuf {

src/config.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::path::PathBuf;
16+
17+
/// Returns the gws configuration directory.
18+
///
19+
/// Prefers `~/.config/gws` for a consistent, cross-platform path.
20+
/// Falls back to the OS-specific config directory (e.g. `~/Library/Application Support/gws`
21+
/// on macOS) for backward compatibility with existing installs.
22+
///
23+
/// In tests, the `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` environment variable overrides the default.
24+
pub fn config_dir() -> PathBuf {
25+
#[cfg(test)]
26+
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
27+
return PathBuf::from(dir);
28+
}
29+
30+
// Use ~/.config/gws on all platforms for a consistent, user-friendly path.
31+
let primary = dirs::home_dir()
32+
.unwrap_or_else(|| PathBuf::from("."))
33+
.join(".config")
34+
.join("gws");
35+
if primary.exists() {
36+
return primary;
37+
}
38+
39+
// Backward compat: fall back to OS-specific config dir for existing installs
40+
// (e.g. ~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows).
41+
let legacy = dirs::config_dir()
42+
.unwrap_or_else(|| PathBuf::from("."))
43+
.join("gws");
44+
if legacy.exists() {
45+
return legacy;
46+
}
47+
48+
primary
49+
}

src/credential_store.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
4545
.or_else(|_| std::env::var("USERNAME"))
4646
.unwrap_or_else(|_| "unknown-user".to_string());
4747

48-
let key_file = crate::auth_commands::config_dir().join(".encryption_key");
48+
let key_file = crate::config::config_dir().join(".encryption_key");
4949

5050
let entry = Entry::new("gws-cli", &username);
5151

@@ -216,7 +216,7 @@ pub fn decrypt(data: &[u8]) -> anyhow::Result<Vec<u8>> {
216216

217217
/// Returns the path for encrypted credentials.
218218
pub fn encrypted_credentials_path() -> PathBuf {
219-
crate::auth_commands::config_dir().join("credentials.enc")
219+
crate::config::config_dir().join("credentials.enc")
220220
}
221221

222222
/// Saves credentials JSON to an encrypted file.
@@ -278,7 +278,7 @@ pub fn load_encrypted() -> anyhow::Result<String> {
278278
pub fn encrypted_credentials_path_for(account: &str) -> PathBuf {
279279
let normalised = crate::accounts::normalize_email(account);
280280
let b64 = crate::accounts::email_to_b64(&normalised);
281-
crate::auth_commands::config_dir().join(format!("credentials.{b64}.enc"))
281+
crate::config::config_dir().join(format!("credentials.{b64}.enc"))
282282
}
283283

284284
/// Saves credentials JSON to a per-account encrypted file.

src/discovery.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ pub async fn fetch_discovery_document(
195195
let version =
196196
crate::validate::validate_api_identifier(version).map_err(|e| anyhow::anyhow!("{e}"))?;
197197

198-
let cache_dir = crate::auth_commands::config_dir().join("cache");
198+
let cache_dir = crate::config::config_dir().join("cache");
199199
std::fs::create_dir_all(&cache_dir)?;
200200

201201
let cache_file = cache_dir.join(format!("{service}_{version}.json"));

src/executor.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ async fn handle_json_response(
206206
body_text: &str,
207207
pagination: &PaginationConfig,
208208
sanitize_template: Option<&str>,
209-
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
209+
sanitize_mode: &crate::sanitize::SanitizeMode,
210210
output_format: &crate::formatter::OutputFormat,
211211
pages_fetched: &mut u32,
212212
page_token: &mut Option<String>,
@@ -219,15 +219,14 @@ async fn handle_json_response(
219219
// Run Model Armor sanitization if --sanitize is enabled
220220
if let Some(template) = sanitize_template {
221221
let text_to_check = serde_json::to_string(&json_val).unwrap_or_default();
222-
match crate::helpers::modelarmor::sanitize_text(template, &text_to_check).await {
222+
match crate::sanitize::sanitize_text(template, &text_to_check).await {
223223
Ok(result) => {
224224
let is_match = result.filter_match_state == "MATCH_FOUND";
225225
if is_match {
226226
eprintln!("⚠️ Model Armor: prompt injection detected (filterMatchState: MATCH_FOUND)");
227227
}
228228

229-
if is_match && *sanitize_mode == crate::helpers::modelarmor::SanitizeMode::Block
230-
{
229+
if is_match && *sanitize_mode == crate::sanitize::SanitizeMode::Block {
231230
let blocked = serde_json::json!({
232231
"error": "Content blocked by Model Armor",
233232
"sanitizationResult": serde_json::to_value(&result).unwrap_or_default(),
@@ -365,7 +364,7 @@ pub async fn execute_method(
365364
dry_run: bool,
366365
pagination: &PaginationConfig,
367366
sanitize_template: Option<&str>,
368-
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
367+
sanitize_mode: &crate::sanitize::SanitizeMode,
369368
output_format: &crate::formatter::OutputFormat,
370369
capture_output: bool,
371370
) -> Result<Option<Value>, GwsError> {
@@ -1613,7 +1612,7 @@ async fn test_execute_method_dry_run() {
16131612
let params_json = r#"{"fileId": "123"}"#;
16141613
let body_json = r#"{"name": "test.txt"}"#;
16151614

1616-
let sanitize_mode = crate::helpers::modelarmor::SanitizeMode::Warn;
1615+
let sanitize_mode = crate::sanitize::SanitizeMode::Warn;
16171616
let pagination = PaginationConfig::default();
16181617

16191618
let result = execute_method(
@@ -1658,7 +1657,7 @@ async fn test_execute_method_missing_path_param() {
16581657
..Default::default()
16591658
};
16601659

1661-
let sanitize_mode = crate::helpers::modelarmor::SanitizeMode::Warn;
1660+
let sanitize_mode = crate::sanitize::SanitizeMode::Warn;
16621661
let result = execute_method(
16631662
&doc,
16641663
&method,

0 commit comments

Comments
 (0)