@@ -15,6 +15,7 @@ use serde::Deserialize;
1515use std:: collections:: HashMap ;
1616use std:: env;
1717use std:: path:: PathBuf ;
18+ use std:: sync:: LazyLock ;
1819use uuid:: Uuid ;
1920
2021#[ derive( Clone , Debug , Deserialize ) ]
@@ -148,25 +149,29 @@ impl TandemConfig {
148149 . add_source ( stash_setup_source)
149150 . build ( ) ?
150151 . try_deserialize ( )
151- . map_err ( |err| match err {
152- config:: ConfigError :: Message ( ref s) => match s {
152+ . map_err ( |err| {
153+ // ConfigError is not helping here
154+ // - does not carry the information in structured form
155+ // - missing parameters are returned by at least two different errors, depending the source of the error
156+ // Easier to inspect the error message.
157+ match err. to_string ( ) {
153158 s if s. contains ( "UUID parsing failed" ) => ConfigError :: InvalidDatasetId ,
154159 s if s. contains ( "missing field" ) => {
155- let mut name = extract_field_name ( s) . map_or ( "unknown" . to_string ( ) , |s| s) ;
156-
157- if name == "name" {
158- name = "database.name" . to_string ( ) ;
160+ let ( field, key) = extract_missing_field_and_key ( & s) ;
161+ match ( field, key) {
162+ ( field, None ) if field == "auth" => ConfigError :: MissingAuthKey ,
163+ ( field, None ) if field == "encrypt" => ConfigError :: MissingEncryptKey ,
164+ ( field, None ) if field == "database" => ConfigError :: MissingDatabaseKey ,
165+ ( field, None ) => ConfigError :: MissingField { field } ,
166+ ( field, Some ( key) ) => ConfigError :: MissingFieldForKey { key, field } ,
159167 }
160-
161- ConfigError :: MissingParameter { name }
162168 }
163169 s if s. contains ( "does not have variant constructor" ) => {
164- let ( name, value) = extract_invalid_field ( s) ;
170+ let ( name, value) = extract_invalid_field ( & s) ;
165171 ConfigError :: InvalidParameter { name, value }
166172 }
167173 _ => err. into ( ) ,
168- } ,
169- _ => err. into ( ) ,
174+ }
170175 } ) ?;
171176
172177 Ok ( config)
@@ -254,18 +259,29 @@ impl Default for PrometheusConfig {
254259 }
255260}
256261
262+ static RE : LazyLock < Regex > = LazyLock :: new ( || Regex :: new ( r"`([^`]+)`" ) . unwrap ( ) ) ;
263+
257264///
258- /// Extracts a field name (if present) from a config::ConfigError::Message
265+ /// Extracts a field name (if present) from a config::ConfigError string
259266/// This is called in `build` if a ConfigError message contains the string `missing field`
267+ /// Expected string is in the forms:
268+ /// "missing field `{field}}` for key `{key}`"
269+ /// "missing field `{field}}`"
260270///
261- fn extract_field_name ( input : & str ) -> Option < String > {
262- let re = Regex :: new ( r"`(\w+)`" ) . unwrap ( ) ;
263- re. captures ( input)
264- . and_then ( |caps| caps. get ( 1 ) . map ( |m| m. as_str ( ) . to_string ( ) ) )
271+ fn extract_missing_field_and_key ( input : & str ) -> ( String , Option < String > ) {
272+ let default = "unknown" ;
273+ let values = RE
274+ . find_iter ( input)
275+ . map ( |m| m. as_str ( ) . trim_matches ( '`' ) )
276+ . collect :: < Vec < _ > > ( ) ;
277+ (
278+ values. first ( ) . map_or ( default. to_owned ( ) , |s| s. to_string ( ) ) ,
279+ values. get ( 1 ) . map ( |s| s. to_string ( ) ) ,
280+ )
265281}
266282
267283///
268- /// Extracts a field name (if present) from a config::ConfigError::Message
284+ /// Extracts a field name (if present) from a config::ConfigError string
269285/// This is called in `build` if a ConfigError message contains the string `does not have variant constructor`
270286///
271287/// Error string is `enum {name} does not have variant constructor {value}`
@@ -293,10 +309,14 @@ fn extract_invalid_field(input: &str) -> (String, String) {
293309
294310#[ cfg( test) ]
295311mod tests {
296- use crate :: { config:: TandemConfig , error:: Error } ;
312+ use crate :: {
313+ config:: { tandem:: extract_missing_field_and_key, TandemConfig } ,
314+ error:: Error ,
315+ } ;
297316 use cipherstash_client:: config:: vars:: {
298317 CS_CLIENT_ACCESS_KEY , CS_CLIENT_ID , CS_CLIENT_KEY , CS_DEFAULT_KEYSET_ID , CS_WORKSPACE_ID ,
299318 } ;
319+ use std:: collections:: HashMap ;
300320 use uuid:: Uuid ;
301321
302322 const CS_PREFIX : & str = "CS_TEST" ;
@@ -419,4 +439,176 @@ mod tests {
419439 } ,
420440 ) ;
421441 }
442+
443+ #[ test]
444+ /// the env vars from stash setup should be the preferred option
445+ /// File -> extended env (generated by the config struct layout) -> stash setup env
446+ fn extract_field_and_key_name_from_config_error ( ) {
447+ let s = "missing field `client_access_key` for key `auth`" ;
448+
449+ let ( field, key) = extract_missing_field_and_key ( s) ;
450+
451+ assert_eq ! ( field, "client_access_key" . to_string( ) ) ;
452+ assert_eq ! ( key. unwrap( ) , "auth" . to_string( ) ) ;
453+
454+ // Bad input - auth is extracted as field
455+ let s = "blah {client_access_key} for vtha `auth`" ;
456+
457+ let ( field, key) = extract_missing_field_and_key ( s) ;
458+
459+ assert_eq ! ( field, "auth" . to_string( ) ) ;
460+ assert ! ( key. is_none( ) ) ;
461+
462+ // Bad input - no field can be extracted is extracted as field
463+ let s = "blah {client_access_key} for vtha auth" ;
464+
465+ let ( field, key) = extract_missing_field_and_key ( s) ;
466+
467+ assert_eq ! ( field, "unknown" . to_string( ) ) ;
468+ assert ! ( key. is_none( ) ) ;
469+ }
470+
471+ /// Returns the default environment variables as a Vec
472+ fn default_env_vars ( ) -> Vec < ( & ' static str , Option < & ' static str > ) > {
473+ vec ! [
474+ ( "CS_CLIENT_ID" , Some ( "00000000-0000-0000-0000-000000000000" ) ) ,
475+ ( "CS_CLIENT_KEY" , Some ( "CS_CLIENT_KEY" ) ) ,
476+ (
477+ "CS_DEFAULT_KEYSET_ID" ,
478+ Some ( "00000000-0000-0000-0000-000000000000" ) ,
479+ ) ,
480+ ( "CS_WORKSPACE_ID" , Some ( "CS_WORKSPACE_ID" ) ) ,
481+ ( "CS_CLIENT_ACCESS_KEY" , Some ( "CS_CLIENT_ACCESS_KEY" ) ) ,
482+ ( "CS_DATABASE__USERNAME" , Some ( "CS_DATABASE__USERNAME" ) ) ,
483+ ( "CS_DATABASE__PASSWORD" , Some ( "CS_DATABASE__PASSWORD" ) ) ,
484+ ( "CS_DATABASE__NAME" , Some ( "CS_DATABASE__NAME" ) ) ,
485+ ]
486+ }
487+
488+ /// Merges the default environment variables with overrides
489+ fn merge_env_vars (
490+ overrides : Vec < ( & ' static str , Option < & ' static str > ) > ,
491+ ) -> Vec < ( & ' static str , Option < & ' static str > ) > {
492+ let mut env_map: HashMap < & str , Option < & str > > = default_env_vars ( ) . into_iter ( ) . collect ( ) ;
493+
494+ for ( key, value) in overrides {
495+ env_map. insert ( key, value) ;
496+ }
497+
498+ env_map. into_iter ( ) . collect ( )
499+ }
500+
501+ #[ test]
502+ fn missing_auth_config ( ) {
503+ // Missing both workspace id and client_access_key
504+
505+ let env = merge_env_vars ( vec ! [
506+ ( "CS_WORKSPACE_ID" , None ) ,
507+ ( "CS_CLIENT_ACCESS_KEY" , None ) ,
508+ ] ) ;
509+
510+ temp_env:: with_vars ( env, || {
511+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
512+ assert ! ( result. is_err( ) ) ;
513+
514+ if let Err ( err) = result {
515+ assert ! ( err. to_string( ) . contains( "Missing [auth] configuration" ) ) ;
516+ } else {
517+ unreachable ! ( ) ;
518+ }
519+ } ) ;
520+
521+ // Missing client_access_key
522+ let env = merge_env_vars ( vec ! [ ( "CS_CLIENT_ACCESS_KEY" , None ) ] ) ;
523+
524+ temp_env:: with_vars ( env, || {
525+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
526+ assert ! ( result. is_err( ) ) ;
527+
528+ if let Err ( err) = result {
529+ assert ! ( err
530+ . to_string( )
531+ . contains( "Missing client_access_key from [auth] configuration." ) ) ;
532+ } else {
533+ unreachable ! ( ) ;
534+ }
535+ } ) ;
536+ }
537+
538+ #[ test]
539+ fn missing_encrypt_config ( ) {
540+ // Missing all encrypt config
541+ let env = merge_env_vars ( vec ! [
542+ ( "CS_CLIENT_ID" , None ) ,
543+ ( "CS_CLIENT_KEY" , None ) ,
544+ ( "CS_DEFAULT_KEYSET_ID" , None ) ,
545+ ] ) ;
546+
547+ temp_env:: with_vars ( env, || {
548+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
549+ assert ! ( result. is_err( ) ) ;
550+
551+ if let Err ( err) = result {
552+ assert ! ( err. to_string( ) . contains( "Missing [encrypt] configuration" ) ) ;
553+ } else {
554+ unreachable ! ( ) ;
555+ }
556+ } ) ;
557+
558+ // Missing client_id
559+ let env = merge_env_vars ( vec ! [ ( "CS_CLIENT_ID" , None ) ] ) ;
560+
561+ temp_env:: with_vars ( env, || {
562+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
563+ assert ! ( result. is_err( ) ) ;
564+
565+ if let Err ( err) = result {
566+ assert ! ( err
567+ . to_string( )
568+ . contains( "Missing client_id from [encrypt] configuration." ) ) ;
569+ } else {
570+ unreachable ! ( ) ;
571+ }
572+ } ) ;
573+ }
574+
575+ #[ test]
576+ fn missing_database_config ( ) {
577+ // Missing all database config
578+
579+ let env = merge_env_vars ( vec ! [
580+ ( "CS_DATABASE__USERNAME" , None ) ,
581+ ( "CS_DATABASE__PASSWORD" , None ) ,
582+ ( "CS_DATABASE__NAME" , None ) ,
583+ ( "CS_DATABASE__HOST" , None ) ,
584+ ( "CS_DATABASE__PORT" , None ) ,
585+ ] ) ;
586+
587+ temp_env:: with_vars ( env, || {
588+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
589+ assert ! ( result. is_err( ) ) ;
590+
591+ if let Err ( err) = result {
592+ assert ! ( err. to_string( ) . contains( "Missing [database] configuration" ) ) ;
593+ } else {
594+ unreachable ! ( ) ;
595+ }
596+ } ) ;
597+
598+ // Missing name
599+ let env = merge_env_vars ( vec ! [ ( "CS_DATABASE__NAME" , None ) ] ) ;
600+
601+ temp_env:: with_vars ( env, || {
602+ let result = TandemConfig :: build ( "tests/config/unknown.toml" ) ;
603+ assert ! ( result. is_err( ) ) ;
604+
605+ if let Err ( err) = result {
606+ assert ! ( err
607+ . to_string( )
608+ . contains( "Missing name from [database] configuration." ) ) ;
609+ } else {
610+ unreachable ! ( ) ;
611+ }
612+ } ) ;
613+ }
422614}
0 commit comments