1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8const EXPECTED_BOOLEAN: &str = "one of: true, false, 1, 0, yes, no, on, off";
9const EXPECTED_SQLSTATES: &str = "true, false, or a comma-separated list of SQLSTATEs \
10 (P0001) and prefixes ending in * (PT*)";
11
12#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
14#[error("invalid value for {variable}: {value:?}; expected {expected}")]
15pub struct ConfigError {
16 variable: &'static str,
17 value: String,
18 expected: &'static str,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct AppConfig {
25 #[serde(default = "default_db_uri")]
30 pub db_uri: String,
31
32 #[serde(default = "default_db_schemas")]
34 pub db_schemas: Vec<String>,
35
36 pub db_anon_role: Option<String>,
38
39 #[serde(default = "default_pool_size")]
41 pub db_pool_size: u32,
42
43 #[serde(default = "default_pool_timeout")]
45 pub db_pool_timeout: u64,
46
47 #[serde(default = "default_tx_isolation")]
53 pub db_tx_isolation: IsolationLevel,
54
55 #[serde(default = "default_true")]
57 pub db_prepared_statements: bool,
58
59 #[serde(default = "default_extra_search_path")]
67 pub db_extra_search_path: Vec<String>,
68
69 #[serde(default = "default_db_channel")]
71 pub db_channel: String,
72
73 #[serde(default)]
75 pub db_channel_enabled: bool,
76
77 pub db_pre_request: Option<String>,
80
81 pub db_max_rows: Option<i64>,
83
84 #[serde(default)]
89 pub db_aggregates_enabled: bool,
90
91 #[serde(default = "default_host")]
96 pub server_host: String,
97
98 #[serde(default = "default_port")]
100 pub server_port: u16,
101
102 #[serde(default)]
110 pub server_cors_origins: Vec<String>,
111
112 #[serde(default = "default_max_body_size")]
119 pub max_body_size: usize,
120
121 pub server_unix_socket: Option<String>,
123
124 pub admin_server_port: Option<u16>,
126
127 pub jwt_secret: Option<String>,
132
133 #[serde(default)]
135 pub jwt_secret_is_base64: bool,
136
137 pub jwt_aud: Option<String>,
139
140 pub jwt_iss: Option<String>,
143
144 #[serde(default = "default_jwt_role_claim")]
146 pub jwt_role_claim_key: String,
147
148 #[serde(default = "default_true")]
150 pub jwt_cache_enabled: bool,
151
152 #[serde(default = "default_jwt_cache_max")]
157 pub jwt_cache_max_lifetime: u64,
158
159 pub jwt_jwks_url: Option<String>,
166
167 #[serde(default = "default_jwt_jwks_refresh")]
170 pub jwt_jwks_refresh: u64,
171
172 pub hasura_admin_secret: Option<String>,
185
186 pub hasura_unauthorized_role: Option<String>,
190
191 pub openapi_server_proxy_uri: Option<String>,
198
199 #[serde(default = "default_openapi_mode")]
201 pub openapi_mode: OpenApiMode,
202
203 #[serde(default = "default_log_level")]
208 pub log_level: LogLevel,
209
210 #[serde(default)]
219 pub log_format: LogFormat,
220
221 #[serde(default)]
229 pub otel_db_statement: bool,
230
231 #[serde(default)]
236 pub role_settings: HashMap<String, RoleSettings>,
237
238 #[serde(default)]
240 pub app_settings: HashMap<String, String>,
241
242 #[serde(default)]
247 pub graphql_federation: bool,
248
249 #[serde(default)]
251 pub graphql_type_prefix: Option<String>,
252
253 #[serde(default)]
265 pub compat_mode: bool,
266
267 #[serde(default, deserialize_with = "deserialize_sqlstate_patterns")]
278 pub db_errors_verbatim: Vec<String>,
279}
280
281impl Default for AppConfig {
282 fn default() -> Self {
283 Self {
284 db_uri: default_db_uri(),
285 db_schemas: default_db_schemas(),
286 db_anon_role: None,
287 db_pool_size: default_pool_size(),
288 db_pool_timeout: default_pool_timeout(),
289 db_tx_isolation: default_tx_isolation(),
290 db_prepared_statements: true,
291 db_extra_search_path: default_extra_search_path(),
292 db_channel: default_db_channel(),
293 db_channel_enabled: false,
294 db_pre_request: None,
295 db_max_rows: None,
296 db_aggregates_enabled: false,
297 server_host: default_host(),
298 server_port: default_port(),
299 server_cors_origins: Vec::new(),
300 max_body_size: default_max_body_size(),
301 server_unix_socket: None,
302 admin_server_port: None,
303 jwt_secret: None,
304 jwt_secret_is_base64: false,
305 jwt_aud: None,
306 jwt_iss: None,
307 jwt_role_claim_key: default_jwt_role_claim(),
308 jwt_cache_enabled: true,
309 jwt_cache_max_lifetime: default_jwt_cache_max(),
310 jwt_jwks_url: None,
311 jwt_jwks_refresh: default_jwt_jwks_refresh(),
312 hasura_admin_secret: None,
313 hasura_unauthorized_role: None,
314 openapi_server_proxy_uri: None,
315 openapi_mode: OpenApiMode::FollowPrivileges,
316 log_level: default_log_level(),
317 log_format: LogFormat::Text,
318 otel_db_statement: false,
319 role_settings: HashMap::new(),
320 app_settings: HashMap::new(),
321 graphql_federation: false,
322 graphql_type_prefix: None,
323 compat_mode: false,
324 db_errors_verbatim: Vec::new(),
325 }
326 }
327}
328
329fn warn_ignored(var: &str, value: &str, expected: &str) {
333 tracing::warn!("Ignoring {}={:?}: expected {}", var, value, expected);
334}
335
336fn parse_bool(value: &str) -> Option<bool> {
338 match value.trim().to_ascii_lowercase().as_str() {
339 "true" | "1" | "yes" | "on" => Some(true),
340 "false" | "0" | "no" | "off" => Some(false),
341 _ => None,
342 }
343}
344
345fn invalid(variable: &'static str, value: &str, expected: &'static str) -> ConfigError {
348 let error = ConfigError {
349 variable,
350 value: value.to_string(),
351 expected,
352 };
353 tracing::warn!("{error}");
354 error
355}
356
357fn env_bool(variable: &'static str, value: &str) -> Result<bool, ConfigError> {
358 parse_bool(value).ok_or_else(|| invalid(variable, value, EXPECTED_BOOLEAN))
359}
360
361fn parse_sqlstate_patterns(value: &str) -> Option<Vec<String>> {
367 match parse_bool(value) {
368 Some(true) => return Some(vec!["*".to_string()]),
369 Some(false) => return Some(Vec::new()),
370 None => {}
371 }
372 let sqlstate_chars = |s: &str| s.chars().all(|c| c.is_ascii_alphanumeric());
373 value
374 .split(',')
375 .map(|s| s.trim().to_ascii_uppercase())
376 .filter(|s| !s.is_empty())
377 .map(|s| {
378 let valid = match s.strip_suffix('*') {
379 Some(prefix) => prefix.len() < 5 && sqlstate_chars(prefix),
380 None => s.len() == 5 && sqlstate_chars(&s),
381 };
382 valid.then_some(s)
383 })
384 .collect()
385}
386
387fn deserialize_sqlstate_patterns<'de, D: serde::Deserializer<'de>>(
391 deserializer: D,
392) -> Result<Vec<String>, D::Error> {
393 let raw = Vec::<String>::deserialize(deserializer)?.join(",");
394 parse_sqlstate_patterns(&raw).ok_or_else(|| {
395 serde::de::Error::custom(format!(
396 "invalid db_errors_verbatim {raw:?}: expected {EXPECTED_SQLSTATES}"
397 ))
398 })
399}
400
401fn sqlstate_matches(pattern: &str, code: &str) -> bool {
404 match pattern.strip_suffix('*') {
405 Some(prefix) => code.starts_with(prefix),
406 None => pattern == code,
407 }
408}
409
410fn parse_graphql_type_prefix(value: &str) -> Result<Option<String>, &'static str> {
412 let value = value.trim();
413 if value.is_empty() {
414 return Ok(None);
415 }
416
417 let mut chars = value.chars();
418 let valid_first = chars
419 .next()
420 .is_some_and(|c| c == '_' || c.is_ascii_alphabetic());
421 if !valid_first || !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
422 return Err("a GraphQL identifier beginning with a letter or underscore");
423 }
424
425 Ok(Some(value.to_string()))
426}
427
428fn env_name(name: String) -> String {
434 if cfg!(windows) {
435 upper_env_name(name)
436 } else {
437 name
438 }
439}
440
441fn upper_env_name(mut name: String) -> String {
442 name.make_ascii_uppercase();
443 name
444}
445
446impl AppConfig {
447 pub fn from_env() -> Self {
454 Self::try_from_env().unwrap_or_else(|error| panic!("{error}"))
455 }
456
457 pub fn try_from_env() -> Result<Self, ConfigError> {
460 Self::try_from_vars(
463 std::env::vars_os().filter_map(|(k, v)| {
464 Some((env_name(k.into_string().ok()?), v.into_string().ok()?))
465 }),
466 )
467 }
468
469 pub fn try_from_vars<I, K, V>(vars: I) -> Result<Self, ConfigError>
474 where
475 I: IntoIterator<Item = (K, V)>,
476 K: Into<String>,
477 V: Into<String>,
478 {
479 let pairs: Vec<(String, String)> = vars
484 .into_iter()
485 .map(|(k, v)| (k.into(), v.into()))
486 .collect();
487 let vars: HashMap<&str, &str> = pairs
488 .iter()
489 .map(|(k, v)| (k.as_str(), v.as_str()))
490 .collect();
491 let get = |key: &str| vars.get(key).map(|v| v.to_string()).ok_or(());
492 let mut config = Self::default();
493
494 if let Ok(uri) = get("PGRST_DB_URI") {
495 config.db_uri = uri;
496 }
497 if let Ok(uri) = get("DATABASE_URL") {
498 config.db_uri = uri;
499 }
500 if let Ok(schemas) = get("PGRST_DB_SCHEMAS") {
501 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
502 }
503 if let Ok(role) = get("PGRST_DB_ANON_ROLE") {
504 config.db_anon_role = Some(role);
505 }
506 for var in ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"] {
512 if let Ok(size) = get(var) {
513 match size.parse::<u32>() {
514 Ok(n) if n > 0 => config.db_pool_size = n,
515 _ => warn_ignored(var, &size, "a positive integer"),
516 }
517 }
518 }
519 if let Ok(secs) = get("PGRST_DB_POOL_TIMEOUT") {
520 match secs.parse::<u64>() {
521 Ok(n) if n > 0 => config.db_pool_timeout = n,
522 _ => warn_ignored("PGRST_DB_POOL_TIMEOUT", &secs, "a positive integer"),
523 }
524 }
525 if let Ok(level) = get("PGRST_DB_TX_ISOLATION") {
526 match IsolationLevel::parse_config(&level) {
527 Some(l) => config.db_tx_isolation = l,
528 None => warn_ignored(
529 "PGRST_DB_TX_ISOLATION",
530 &level,
531 "one of: read committed, repeatable read, serializable",
532 ),
533 }
534 }
535 if let Ok(v) = get("PGRST_DB_EXTRA_SEARCH_PATH") {
536 config.db_extra_search_path = v
537 .split(',')
538 .map(|s| s.trim().to_string())
539 .filter(|s| !s.is_empty())
540 .collect();
541 }
542
543 if let Ok(v) = get("PGRST_DB_AGGREGATES_ENABLED") {
544 config.db_aggregates_enabled = env_bool("PGRST_DB_AGGREGATES_ENABLED", &v)?;
545 }
546 if let Ok(v) = get("PGRST_DB_PREPARED_STATEMENTS") {
547 config.db_prepared_statements = env_bool("PGRST_DB_PREPARED_STATEMENTS", &v)?;
548 }
549 if let Ok(v) = get("PGRST_DB_PRE_REQUEST") {
550 if !v.trim().is_empty() {
551 config.db_pre_request = Some(v.trim().to_string());
552 }
553 }
554 if let Ok(v) = get("PGRST_DB_CHANNEL") {
555 if !v.trim().is_empty() {
556 config.db_channel = v.trim().to_string();
557 }
558 }
559 if let Ok(v) = get("PGRST_DB_CHANNEL_ENABLED") {
560 config.db_channel_enabled = env_bool("PGRST_DB_CHANNEL_ENABLED", &v)?;
561 }
562
563 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
565 if let Ok(max_rows) = get(var) {
566 match max_rows.parse::<i64>() {
567 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
568 _ => warn_ignored(var, &max_rows, "a non-negative integer"),
569 }
570 }
571 }
572 if let Ok(secret) = get("PGRST_JWT_SECRET") {
573 config.jwt_secret = Some(secret);
574 }
575 if let Ok(aud) = get("PGRST_JWT_AUD") {
576 config.jwt_aud = Some(aud);
577 }
578 if let Ok(iss) = get("PGRST_JWT_ISS") {
581 if !iss.trim().is_empty() {
582 config.jwt_iss = Some(iss);
583 }
584 }
585 if let Ok(v) = get("PGRST_JWT_SECRET_IS_BASE64") {
590 config.jwt_secret_is_base64 = env_bool("PGRST_JWT_SECRET_IS_BASE64", &v)?;
591 }
592 if let Ok(key) = get("PGRST_JWT_ROLE_CLAIM_KEY") {
593 if !key.trim().is_empty() {
594 config.jwt_role_claim_key = key;
595 }
596 }
597 if let Ok(v) = get("PGRST_JWT_CACHE_ENABLED") {
598 config.jwt_cache_enabled = env_bool("PGRST_JWT_CACHE_ENABLED", &v)?;
599 }
600 if let Ok(v) = get("PGRST_JWT_CACHE_MAX_LIFETIME") {
603 match v.parse::<u64>() {
604 Ok(n) => config.jwt_cache_max_lifetime = n,
605 Err(_) => warn_ignored(
606 "PGRST_JWT_CACHE_MAX_LIFETIME",
607 &v,
608 "a non-negative integer number of seconds",
609 ),
610 }
611 }
612 if let Ok(url) = get("PGRST_JWT_JWKS_URL") {
614 if !url.trim().is_empty() {
615 config.jwt_jwks_url = Some(url.trim().to_string());
616 }
617 }
618 if let Ok(secs) = get("PGRST_JWT_JWKS_REFRESH") {
619 match secs.parse::<u64>() {
620 Ok(n) if n > 0 => config.jwt_jwks_refresh = n,
621 _ => warn_ignored(
622 "PGRST_JWT_JWKS_REFRESH",
623 &secs,
624 "a positive integer number of seconds",
625 ),
626 }
627 }
628 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
634 if let Ok(secret) = get(var) {
635 if !secret.is_empty() {
636 config.hasura_admin_secret = Some(secret);
637 }
638 }
639 }
640 for var in [
641 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
642 "PGRST_HASURA_UNAUTHORIZED_ROLE",
643 ] {
644 if let Ok(role) = get(var) {
645 if !role.is_empty() {
646 config.hasura_unauthorized_role = Some(role);
647 }
648 }
649 }
650 if let Ok(host) = get("PGRST_SERVER_HOST") {
651 config.server_host = host;
652 }
653 if let Ok(port) = get("PGRST_SERVER_PORT") {
654 if let Ok(p) = port.parse() {
655 config.server_port = p;
656 }
657 }
658 if let Ok(port) = get("PORT") {
659 if let Ok(p) = port.parse() {
660 config.server_port = p;
661 }
662 }
663 if let Ok(origins) = get("PGRST_SERVER_CORS_ORIGINS") {
667 config.server_cors_origins = if origins.trim() == "*" {
668 Vec::new()
669 } else {
670 origins
671 .split(',')
672 .map(|s| s.trim().to_string())
673 .filter(|s| !s.is_empty())
674 .collect()
675 };
676 }
677 if let Ok(bytes) = get("PGRST_MAX_BODY_SIZE") {
678 match bytes.parse::<usize>() {
679 Ok(n) if n > 0 => config.max_body_size = n,
680 _ => warn_ignored("PGRST_MAX_BODY_SIZE", &bytes, "a positive integer"),
681 }
682 }
683 if let Ok(level) = get("PGRST_LOG_LEVEL") {
684 match LogLevel::parse_config(&level) {
685 Some(l) => config.log_level = l,
686 None => warn_ignored(
687 "PGRST_LOG_LEVEL",
688 &level,
689 "one of: crit, error, warn, info, debug",
690 ),
691 }
692 }
693 if let Ok(format) = get("PGRST_LOG_FORMAT") {
694 match LogFormat::parse_config(&format) {
695 Some(f) => config.log_format = f,
696 None => warn_ignored("PGRST_LOG_FORMAT", &format, "one of: text, json"),
697 }
698 }
699 if let Ok(v) = get("PGRST_OTEL_DB_STATEMENT") {
700 config.otel_db_statement = env_bool("PGRST_OTEL_DB_STATEMENT", &v)?;
701 }
702 if let Ok(path) = get("PGRST_SERVER_UNIX_SOCKET") {
703 if !path.trim().is_empty() {
704 config.server_unix_socket = Some(path.trim().to_string());
705 }
706 }
707 if let Ok(port) = get("PGRST_ADMIN_SERVER_PORT") {
708 match port.parse::<u16>() {
709 Ok(p) if p > 0 => config.admin_server_port = Some(p),
710 _ => warn_ignored("PGRST_ADMIN_SERVER_PORT", &port, "a port number"),
711 }
712 }
713 if let Ok(uri) = get("PGRST_OPENAPI_SERVER_PROXY_URI") {
714 if !uri.trim().is_empty() {
715 config.openapi_server_proxy_uri = Some(uri.trim().to_string());
716 }
717 }
718 if let Ok(mode) = get("PGRST_OPENAPI_MODE") {
719 match OpenApiMode::parse_config(&mode) {
720 Some(m) => config.openapi_mode = m,
721 None => warn_ignored(
722 "PGRST_OPENAPI_MODE",
723 &mode,
724 "one of: disabled, follow-privileges, ignore-privileges",
725 ),
726 }
727 }
728
729 if let Ok(raw) = get("PGRST_ROLE_SETTINGS") {
733 match serde_json::from_str::<HashMap<String, RoleSettings>>(&raw) {
734 Ok(settings) => config.role_settings = settings,
735 Err(e) => warn_ignored(
736 "PGRST_ROLE_SETTINGS",
737 &raw,
738 &format!("a JSON object of role to settings ({e})"),
739 ),
740 }
741 }
742
743 if let Ok(value) = get("PGRST_GRAPHQL_FEDERATION") {
744 config.graphql_federation = env_bool("PGRST_GRAPHQL_FEDERATION", &value)?;
745 }
746 if let Ok(value) = get("PGRST_GRAPHQL_TYPE_PREFIX") {
747 match parse_graphql_type_prefix(&value) {
748 Ok(prefix) => config.graphql_type_prefix = prefix,
749 Err(expected) => warn_ignored("PGRST_GRAPHQL_TYPE_PREFIX", &value, expected),
750 }
751 }
752
753 for (key, value) in &pairs {
759 if let Some(name) = key.strip_prefix("PGRST_APP_SETTINGS_") {
760 if !name.is_empty() {
761 config
762 .app_settings
763 .insert(name.to_ascii_lowercase(), value.clone());
764 }
765 }
766 }
767 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
770 if let Ok(v) = get(var) {
771 config.compat_mode = env_bool(var, &v)?;
772 }
773 }
774
775 if let Ok(v) = get("PGRST_DB_ERRORS_VERBATIM") {
776 config.db_errors_verbatim = parse_sqlstate_patterns(&v)
777 .ok_or_else(|| invalid("PGRST_DB_ERRORS_VERBATIM", &v, EXPECTED_SQLSTATES))?;
778 }
779
780 Ok(config)
781 }
782
783 pub fn verbatim_db_error(&self, error: &crate::Error) -> bool {
787 match error {
788 crate::Error::Database(db) => {
789 self.compat_mode
790 || self
791 .db_errors_verbatim
792 .iter()
793 .any(|pattern| sqlstate_matches(pattern, &db.code))
794 }
795 _ => false,
796 }
797 }
798
799 pub fn any_verbatim_db_errors(&self) -> bool {
802 self.compat_mode || !self.db_errors_verbatim.is_empty()
803 }
804
805 pub fn default_schema(&self) -> &str {
807 self.db_schemas
808 .first()
809 .map(|s| s.as_str())
810 .unwrap_or("public")
811 }
812}
813
814impl Serialize for IsolationLevel {
820 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
821 serializer.serialize_str(self.as_config_str())
822 }
823}
824
825impl<'de> Deserialize<'de> for IsolationLevel {
826 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
827 let raw = String::deserialize(deserializer)?;
828 Self::parse_config(&raw).ok_or_else(|| {
829 serde::de::Error::custom(format!(
830 "unknown isolation level {raw:?}: expected one of \
831 read committed, repeatable read, serializable"
832 ))
833 })
834 }
835}
836
837impl Serialize for OpenApiMode {
838 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
839 serializer.serialize_str(self.as_config_str())
840 }
841}
842
843impl<'de> Deserialize<'de> for OpenApiMode {
844 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
845 let raw = String::deserialize(deserializer)?;
846 Self::parse_config(&raw).ok_or_else(|| {
847 serde::de::Error::custom(format!(
848 "unknown OpenAPI mode {raw:?}: expected one of \
849 disabled, follow-privileges, ignore-privileges"
850 ))
851 })
852 }
853}
854
855impl Serialize for LogLevel {
856 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
857 serializer.serialize_str(self.as_config_str())
858 }
859}
860
861impl<'de> Deserialize<'de> for LogLevel {
862 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
863 let raw = String::deserialize(deserializer)?;
864 Self::parse_config(&raw).ok_or_else(|| {
865 serde::de::Error::custom(format!(
866 "unknown log level {raw:?}: expected one of \
867 crit, error, warn, info, debug"
868 ))
869 })
870 }
871}
872
873impl Serialize for LogFormat {
874 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
875 serializer.serialize_str(self.as_config_str())
876 }
877}
878
879impl<'de> Deserialize<'de> for LogFormat {
880 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
881 let raw = String::deserialize(deserializer)?;
882 Self::parse_config(&raw).ok_or_else(|| {
883 serde::de::Error::custom(format!(
884 "unknown log format {raw:?}: expected one of text, json"
885 ))
886 })
887 }
888}
889
890#[derive(Clone, Debug, Default, Serialize, Deserialize)]
892#[non_exhaustive]
893pub struct RoleSettings {
894 pub isolation_level: Option<IsolationLevel>,
896 pub statement_timeout: Option<u64>,
898}
899
900#[derive(Clone, Debug, PartialEq, Eq)]
906pub enum IsolationLevel {
907 ReadCommitted,
909 RepeatableRead,
911 Serializable,
915}
916
917impl IsolationLevel {
918 pub fn to_sql(&self) -> &'static str {
920 match self {
921 Self::ReadCommitted => "READ COMMITTED",
922 Self::RepeatableRead => "REPEATABLE READ",
923 Self::Serializable => "SERIALIZABLE",
924 }
925 }
926
927 pub fn parse_config(s: &str) -> Option<Self> {
933 match s
934 .trim()
935 .to_ascii_lowercase()
936 .replace(['-', '_'], " ")
937 .as_str()
938 {
939 "read committed" => Some(Self::ReadCommitted),
940 "repeatable read" => Some(Self::RepeatableRead),
941 "serializable" => Some(Self::Serializable),
942 _ => None,
943 }
944 }
945
946 pub fn as_config_str(&self) -> &'static str {
948 match self {
949 Self::ReadCommitted => "read committed",
950 Self::RepeatableRead => "repeatable read",
951 Self::Serializable => "serializable",
952 }
953 }
954}
955
956#[derive(Clone, Debug, PartialEq, Eq)]
963pub enum OpenApiMode {
964 Disabled,
966 FollowPrivileges,
969 IgnorePrivileges,
971}
972
973impl OpenApiMode {
974 pub fn parse_config(s: &str) -> Option<Self> {
977 match s
978 .trim()
979 .to_ascii_lowercase()
980 .replace(['-', '_'], " ")
981 .as_str()
982 {
983 "disabled" => Some(Self::Disabled),
984 "follow privileges" => Some(Self::FollowPrivileges),
985 "ignore privileges" => Some(Self::IgnorePrivileges),
986 _ => None,
987 }
988 }
989
990 pub fn as_config_str(&self) -> &'static str {
992 match self {
993 Self::Disabled => "disabled",
994 Self::FollowPrivileges => "follow-privileges",
995 Self::IgnorePrivileges => "ignore-privileges",
996 }
997 }
998}
999
1000#[derive(Clone, Debug, PartialEq, Eq)]
1002pub enum LogLevel {
1003 Crit,
1006 Error,
1008 Warn,
1010 Info,
1013 Debug,
1016}
1017
1018impl LogLevel {
1019 pub fn to_tracing(&self) -> tracing::Level {
1024 match self {
1025 Self::Crit | Self::Error => tracing::Level::ERROR,
1026 Self::Warn => tracing::Level::WARN,
1027 Self::Info => tracing::Level::INFO,
1028 Self::Debug => tracing::Level::DEBUG,
1029 }
1030 }
1031
1032 pub fn parse_config(s: &str) -> Option<Self> {
1034 match s.trim().to_ascii_lowercase().as_str() {
1035 "crit" | "critical" => Some(Self::Crit),
1036 "error" => Some(Self::Error),
1037 "warn" | "warning" => Some(Self::Warn),
1038 "info" => Some(Self::Info),
1039 "debug" => Some(Self::Debug),
1040 _ => None,
1041 }
1042 }
1043
1044 pub fn as_config_str(&self) -> &'static str {
1046 match self {
1047 Self::Crit => "crit",
1048 Self::Error => "error",
1049 Self::Warn => "warn",
1050 Self::Info => "info",
1051 Self::Debug => "debug",
1052 }
1053 }
1054}
1055
1056#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1058pub enum LogFormat {
1059 #[default]
1062 Text,
1063 Json,
1066}
1067
1068impl LogFormat {
1069 pub fn parse_config(s: &str) -> Option<Self> {
1071 match s.trim().to_ascii_lowercase().as_str() {
1072 "text" => Some(Self::Text),
1073 "json" => Some(Self::Json),
1074 _ => None,
1075 }
1076 }
1077
1078 pub fn as_config_str(&self) -> &'static str {
1080 match self {
1081 Self::Text => "text",
1082 Self::Json => "json",
1083 }
1084 }
1085}
1086
1087fn default_db_uri() -> String {
1089 "postgresql://localhost/postgres".to_string()
1090}
1091
1092fn default_db_schemas() -> Vec<String> {
1093 vec!["public".to_string()]
1094}
1095
1096fn default_pool_size() -> u32 {
1097 10
1098}
1099
1100fn default_pool_timeout() -> u64 {
1101 10
1102}
1103
1104fn default_tx_isolation() -> IsolationLevel {
1105 IsolationLevel::ReadCommitted
1106}
1107
1108fn default_max_body_size() -> usize {
1109 10 * 1024 * 1024
1110}
1111
1112fn default_extra_search_path() -> Vec<String> {
1113 vec!["public".to_string()]
1114}
1115
1116fn default_db_channel() -> String {
1117 "pgrst".to_string()
1118}
1119
1120fn default_host() -> String {
1121 "127.0.0.1".to_string()
1122}
1123
1124fn default_port() -> u16 {
1125 3000
1126}
1127
1128fn default_jwt_role_claim() -> String {
1129 "role".to_string()
1130}
1131
1132fn default_jwt_cache_max() -> u64 {
1133 3600
1134}
1135
1136fn default_jwt_jwks_refresh() -> u64 {
1137 300
1138}
1139
1140fn default_openapi_mode() -> OpenApiMode {
1141 OpenApiMode::FollowPrivileges
1142}
1143
1144fn default_log_level() -> LogLevel {
1149 LogLevel::Info
1150}
1151
1152fn default_true() -> bool {
1153 true
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159
1160 #[test]
1161 fn test_default_config() {
1162 let config = AppConfig::default();
1163 assert_eq!(config.server_port, 3000);
1164 assert_eq!(config.db_pool_size, 10);
1165 assert!(config.db_prepared_statements);
1166 assert!(!config.graphql_federation);
1167 assert_eq!(config.graphql_type_prefix, None);
1168 }
1169
1170 #[test]
1171 fn db_errors_verbatim_takes_sqlstates_prefixes_and_booleans() {
1172 let parse = |v: &str| {
1173 AppConfig::try_from_vars([("PGRST_DB_ERRORS_VERBATIM", v)])
1174 .map(|c| c.db_errors_verbatim)
1175 };
1176 assert_eq!(parse("P0001, pt*").unwrap(), ["P0001", "PT*"]);
1177 assert_eq!(parse("true").unwrap(), ["*"]);
1178 assert_eq!(parse("off").unwrap(), Vec::<String>::new());
1179 assert_eq!(parse("").unwrap(), Vec::<String>::new());
1180 for bad in ["P001", "P00011", "P0*01", "P0001*", "4250!"] {
1181 assert!(parse(bad).is_err(), "{bad:?}");
1182 }
1183
1184 let from_json = |v: &str| {
1186 serde_json::from_str::<AppConfig>(&format!(r#"{{"db_errors_verbatim": {v}}}"#))
1187 .map(|c| c.db_errors_verbatim)
1188 };
1189 assert_eq!(from_json(r#"["p0001", "PT*"]"#).unwrap(), ["P0001", "PT*"]);
1190 assert_eq!(from_json(r#"["true"]"#).unwrap(), ["*"]);
1191 assert!(from_json(r#"["P001"]"#).is_err());
1192 }
1193
1194 #[test]
1195 fn verbatim_db_error_matches_only_listed_database_errors() {
1196 let db = |code: &str| {
1197 crate::Error::Database(crate::error::DatabaseError {
1198 code: code.to_string(),
1199 message: String::new(),
1200 details: None,
1201 hint: None,
1202 constraint: None,
1203 table: None,
1204 column: None,
1205 })
1206 };
1207 let mut config = AppConfig::default();
1208 assert!(!config.verbatim_db_error(&db("P0001")));
1209 assert!(!config.any_verbatim_db_errors());
1210
1211 config.db_errors_verbatim = vec!["P0001".to_string(), "PT*".to_string()];
1212 assert!(config.any_verbatim_db_errors());
1213 assert!(config.verbatim_db_error(&db("P0001")));
1214 assert!(config.verbatim_db_error(&db("PT402")));
1215 assert!(!config.verbatim_db_error(&db("23505")));
1216 assert!(!config.verbatim_db_error(&crate::Error::NotFound("t".to_string())));
1217
1218 config.db_errors_verbatim = vec!["*".to_string()];
1219 assert!(config.verbatim_db_error(&db("42P01")));
1220
1221 config.db_errors_verbatim.clear();
1222 config.compat_mode = true;
1223 assert!(config.verbatim_db_error(&db("23505")));
1224 }
1225
1226 #[test]
1227 fn test_default_schema() {
1228 let mut config = AppConfig::default();
1229 assert_eq!(config.default_schema(), "public");
1230
1231 config.db_schemas = vec!["api".to_string(), "public".to_string()];
1232 assert_eq!(config.default_schema(), "api");
1233 }
1234
1235 #[test]
1236 fn test_isolation_level_sql() {
1237 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
1238 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
1239 }
1240
1241 #[test]
1242 fn isolation_level_accepts_the_documented_spellings() {
1243 for s in [
1246 "read committed",
1247 "READ COMMITTED",
1248 "read-committed",
1249 " Read_Committed ",
1250 ] {
1251 assert_eq!(
1252 IsolationLevel::parse_config(s),
1253 Some(IsolationLevel::ReadCommitted),
1254 "{s:?}"
1255 );
1256 }
1257 assert_eq!(
1258 IsolationLevel::parse_config("serializable"),
1259 Some(IsolationLevel::Serializable)
1260 );
1261 assert_eq!(
1262 IsolationLevel::parse_config("repeatable read"),
1263 Some(IsolationLevel::RepeatableRead)
1264 );
1265 assert_eq!(IsolationLevel::parse_config("dirty read"), None);
1268 assert_eq!(IsolationLevel::parse_config(""), None);
1269 }
1270
1271 #[test]
1272 fn log_level_accepts_the_documented_spellings() {
1273 assert_eq!(LogLevel::parse_config("INFO"), Some(LogLevel::Info));
1274 assert_eq!(LogLevel::parse_config("warning"), Some(LogLevel::Warn));
1275 assert_eq!(LogLevel::parse_config("crit"), Some(LogLevel::Crit));
1276 assert_eq!(LogLevel::parse_config("trace"), None);
1279 }
1280
1281 #[test]
1282 fn default_log_level_matches_the_documented_one() {
1283 assert_eq!(AppConfig::default().log_level, LogLevel::Info);
1286 }
1287
1288 #[test]
1289 fn parse_bool_accepts_only_the_supported_spellings() {
1290 for s in ["true", "TRUE", "1", "yes", "on", " On "] {
1291 assert_eq!(parse_bool(s), Some(true), "{s:?}");
1292 }
1293 for s in ["false", "FALSE", "0", "no", "off", " Off "] {
1294 assert_eq!(parse_bool(s), Some(false), "{s:?}");
1295 }
1296 for s in ["", "maybe", "enabled", "2"] {
1297 assert_eq!(parse_bool(s), None, "{s:?}");
1298 }
1299 }
1300
1301 #[test]
1302 fn graphql_federation_configuration_is_read() {
1303 let config = with_env(&[
1304 ("PGRST_GRAPHQL_FEDERATION", "true"),
1305 ("PGRST_GRAPHQL_TYPE_PREFIX", " test "),
1306 ]);
1307
1308 assert!(config.graphql_federation);
1309 assert_eq!(config.graphql_type_prefix.as_deref(), Some("test"));
1310 }
1311
1312 #[test]
1313 fn graphql_federation_accepts_an_explicit_false() {
1314 assert!(!with_env(&[("PGRST_GRAPHQL_FEDERATION", "false")]).graphql_federation);
1315 }
1316
1317 #[test]
1318 fn malformed_graphql_federation_configuration_is_rejected() {
1319 let error = with_env_result(&[("PGRST_GRAPHQL_FEDERATION", "sometimes")])
1320 .expect_err("an invalid boolean must reject the configuration");
1321
1322 assert_eq!(
1323 error.to_string(),
1324 "invalid value for PGRST_GRAPHQL_FEDERATION: \"sometimes\"; expected one of: true, false, 1, 0, yes, no, on, off"
1325 );
1326 }
1327
1328 #[test]
1329 fn an_empty_graphql_prefix_means_unset() {
1330 let config = with_env(&[("PGRST_GRAPHQL_TYPE_PREFIX", " ")]);
1331
1332 assert_eq!(config.graphql_type_prefix, None);
1333 }
1334
1335 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1337
1338 fn with_env(vars: &[(&str, &str)]) -> AppConfig {
1340 with_env_result(vars).expect("test configuration should be valid")
1341 }
1342
1343 fn with_env_result(vars: &[(&str, &str)]) -> Result<AppConfig, ConfigError> {
1344 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1345 for (k, v) in vars {
1346 std::env::set_var(k, v);
1347 }
1348 let config = AppConfig::try_from_env();
1349 for (k, _) in vars {
1350 std::env::remove_var(k);
1351 }
1352 config
1353 }
1354
1355 #[test]
1356 fn documented_variables_are_read() {
1357 let config = with_env(&[
1360 ("PGRST_DB_POOL_SIZE", "42"),
1361 ("PGRST_DB_POOL_TIMEOUT", "7"),
1362 ("PGRST_DB_TX_ISOLATION", "serializable"),
1363 ("PGRST_JWT_SECRET_IS_BASE64", "true"),
1364 ("PGRST_JWT_ROLE_CLAIM_KEY", "user.role"),
1365 ("PGRST_LOG_LEVEL", "warn"),
1366 ("PGRST_MAX_BODY_SIZE", "1024"),
1367 (
1368 "PGRST_SERVER_CORS_ORIGINS",
1369 "https://a.example, https://b.example",
1370 ),
1371 ]);
1372
1373 assert_eq!(config.db_pool_size, 42);
1374 assert_eq!(config.db_pool_timeout, 7);
1375 assert_eq!(config.db_tx_isolation, IsolationLevel::Serializable);
1376 assert!(config.jwt_secret_is_base64);
1377 assert_eq!(config.jwt_role_claim_key, "user.role");
1378 assert_eq!(config.log_level, LogLevel::Warn);
1379 assert_eq!(config.max_body_size, 1024);
1380 assert_eq!(
1381 config.server_cors_origins,
1382 vec![
1383 "https://a.example".to_string(),
1384 "https://b.example".to_string()
1385 ]
1386 );
1387 }
1388
1389 #[test]
1390 fn the_jwt_issuer_is_read_and_an_empty_one_is_unset() {
1391 let read = |value: &str| {
1392 AppConfig::try_from_vars([("PGRST_JWT_ISS", value)])
1393 .unwrap()
1394 .jwt_iss
1395 };
1396 assert_eq!(
1397 read("https://issuer.example").as_deref(),
1398 Some("https://issuer.example")
1399 );
1400 assert_eq!(read(" "), None);
1401 assert_eq!(
1402 AppConfig::try_from_vars(Vec::<(String, String)>::new())
1403 .unwrap()
1404 .jwt_iss,
1405 None
1406 );
1407 }
1408
1409 #[test]
1410 fn the_jwks_url_and_its_refresh_interval_are_read() {
1411 let read = |vars: &[(&str, &str)]| AppConfig::try_from_vars(vars.to_vec()).unwrap();
1412 let config = read(&[
1413 ("PGRST_JWT_JWKS_URL", " https://idp.example/jwks "),
1414 ("PGRST_JWT_JWKS_REFRESH", "60"),
1415 ]);
1416 assert_eq!(
1417 config.jwt_jwks_url.as_deref(),
1418 Some("https://idp.example/jwks")
1419 );
1420 assert_eq!(config.jwt_jwks_refresh, 60);
1421
1422 let defaults = read(&[("PGRST_JWT_JWKS_URL", ""), ("PGRST_JWT_JWKS_REFRESH", "0")]);
1423 assert_eq!(defaults.jwt_jwks_url, None);
1424 assert_eq!(defaults.jwt_jwks_refresh, 300);
1425 assert_eq!(
1426 read(&[("PGRST_JWT_JWKS_REFRESH", "soon")]).jwt_jwks_refresh,
1427 300
1428 );
1429 }
1430
1431 #[test]
1432 fn variables_can_come_from_somewhere_other_than_the_environment() {
1433 let config = AppConfig::try_from_vars([
1437 ("PGRST_DB_SCHEMAS", "api, extra"),
1438 ("PGRST_DB_ANON_ROLE", "web_anon"),
1439 ("PGRST_JWT_SECRET", "a-secret"),
1440 ("PGRST_DB_MAX_ROWS", "100"),
1441 ("PGRST_APP_SETTINGS_Tenant", "acme"),
1442 ])
1443 .unwrap();
1444
1445 assert_eq!(config.db_schemas, ["api", "extra"]);
1446 assert_eq!(config.db_anon_role.as_deref(), Some("web_anon"));
1447 assert_eq!(config.jwt_secret.as_deref(), Some("a-secret"));
1448 assert_eq!(config.db_max_rows, Some(100));
1449 assert_eq!(
1450 config.app_settings.get("tenant").map(String::as_str),
1451 Some("acme")
1452 );
1453
1454 let refused = AppConfig::try_from_vars([("PGRST_DB_AGGREGATES_ENABLED", "maybe")]);
1455 assert!(refused.is_err());
1456 }
1457
1458 #[test]
1459 fn app_settings_differing_only_in_case_take_the_later_value() {
1460 for _ in 0..16 {
1463 let config = AppConfig::try_from_vars([
1464 ("PGRST_APP_SETTINGS_Tenant", "first"),
1465 ("PGRST_APP_SETTINGS_TENANT", "second"),
1466 ("PGRST_APP_SETTINGS_tenant", "third"),
1467 ])
1468 .unwrap();
1469 assert_eq!(
1470 config.app_settings.get("tenant").map(String::as_str),
1471 Some("third")
1472 );
1473
1474 let config = AppConfig::try_from_vars([
1475 ("PGRST_APP_SETTINGS_tenant", "third"),
1476 ("PGRST_APP_SETTINGS_TENANT", "second"),
1477 ])
1478 .unwrap();
1479 assert_eq!(
1480 config.app_settings.get("tenant").map(String::as_str),
1481 Some("second")
1482 );
1483 }
1484 }
1485
1486 #[test]
1487 fn windows_environment_names_are_matched_whatever_their_case() {
1488 let config = AppConfig::try_from_vars([
1490 (upper_env_name("pgrst_db_schemas".into()), "api"),
1491 (upper_env_name("Pgrst_App_Settings_Tenant".into()), "acme"),
1492 ])
1493 .unwrap();
1494 assert_eq!(config.db_schemas, ["api"]);
1495 assert_eq!(
1496 config.app_settings.get("tenant").map(String::as_str),
1497 Some("acme")
1498 );
1499 assert_eq!(env_name("PGRST_DB_URI".into()), "PGRST_DB_URI");
1500 assert_eq!(
1501 env_name("pgrst_db_uri".into()) == "PGRST_DB_URI",
1502 cfg!(windows)
1503 );
1504 }
1505
1506 #[test]
1507 fn pool_size_accepts_postgrests_own_name() {
1508 assert_eq!(with_env(&[("PGRST_DB_POOL", "5")]).db_pool_size, 5);
1509 }
1510
1511 #[test]
1512 fn a_star_origin_means_any_origin() {
1513 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", "*")])
1516 .server_cors_origins
1517 .is_empty());
1518 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", " , ")])
1519 .server_cors_origins
1520 .is_empty());
1521 }
1522
1523 #[test]
1524 fn an_unusable_value_leaves_the_default() {
1525 let config = with_env(&[
1526 ("PGRST_DB_POOL_SIZE", "lots"),
1527 ("PGRST_DB_POOL_TIMEOUT", "0"),
1528 ("PGRST_MAX_BODY_SIZE", "-1"),
1529 ("PGRST_DB_TX_ISOLATION", "dirty read"),
1530 ("PGRST_LOG_LEVEL", "shout"),
1531 ]);
1532
1533 let d = AppConfig::default();
1534 assert_eq!(config.db_pool_size, d.db_pool_size);
1535 assert_eq!(config.db_pool_timeout, d.db_pool_timeout);
1536 assert_eq!(config.max_body_size, d.max_body_size);
1537 assert_eq!(config.db_tx_isolation, d.db_tx_isolation);
1538 assert_eq!(config.log_level, d.log_level);
1539 }
1540
1541 #[test]
1542 fn the_formerly_dead_fields_are_read() {
1543 let config = with_env(&[
1546 ("PGRST_DB_PREPARED_STATEMENTS", "false"),
1547 ("PGRST_DB_PRE_REQUEST", "auth.check"),
1548 ("PGRST_DB_CHANNEL", "my_channel"),
1549 ("PGRST_DB_CHANNEL_ENABLED", "yes"),
1550 ("PGRST_JWT_CACHE_ENABLED", "false"),
1551 ("PGRST_JWT_CACHE_MAX_LIFETIME", "60"),
1552 ("PGRST_SERVER_UNIX_SOCKET", "/tmp/postrust.sock"),
1553 ("PGRST_ADMIN_SERVER_PORT", "3001"),
1554 ("PGRST_OPENAPI_SERVER_PROXY_URI", "https://api.example.com"),
1555 ("PGRST_OPENAPI_MODE", "ignore-privileges"),
1556 ]);
1557
1558 assert!(!config.db_prepared_statements);
1559 assert_eq!(config.db_pre_request.as_deref(), Some("auth.check"));
1560 assert_eq!(config.db_channel, "my_channel");
1561 assert!(config.db_channel_enabled);
1562 assert!(!config.jwt_cache_enabled);
1563 assert_eq!(config.jwt_cache_max_lifetime, 60);
1564 assert_eq!(
1565 config.server_unix_socket.as_deref(),
1566 Some("/tmp/postrust.sock")
1567 );
1568 assert_eq!(config.admin_server_port, Some(3001));
1569 assert_eq!(
1570 config.openapi_server_proxy_uri.as_deref(),
1571 Some("https://api.example.com")
1572 );
1573 assert_eq!(config.openapi_mode, OpenApiMode::IgnorePrivileges);
1574 }
1575
1576 #[test]
1577 fn tracing_settings_are_read() {
1578 let d = AppConfig::default();
1579 assert_eq!(d.log_format, LogFormat::Text);
1580 assert!(!d.otel_db_statement);
1581
1582 let config = with_env(&[
1583 ("PGRST_LOG_FORMAT", "JSON"),
1584 ("PGRST_OTEL_DB_STATEMENT", "on"),
1585 ]);
1586 assert_eq!(config.log_format, LogFormat::Json);
1587 assert!(config.otel_db_statement);
1588
1589 assert_eq!(
1592 with_env(&[("PGRST_LOG_FORMAT", "yaml")]).log_format,
1593 LogFormat::Text
1594 );
1595 assert!(with_env_result(&[("PGRST_OTEL_DB_STATEMENT", "maybe")]).is_err());
1596 }
1597
1598 #[test]
1599 fn a_zero_jwt_cache_lifetime_is_kept_because_it_turns_the_cache_off() {
1600 assert_eq!(
1603 with_env(&[("PGRST_JWT_CACHE_MAX_LIFETIME", "0")]).jwt_cache_max_lifetime,
1604 0
1605 );
1606 }
1607
1608 #[test]
1609 fn app_settings_come_from_a_prefix() {
1610 let config = with_env(&[
1611 ("PGRST_APP_SETTINGS_JWT_LIFETIME", "3600"),
1612 ("PGRST_APP_SETTINGS_TenantName", "acme"),
1613 ]);
1614 assert_eq!(
1617 config.app_settings.get("jwt_lifetime").map(String::as_str),
1618 Some("3600")
1619 );
1620 assert_eq!(
1621 config.app_settings.get("tenantname").map(String::as_str),
1622 Some("acme")
1623 );
1624 }
1625
1626 #[test]
1627 fn role_settings_parse_from_json_using_the_documented_spellings() {
1628 let config = with_env(&[(
1629 "PGRST_ROLE_SETTINGS",
1630 r#"{"web_user":{"isolation_level":"serializable","statement_timeout":5000}}"#,
1631 )]);
1632 let web_user = config.role_settings.get("web_user").expect("role present");
1633 assert_eq!(web_user.isolation_level, Some(IsolationLevel::Serializable));
1634 assert_eq!(web_user.statement_timeout, Some(5000));
1635 }
1636
1637 #[test]
1638 fn unparseable_role_settings_leave_the_map_empty() {
1639 assert!(with_env(&[("PGRST_ROLE_SETTINGS", "{not json")])
1640 .role_settings
1641 .is_empty());
1642 assert!(with_env(&[(
1645 "PGRST_ROLE_SETTINGS",
1646 r#"{"r":{"isolation_level":"dirty"}}"#
1647 )])
1648 .role_settings
1649 .is_empty());
1650 }
1651
1652 #[test]
1653 fn openapi_mode_accepts_the_documented_spellings() {
1654 assert_eq!(
1655 OpenApiMode::parse_config("follow-privileges"),
1656 Some(OpenApiMode::FollowPrivileges)
1657 );
1658 assert_eq!(
1659 OpenApiMode::parse_config("IGNORE_PRIVILEGES"),
1660 Some(OpenApiMode::IgnorePrivileges)
1661 );
1662 assert_eq!(
1663 OpenApiMode::parse_config("disabled"),
1664 Some(OpenApiMode::Disabled)
1665 );
1666 assert_eq!(OpenApiMode::parse_config("security-definer"), None);
1669 }
1670
1671 #[test]
1672 fn the_config_enums_round_trip_through_json() {
1673 for level in [
1679 IsolationLevel::ReadCommitted,
1680 IsolationLevel::RepeatableRead,
1681 IsolationLevel::Serializable,
1682 ] {
1683 let json = serde_json::to_string(&level).unwrap();
1684 let back: IsolationLevel = serde_json::from_str(&json).unwrap();
1685 assert_eq!(back, level, "{json}");
1686 }
1687
1688 for mode in [
1689 OpenApiMode::Disabled,
1690 OpenApiMode::FollowPrivileges,
1691 OpenApiMode::IgnorePrivileges,
1692 ] {
1693 let json = serde_json::to_string(&mode).unwrap();
1694 assert_eq!(serde_json::from_str::<OpenApiMode>(&json).unwrap(), mode);
1695 }
1696
1697 for level in [
1698 LogLevel::Crit,
1699 LogLevel::Error,
1700 LogLevel::Warn,
1701 LogLevel::Info,
1702 LogLevel::Debug,
1703 ] {
1704 let json = serde_json::to_string(&level).unwrap();
1705 assert_eq!(serde_json::from_str::<LogLevel>(&json).unwrap(), level);
1706 }
1707 }
1708
1709 #[test]
1710 fn a_whole_config_round_trips_through_json() {
1711 let mut config = AppConfig::default();
1713 config.role_settings.insert(
1714 "web_user".to_string(),
1715 RoleSettings {
1716 isolation_level: Some(IsolationLevel::RepeatableRead),
1717 statement_timeout: Some(5000),
1718 },
1719 );
1720
1721 let json = serde_json::to_string(&config).unwrap();
1722 let back: AppConfig = serde_json::from_str(&json).expect("config should round trip");
1723
1724 assert_eq!(back.db_tx_isolation, config.db_tx_isolation);
1725 assert_eq!(back.log_level, config.log_level);
1726 assert_eq!(back.openapi_mode, config.openapi_mode);
1727 assert_eq!(
1728 back.role_settings["web_user"].isolation_level,
1729 Some(IsolationLevel::RepeatableRead)
1730 );
1731 }
1732
1733 #[test]
1734 fn the_serialized_spelling_is_the_configuration_spelling() {
1735 assert_eq!(
1738 serde_json::to_string(&IsolationLevel::ReadCommitted).unwrap(),
1739 "\"read committed\""
1740 );
1741 assert_eq!(
1742 serde_json::to_string(&OpenApiMode::FollowPrivileges).unwrap(),
1743 "\"follow-privileges\""
1744 );
1745 assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1746 }
1747
1748 #[test]
1749 fn an_empty_role_claim_key_is_not_accepted() {
1750 assert_eq!(
1753 with_env(&[("PGRST_JWT_ROLE_CLAIM_KEY", " ")]).jwt_role_claim_key,
1754 AppConfig::default().jwt_role_claim_key
1755 );
1756 }
1757}