workspace_tools 0.12.0

Reliable workspace-relative path resolution for Rust projects. Automatically finds your workspace root and provides consistent file path handling regardless of execution context. Features memory-safe secret management, configuration loading with validation, and resource discovery.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! comprehensive tests for `workspace_tools` functionality
//!
//! ## test matrix for workspace functionality
//!
//! | id   | aspect tested           | environment     | expected behavior       |
//! |------|-------------------------|-----------------|-------------------------|
//! | t1.1 | workspace resolution    | env var set     | resolves successfully   |
//! | t1.2 | workspace resolution    | env var missing | returns error          |
//! | t1.3 | workspace validation    | valid path      | validation succeeds     |
//! | t1.4 | workspace validation    | invalid path    | validation fails        |
//! | t2.1 | standard directories    | any workspace   | returns correct paths   |
//! | t2.2 | path joining           | relative paths  | joins correctly         |
//! | t2.3 | workspace boundaries    | internal path   | returns true           |
//! | t2.4 | workspace boundaries    | external path   | returns false          |
//! | t3.1 | fallback resolution     | no env, cwd     | uses current dir        |
//! | t3.2 | git root resolution     | git repo        | finds git root         |
//! | t4.1 | cross-platform paths    | any platform    | normalizes correctly    |

use workspace_tools :: { Workspace, WorkspaceError, workspace };
use tempfile ::TempDir;
use std :: { env, path ::PathBuf };
use std ::sync ::Mutex;

// Global mutex to serialize environment variable tests
static ENV_TEST_MUTEX: Mutex< () > = Mutex ::new( () );

/// test workspace resolution with environment variable set
/// test combination: t1.1
#[ test ]
fn test_workspace_resolution_with_env_var()
{
  let _lock = ENV_TEST_MUTEX.lock().unwrap();
  
  let temp_dir = TempDir ::new().unwrap();
  let original = env ::var( "WORKSPACE_PATH" ).ok();
  
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  assert_eq!( workspace.root(), temp_dir.path() );
  
  // restore original value
  match original
  {
  Some( value ) => env ::set_var( "WORKSPACE_PATH", value ),
  None => env ::remove_var( "WORKSPACE_PATH" ),
 }
}

/// test workspace resolution with missing environment variable
/// test combination: t1.2
#[ test ]
fn test_workspace_resolution_missing_env_var()
{
  env ::remove_var( "WORKSPACE_PATH" );
  
  let result = Workspace ::resolve();
  assert!( result.is_err() );
  
  match result.unwrap_err()
  {
  WorkspaceError ::EnvironmentVariableMissing( var ) =>
  {
   assert_eq!( var, "WORKSPACE_PATH" );
 }
  other => panic!( "expected EnvironmentVariableMissing, got {other:?}" ),
 }
}

/// test workspace validation with valid path
/// test combination: t1.3
#[ test ]
fn test_workspace_validation_valid_path()
{
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  let result = workspace.validate();
  
  assert!( result.is_ok() );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
}

/// test workspace validation with invalid path
/// test combination: t1.4
#[ test ]
fn test_workspace_validation_invalid_path()
{
  // Save original env var to restore later
  let original_workspace_path = env ::var( "WORKSPACE_PATH" ).ok();

  // Use platform-appropriate nonexistent path
  #[ cfg( windows ) ]
  let invalid_path = PathBuf ::from( "C:\\nonexistent\\workspace\\path\\12345" );
  #[ cfg( not( windows ) ) ]
  let invalid_path = PathBuf ::from( "/nonexistent/workspace/path/12345" );

  env ::set_var( "WORKSPACE_PATH", &invalid_path );

  let result = Workspace ::resolve();

  // Restore original environment immediately after resolve
  match original_workspace_path
  {
  Some( path ) => env ::set_var( "WORKSPACE_PATH", path ),
  None => env ::remove_var( "WORKSPACE_PATH" ),
 }

  // Now check the result
  assert!( result.is_err() );

  match result.unwrap_err()
  {
  WorkspaceError ::PathNotFound( path ) =>
  {
   assert_eq!( path, invalid_path );
 }
  other => panic!( "expected PathNotFound, got {other:?}" ),
 }
}

