gatekeep_axum/
response.rs1use std::collections::BTreeMap;
2
3use axum::{
4 Json,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use gatekeep::{DenialReason, DenyShape, Locale, ReasonCatalog, ReasonCode};
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum DenialError {
15 Forbidden,
17 NotFound,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct DenialBody {
24 pub error: DenialError,
26 pub message: String,
28 pub reason: Option<String>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct DenialResponse {
35 pub status: StatusCode,
37 pub body: DenialBody,
39}
40
41impl IntoResponse for DenialResponse {
42 fn into_response(self) -> Response {
43 (self.status, Json(self.body)).into_response()
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct DenialResponseConfig {
50 forbidden_fallback: String,
51 hidden_fallback: String,
52 hidden_reason: Option<ReasonCode>,
53}
54
55impl DenialResponseConfig {
56 #[must_use]
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 #[must_use]
64 pub fn with_forbidden_fallback(mut self, message: impl Into<String>) -> Self {
65 self.forbidden_fallback = message.into();
66 self
67 }
68
69 #[must_use]
71 pub fn with_hidden_fallback(mut self, message: impl Into<String>) -> Self {
72 self.hidden_fallback = message.into();
73 self
74 }
75
76 pub fn try_with_hidden_reason(
81 mut self,
82 reason: impl Into<String>,
83 ) -> gatekeep::GatekeepResult<Self> {
84 self.hidden_reason = Some(ReasonCode::new(reason)?);
85 Ok(self)
86 }
87
88 pub(crate) fn denied<C: ReasonCatalog>(
89 &self,
90 shape: DenyShape,
91 reason: Option<&DenialReason>,
92 locale: &Locale,
93 catalog: &C,
94 ) -> DenialResponse {
95 match shape {
96 DenyShape::Forbidden => self.forbidden(reason, locale, catalog),
97 DenyShape::Hidden => self.hidden(locale, catalog),
98 }
99 }
100
101 fn forbidden<C: ReasonCatalog>(
102 &self,
103 reason: Option<&DenialReason>,
104 locale: &Locale,
105 catalog: &C,
106 ) -> DenialResponse {
107 let message = reason.map_or_else(
108 || self.forbidden_fallback.clone(),
109 |reason| catalog.render(reason, locale),
110 );
111 DenialResponse {
112 status: StatusCode::FORBIDDEN,
113 body: DenialBody {
114 error: DenialError::Forbidden,
115 message,
116 reason: reason.map(|reason| reason.code.as_str().to_owned()),
117 },
118 }
119 }
120
121 fn hidden<C: ReasonCatalog>(&self, locale: &Locale, catalog: &C) -> DenialResponse {
122 let message = self.hidden_reason.as_ref().map_or_else(
123 || self.hidden_fallback.clone(),
124 |code| {
125 let reason = DenialReason {
126 code: code.clone(),
127 params: BTreeMap::new(),
128 shape: DenyShape::Forbidden,
129 };
130 catalog.render(&reason, locale)
131 },
132 );
133 DenialResponse {
134 status: StatusCode::NOT_FOUND,
135 body: DenialBody {
136 error: DenialError::NotFound,
137 message,
138 reason: None,
139 },
140 }
141 }
142}
143
144impl Default for DenialResponseConfig {
145 fn default() -> Self {
146 Self {
147 forbidden_fallback: "forbidden".to_owned(),
148 hidden_fallback: "not found".to_owned(),
149 hidden_reason: None,
150 }
151 }
152}