1use std::sync::{Arc, Mutex};
31
32use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
33use bel::{Context, Program, Value};
34use serde::{Deserialize, Serialize};
35
36const WORK_TODO_POLICY: &str = "!text.contains('TV')"; const HOME_TODO_POLICY: &str = "!text.contains('Rust')"; #[derive(Clone, Deserialize, Serialize)]
42struct Todo {
43 text: String,
44 kind: TodoKind,
45}
46
47#[derive(Clone, Deserialize, Serialize)]
49#[serde(rename_all = "lowercase")]
50enum TodoKind {
51 Work,
52 Home,
53}
54
55async fn add_todo(
57 State(AppContext {
58 todos,
59 decider,
60 }): State<AppContext>,
61 Json(todo): Json<Todo>,
62) -> impl IntoResponse {
63 match decider.todo_is_allowed(&todo) {
65 Ok(is_allowed) => {
66 if !is_allowed {
68 return StatusCode::BAD_REQUEST;
69 }
70
71 let mut todos = todos.lock().unwrap();
73 todos.push(todo);
74 StatusCode::ACCEPTED
75 }
76 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
78 }
79}
80
81async fn list_todos(
83 State(AppContext {
84 todos, ..
85 }): State<AppContext>,
86) -> impl IntoResponse {
87 Json(todos.lock().unwrap().clone())
88}
89
90struct PolicyDecider(Context<'static>);
92
93impl PolicyDecider {
94 fn new() -> Self {
96 Self(Context::default())
97 }
98
99 fn todo_is_allowed(&self, todo: &Todo) -> Result<bool, TodosError> {
101 let mut ctx = self.0.new_inner_scope();
103 ctx.add_variable_from_value("text", todo.text.clone());
105
106 let policy = match todo.kind {
108 TodoKind::Home => HOME_TODO_POLICY,
109 TodoKind::Work => WORK_TODO_POLICY,
110 };
111
112 let program = Program::compile(policy)?;
114
115 match program.execute(&ctx)? {
118 Value::Bool(b) => Ok(b),
119 _ => Err(TodosError::Invalid),
120 }
121 }
122}
123
124#[derive(Debug, thiserror::Error)]
126enum TodosError {
127 #[error("CEL execution error: {0}")]
128 Execution(#[from] bel::ExecutionError),
129 #[error(transparent)]
130 Io(#[from] std::io::Error),
131 #[error("CEL parse error: {0}")]
132 Parse(#[from] bel::ParseErrors),
133 #[error("invalid TODO")]
134 Invalid,
135}
136
137#[derive(Clone)]
139struct AppContext {
140 todos: Arc<Mutex<Vec<Todo>>>,
141 decider: Arc<PolicyDecider>,
142}
143
144#[tokio::main]
145async fn main() -> Result<(), TodosError> {
146 let ctx = AppContext {
147 todos: Arc::new(Mutex::new(Vec::new())),
148 decider: Arc::new(PolicyDecider::new()),
149 };
150
151 let app = Router::new()
152 .route("/todos", get(list_todos).post(add_todo))
153 .with_state(ctx);
154
155 let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
156
157 Ok(axum::serve(listener, app).await?)
158}