mas_handlers/admin/v1/policy_data/
set.rs1use std::sync::Arc;
7
8use aide::{NoApi, OperationIo, transform::TransformOperation};
9use axum::{Json, extract::State, response::IntoResponse};
10use hyper::StatusCode;
11use mas_axum_utils::record_error;
12use mas_policy::PolicyFactory;
13use mas_storage::BoxRng;
14use schemars::JsonSchema;
15use serde::Deserialize;
16
17use crate::{
18 admin::{
19 call_context::CallContext,
20 model::PolicyData,
21 response::{ErrorResponse, SingleResponse},
22 },
23 impl_from_error_for_route,
24};
25
26#[derive(Debug, thiserror::Error, OperationIo)]
27#[aide(output_with = "Json<ErrorResponse>")]
28pub enum RouteError {
29 #[error("Failed to instanciate policy with the provided data")]
30 InvalidPolicyData(#[from] mas_policy::LoadError),
31
32 #[error(transparent)]
33 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
34}
35
36impl_from_error_for_route!(mas_storage::RepositoryError);
37
38impl IntoResponse for RouteError {
39 fn into_response(self) -> axum::response::Response {
40 let error = ErrorResponse::from_error(&self);
41 let sentry_event_id = record_error!(self, Self::Internal(_));
42 let status = match self {
43 RouteError::InvalidPolicyData(_) => StatusCode::BAD_REQUEST,
44 RouteError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
45 };
46 (status, sentry_event_id, Json(error)).into_response()
47 }
48}
49
50fn data_example() -> serde_json::Value {
51 serde_json::json!({
52 "hello": "world",
53 "foo": 42,
54 "bar": true
55 })
56}
57
58#[derive(Deserialize, JsonSchema)]
60#[serde(rename = "SetPolicyDataRequest")]
61pub struct SetPolicyDataRequest {
62 #[schemars(example = "data_example")]
63 pub data: serde_json::Value,
64}
65
66pub fn doc(operation: TransformOperation) -> TransformOperation {
67 operation
68 .id("setPolicyData")
69 .summary("Set the current policy data")
70 .tag("policy-data")
71 .response_with::<201, Json<SingleResponse<PolicyData>>, _>(|t| {
72 let [sample, ..] = PolicyData::samples();
73 let response = SingleResponse::new_canonical(sample);
74 t.description("Policy data was successfully set")
75 .example(response)
76 })
77 .response_with::<400, Json<ErrorResponse>, _>(|t| {
78 let error = ErrorResponse::from_error(&RouteError::InvalidPolicyData(
79 mas_policy::LoadError::invalid_data_example(),
80 ));
81 t.description("Invalid policy data").example(error)
82 })
83}
84
85#[tracing::instrument(name = "handler.admin.v1.policy_data.set", skip_all)]
86pub async fn handler(
87 CallContext {
88 mut repo, clock, ..
89 }: CallContext,
90 NoApi(mut rng): NoApi<BoxRng>,
91 State(policy_factory): State<Arc<PolicyFactory>>,
92 Json(request): Json<SetPolicyDataRequest>,
93) -> Result<(StatusCode, Json<SingleResponse<PolicyData>>), RouteError> {
94 let policy_data = repo
95 .policy_data()
96 .set(&mut rng, &clock, request.data)
97 .await?;
98
99 policy_factory.set_dynamic_data(policy_data.clone()).await?;
101
102 repo.save().await?;
103
104 Ok((
105 StatusCode::CREATED,
106 Json(SingleResponse::new_canonical(policy_data.into())),
107 ))
108}
109
110#[cfg(test)]
111mod tests {
112 use hyper::{Request, StatusCode};
113 use insta::assert_json_snapshot;
114 use sqlx::PgPool;
115
116 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
117
118 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
119 async fn test_create(pool: PgPool) {
120 setup();
121 let mut state = TestState::from_pool(pool).await.unwrap();
122 let token = state.token_with_scope("urn:mas:admin").await;
123
124 let request = Request::post("/api/admin/v1/policy-data")
125 .bearer(&token)
126 .json(serde_json::json!({
127 "data": {
128 "hello": "world"
129 }
130 }));
131 let response = state.request(request).await;
132 response.assert_status(StatusCode::CREATED);
133 let body: serde_json::Value = response.json();
134 assert_json_snapshot!(body, @r###"
135 {
136 "data": {
137 "type": "policy-data",
138 "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
139 "attributes": {
140 "created_at": "2022-01-16T14:40:00Z",
141 "data": {
142 "hello": "world"
143 }
144 },
145 "links": {
146 "self": "/api/admin/v1/policy-data/01FSHN9AG0MZAA6S4AF7CTV32E"
147 }
148 },
149 "links": {
150 "self": "/api/admin/v1/policy-data/01FSHN9AG0MZAA6S4AF7CTV32E"
151 }
152 }
153 "###);
154 }
155}