/// test standard directory paths
/// test combination: t2.1
#[ test ]
fn test_standard_directories()
{
  let temp_dir = TempDir ::new().unwrap();
  
  let workspace = Workspace ::new( temp_dir.path() );
  
  assert_eq!( workspace.config_dir(), temp_dir.path().join( "config" ) );
  assert_eq!( workspace.data_dir(), temp_dir.path().join( "data" ) );
  assert_eq!( workspace.logs_dir(), temp_dir.path().join( "logs" ) );
  assert_eq!( workspace.docs_dir(), temp_dir.path().join( "docs" ) );
  assert_eq!( workspace.tests_dir(), temp_dir.path().join( "tests" ) );
  assert_eq!( workspace.workspace_dir(), temp_dir.path().join( ".workspace" ) );
}

/// test path joining functionality
/// test combination: t2.2
#[ test ]
fn test_path_joining()
{
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  
  let joined = workspace.join( "config/app.toml" );
  let expected = temp_dir.path().join( "config/app.toml" );
  
  assert_eq!( joined, expected );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
}

/// test workspace boundary checking for internal paths
/// test combination: t2.3
#[ test ]
fn test_workspace_boundaries_internal()
{
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  let internal_path = workspace.join( "config/app.toml" );
  
  assert!( workspace.is_workspace_file( &internal_path ) );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
}

/// test workspace boundary checking for external paths
/// test combination: t2.4
#[ test ]
fn test_workspace_boundaries_external()
{
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  let external_path = PathBuf ::from( "/etc/passwd" );
  
  assert!( !workspace.is_workspace_file( &external_path ) );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
}

/// test fallback resolution behavior
/// test combination: t3.1
#[ test ]
fn test_fallback_resolution_current_dir()
{
  env ::remove_var( "WORKSPACE_PATH" );
  
  let workspace = Workspace ::resolve_with_extended_fallbacks();
  
  // with cargo integration enabled, should detect cargo workspace first
  #[ cfg( feature = "serde" ) ]
  {
  // should detect actual cargo workspace (not just fallback to current dir)
  assert!( workspace.is_cargo_workspace() );
  // workspace root should exist and be a directory
  assert!( workspace.root().exists() );
  assert!( workspace.root().is_dir() );
  // should contain a Cargo.toml with workspace configuration
  assert!( workspace.cargo_toml().exists() );
 }
  
  // without cargo integration, should fallback to current directory
  #[ cfg( not( feature = "serde" ) ) ]
  {
  let current_dir = env ::current_dir().unwrap();
  assert_eq!( workspace.root(), current_dir );
 }
}

/// test workspace creation from current directory
#[ test ]
fn test_from_current_dir()
{
  let workspace = Workspace ::from_current_dir().unwrap();
  let current_dir = env ::current_dir().unwrap();
  
  assert_eq!( workspace.root(), current_dir );
}

/// test convenience function
#[ test ]
fn test_convenience_function()
{
  // Save original env var and cwd to restore later
  let original_workspace_path = env ::var( "WORKSPACE_PATH" ).ok();
  let original_cwd = env ::current_dir().ok();

  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );

  // Change to temp directory to avoid finding cargo workspace
  let test_cwd = TempDir ::new().unwrap();
  env ::set_current_dir( test_cwd.path() ).ok();

  let ws = workspace().unwrap();
  assert_eq!( ws.root(), temp_dir.path() );

  // Restore original environment
  if let Some( cwd ) = original_cwd
  {
   env ::set_current_dir( cwd ).ok();
  }
  match original_workspace_path
  {
  Some( path ) => env ::set_var( "WORKSPACE_PATH", path ),
  None => env ::remove_var( "WORKSPACE_PATH" ),
 }
}

/// test error display formatting
#[ test ]
fn test_error_display()
{
  let error = WorkspaceError ::EnvironmentVariableMissing( "TEST_VAR".to_string() );
  let display = format!( "{error}" );
  
  assert!( display.contains( "TEST_VAR" ) );
  assert!( display.contains( "WORKSPACE_PATH" ) );
}

/// test workspace creation with testing utilities
#[ test ]
#[ cfg( feature = "testing" ) ]
fn test_testing_utilities()
{
  use workspace_tools ::testing :: { create_test_workspace, create_test_workspace_with_structure };

  // test basic workspace creation
  let ( _temp_dir, workspace ) = create_test_workspace();
  assert!( workspace.root().exists() );

  // test workspace with structure
  let ( _temp_dir, workspace ) = create_test_workspace_with_structure();
  assert!( workspace.config_dir().exists() );
  assert!( workspace.data_dir().exists() );
  assert!( workspace.logs_dir().exists() );
}

