Skip to main content

axum/
axum.rs

1//! This is a pretty straightforward TODO app using the Axum web framework.
2//! You can add TODOs to your list but only if they meet certain criteria.
3//!
4//! To run it:
5//!
6//! ```shell
7//! cargo run
8//! ```
9//!
10//! Add a TODO (HTTP 202):
11//!
12//! ```shell
13//! curl -w "%{http_code}" -XPOST -H "content-type: application/json" \
14//!   http://localhost:8080/todos -d '{"kind":"work","text":"Learn more Rust"}'
15//! ```
16//!
17//! Fetch the current TODOs:
18//!
19//! ```shell
20//! curl http://localhost:8080/todos
21//! ```
22//!
23//! Add another TODO (HTTP 400):
24//!
25//! ```shell
26//! curl -w "%{http_code}" -XPOST -H "content-type: application/json" \
27//!   http://localhost:8080/todos -d '{"kind":"home","text":"Learn more Rust"}'
28//! ```
29
30use 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
36// Policies dictating which text TODOs may contain
37const WORK_TODO_POLICY: &str = "!text.contains('TV')"; // Don't watch TV at work!
38const HOME_TODO_POLICY: &str = "!text.contains('Rust')"; // Don't hack on Rust at home!
39
40// TODOs carry some text and have a kind
41#[derive(Clone, Deserialize, Serialize)]
42struct Todo {
43    text: String,
44    kind: TodoKind,
45}
46
47// TODOs are either for work or for home
48#[derive(Clone, Deserialize, Serialize)]
49#[serde(rename_all = "lowercase")]
50enum TodoKind {
51    Work,
52    Home,
53}
54
55// POST a new TODO to the list
56async fn add_todo(
57    State(AppContext {
58        todos,
59        decider,
60    }): State<AppContext>,
61    Json(todo): Json<Todo>,
62) -> impl IntoResponse {
63    // Use the policy decider to see if the TODO is allowed
64    match decider.todo_is_allowed(&todo) {
65        Ok(is_allowed) => {
66            // If not, throw HTTP 400
67            if !is_allowed {
68                return StatusCode::BAD_REQUEST;
69            }
70
71            // If allowed, add it to the TODOs and return HTTP 202
72            let mut todos = todos.lock().unwrap();
73            todos.push(todo);
74            StatusCode::ACCEPTED
75        }
76        // If there's an error return an HTTP 500
77        Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
78    }
79}
80
81// GET the current list of TODOs
82async fn list_todos(
83    State(AppContext {
84        todos, ..
85    }): State<AppContext>,
86) -> impl IntoResponse {
87    Json(todos.lock().unwrap().clone())
88}
89
90// The policy engine for our TODOs app
91struct PolicyDecider(Context<'static>);
92
93impl PolicyDecider {
94    // Start with a wrapper around the default Context
95    fn new() -> Self {
96        Self(Context::default())
97    }
98
99    // Determine whether a given TODO is allowed
100    fn todo_is_allowed(&self, todo: &Todo) -> Result<bool, TodosError> {
101        // Create a new mutable context out of the root context
102        let mut ctx = self.0.new_inner_scope();
103        // Add the TODO's text as a variable so that it can be part of the expression
104        ctx.add_variable_from_value("text", todo.text.clone());
105
106        // Which policy to enforce depends on the kind of TODO
107        let policy = match todo.kind {
108            TodoKind::Home => HOME_TODO_POLICY,
109            TodoKind::Work => WORK_TODO_POLICY,
110        };
111
112        // Compile the program
113        let program = Program::compile(policy)?;
114
115        // Execute the program and either return a Boolean or the TODO is
116        // considered invalid
117        match program.execute(&ctx)? {
118            Value::Bool(b) => Ok(b),
119            _ => Err(TodosError::Invalid),
120        }
121    }
122}
123
124// Custom error type
125#[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// The state attached to the HTTP router
138#[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}