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(
86 mut self,
87 reason: impl Into<String>,
88 ) -> gatekeep::GatekeepResult<Self> {
89 self.hidden_reason = Some(ReasonCode::new(reason)?);
90 Ok(self)
91 }
92
93 pub(crate) fn denied<C: ReasonCatalog>(
94 &self,
95 shape: DenyShape,
96 reason: Option<&DenialReason>,
97 locale: &Locale,
98 catalog: &C,
99 ) -> DenialResponse {
100 match shape {
101 DenyShape::Forbidden => self.forbidden(reason, locale, catalog),
102 DenyShape::Hidden => self.hidden(locale, catalog),
103 }
104 }
105
106 fn forbidden<C: ReasonCatalog>(
107 &self,
108 reason: Option<&DenialReason>,
109 locale: &Locale,
110 catalog: &C,
111 ) -> DenialResponse {
112 let message = reason.map_or_else(
113 || self.forbidden_fallback.clone(),
114 |reason| catalog.render(reason, locale),
115 );
116 DenialResponse {
117 status: StatusCode::FORBIDDEN,
118 body: DenialBody {
119 error: DenialError::Forbidden,
120 message,
121 reason: reason.map(|reason| reason.code.as_str().to_owned()),
122 },
123 }
124 }
125
126 fn hidden<C: ReasonCatalog>(&self, locale: &Locale, catalog: &C) -> DenialResponse {
127 let message = self.hidden_reason.as_ref().map_or_else(
128 || self.hidden_fallback.clone(),
129 |code| {
130 let reason = DenialReason {
131 code: code.clone(),
132 params: BTreeMap::new(),
133 shape: DenyShape::Forbidden,
134 };
135 catalog.render(&reason, locale)
136 },
137 );
138 DenialResponse {
139 status: StatusCode::NOT_FOUND,
140 body: DenialBody {
141 error: DenialError::NotFound,
142 message,
143 reason: None,
144 },
145 }
146 }
147}
148
149impl Default for DenialResponseConfig {
150 fn default() -> Self {
151 Self {
152 forbidden_fallback: "forbidden".to_owned(),
153 hidden_fallback: "not found".to_owned(),
154 hidden_reason: None,
155 }
156 }
157}