#[ cfg( feature = "secrets" ) ]
mod secret_management_tests
{
  use super :: *;
  use std ::fs;
  
  /// test secret directory path
  #[ test ]
  fn test_secret_directory()
  {
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  assert_eq!( workspace.secret_dir(), temp_dir.path().join( "secret" ) );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
 }
  
  /// test secret file loading
  #[ test ]
  fn test_secret_file_loading()
  {
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  
  // create secret directory and file
  let secret_dir = workspace.secret_dir();
  fs ::create_dir_all( &secret_dir ).unwrap();
  
  let secret_file = secret_dir.join( "test.env" );
  fs ::write( &secret_file, "API_KEY=secret123\nDB_URL=postgres: //localhost\n# comment\n" ).unwrap();
  
  // load secrets
  let secrets = workspace.load_secrets_from_file( "test.env" ).unwrap();
  
  assert_eq!( secrets.get( "API_KEY" ), Some( &"secret123".to_string() ) );
  assert_eq!( secrets.get( "DB_URL" ), Some( &"postgres: //localhost".to_string() ) );
  assert!( !secrets.contains_key( "comment" ) );
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
 }
  
  /// test secret key loading with fallback
  #[ test ]
  fn test_secret_key_loading_with_fallback()
  {
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "TEST_ENV_KEY", "env_value" );
  
  let workspace = Workspace ::new( temp_dir.path() );
  
  // test fallback to environment variable
  let value = workspace.load_secret_key( "TEST_ENV_KEY", "nonexistent.env" ).unwrap();
  assert_eq!( value, "env_value" );
  
  // cleanup
  env ::remove_var( "TEST_ENV_KEY" );
 }
}

#[ cfg( feature = "glob" ) ]
mod glob_tests
{
  use super :: *;
  use std ::fs;
  
  /// test resource discovery with glob patterns
  #[ test ]
  fn test_find_resources()
  {
  let temp_dir = TempDir ::new().unwrap();
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  
  // create test files
  let src_dir = workspace.join( "src" );
  fs ::create_dir_all( &src_dir ).unwrap();
  
  let test_files = vec![ "lib.rs", "main.rs", "mod.rs" ];
  for file in &test_files
  {
   fs ::write( src_dir.join( file ), "// test content" ).unwrap();
 }
  
  // find rust files
  let found = workspace.find_resources( "src/*.rs" ).unwrap();
  assert_eq!( found.len(), 3 );
  
  // all found files should be rust files
  for path in found
  {
   assert!( path.extension().unwrap() == "rs" );
   assert!( workspace.is_workspace_file( &path ) );
 }
  
  // cleanup
  env ::remove_var( "WORKSPACE_PATH" );
 }
  
  /// test configuration file discovery
  #[ test ]
  fn test_find_config()
  {
  let temp_dir = TempDir ::new().unwrap();
  let original = env ::var( "WORKSPACE_PATH" ).ok();
  
  env ::set_var( "WORKSPACE_PATH", temp_dir.path() );
  
  let workspace = Workspace ::resolve().unwrap();
  
  // create config directory and file
  let config_dir = workspace.config_dir();
  fs ::create_dir_all( &config_dir ).unwrap();
  
  let config_file = config_dir.join( "app.toml" );
  fs ::write( &config_file, "[app]\nname = \"test\"\n" ).unwrap();
  
  // find config
  let found = workspace.find_config( "app" ).unwrap();
  assert_eq!( found, config_file );
  
  // restore environment
  match original
  {
   Some( value ) => env ::set_var( "WORKSPACE_PATH", value ),
   None => env ::remove_var( "WORKSPACE_PATH" ),
 }
 }
  
  /// test config file discovery with multiple extensions
  #[ test ]
  fn test_find_config_multiple_extensions()
  {
  let temp_dir = TempDir ::new().unwrap();
  
  let workspace = Workspace ::new( temp_dir.path() );
  
  // create config directory
  let config_dir = workspace.config_dir();
  fs ::create_dir_all( &config_dir ).unwrap();
  
  // create yaml config (should be found before json)
  let yaml_config = config_dir.join( "database.yaml" );
  fs ::write( &yaml_config, "host: localhost\n" ).unwrap();
  
  let json_config = config_dir.join( "database.json" );
  fs ::write( &json_config, "{\"host\" : \"localhost\"}\n" ).unwrap();
  
  // should find yaml first (based on search order)
  let found = workspace.find_config( "database" ).unwrap();
  assert_eq!( found, yaml_config );
 }
}