support pagination
This commit is contained in:
parent
edfdb0405b
commit
26114a37b8
13
README.md
13
README.md
@ -36,22 +36,22 @@ A useful package for serializing and deserializing data (specifically json), use
|
|||||||
|
|
||||||
### Source overview
|
### Source overview
|
||||||
|
|
||||||
#### src/main.rs
|
#### `src/main.rs`
|
||||||
Our applications main entry point, here we declare the use of the applications external packages for easier management.
|
Our applications main entry point, here we declare the use of the applications external packages for easier management.
|
||||||
`main` is then responsible for creating the new question `store` (behind the necessary tokio `RwLock`). `main` creates the axum router, and specifies all of the routes our application supports by pointing them to handlers in the `src/api.rs` file via the appropriate http methods, before serving the axum service
|
`main` is then responsible for creating the new question `store` (behind the necessary tokio `RwLock`). `main` creates the axum router, and specifies all of the routes our application supports by pointing them to handlers in the `src/api.rs` file via the appropriate http methods, before serving the axum service
|
||||||
#### src/api.rs
|
#### `src/api.rs`
|
||||||
Five handlers are defined here for our five API endpoints, supporting the basic CRUD operations regarding questions. Care was taken to handle most common possible error cases, such as attempting to get a question by an id which does not exist, or attempting to create a question with the same ID as one that already exists.
|
Five handlers are defined here for our five API endpoints, supporting the basic CRUD operations regarding questions. Care was taken to handle most common possible error cases, such as attempting to get a question by an id which does not exist, or attempting to create a question with the same ID as one that already exists.
|
||||||
|
|
||||||
Some minor derivations were taken here from the exact routes specified in the text
|
Some minor derivations were taken here from the exact routes specified in the text
|
||||||
* GET /question/:id is included to return a specific question by id
|
* GET /question/:id is included to return a specific question by id
|
||||||
* GET /questions is programmed to return all questions (soon to support pagination)
|
* GET /questions is programmed to use pagination
|
||||||
* PUT /question does not include the question ID in its path param, but rather just the body
|
* PUT /question does not include the question ID in its path param, but rather just the body
|
||||||
* DELETE /question/:id returns in its body the deleted content
|
* DELETE /question/:id returns in its body the deleted content
|
||||||
#### src/question.rs
|
#### `src/question.rs`
|
||||||
This is the definition of our `struct Question`. An interesting potential for confilct arose here, as our question struct is supposed to keep track of the question id in addition of the in-memory hashmap of our store (next section). This two-sources-of-truth situation would lead to various error cases that we would need to handle, and would cause concerns with scalability.
|
This is the definition of our `struct Question`. An interesting potential for confilct arose here, as our question struct is supposed to keep track of the question id in addition of the in-memory hashmap of our store (next section). This two-sources-of-truth situation would lead to various error cases that we would need to handle, and would cause concerns with scalability.
|
||||||
|
|
||||||
To address this, I seperated the Question 'entity' from the Question 'DTO' (data transfer object) from an ORM philosophy. The result is that the Question entity does not contain a direct reference to the question ID, as it is assumed while we are in program space, the store's hashmap will manage this. `to_entity` and `to_dto` function implementions have been provided to make this easier to work with.
|
To address this, I seperated the Question 'entity' from the Question 'DTO' (data transfer object) from an ORM philosophy. The result is that the Question entity does not contain a direct reference to the question ID, as it is assumed while we are in program space, the store's hashmap will manage this. `to_entity` and `to_dto` function implementions have been provided to make this easier to work with.
|
||||||
#### src/store.rs
|
#### `src/store.rs`
|
||||||
The store is responsible for the management of the questions. It does this by
|
The store is responsible for the management of the questions. It does this by
|
||||||
* Loading the questions from the `questions.json` (if it exists, or creating it if not)
|
* Loading the questions from the `questions.json` (if it exists, or creating it if not)
|
||||||
* Creating an in-memory hashmap of the jokes
|
* Creating an in-memory hashmap of the jokes
|
||||||
@ -59,10 +59,11 @@ The store is responsible for the management of the questions. It does this by
|
|||||||
* Writing any mutating changes out to `questions.json`
|
* Writing any mutating changes out to `questions.json`
|
||||||
* Handling possible unpermitted operations by returning errors
|
* Handling possible unpermitted operations by returning errors
|
||||||
* Handling possible file or I/O errors with some sense of grace before panicing
|
* Handling possible file or I/O errors with some sense of grace before panicing
|
||||||
|
### `questions.json`
|
||||||
|
A test dataset of questions. Generated by [ChatGPT](https://chat.openai.com/)
|
||||||
### Looking ahead
|
### Looking ahead
|
||||||
These are a few things still to be added
|
These are a few things still to be added
|
||||||
#### Higher priority
|
#### Higher priority
|
||||||
* Support pagination (via query parameters) for GET /questions. Omission of the query parameters would default in page 0 and a page size of ~10
|
|
||||||
* Support POST /answers
|
* Support POST /answers
|
||||||
* Add some simple API curl examples to this README
|
* Add some simple API curl examples to this README
|
||||||
* API documentation tooling (`utoipa`)
|
* API documentation tooling (`utoipa`)
|
||||||
|
115
questions.json
115
questions.json
@ -4,6 +4,119 @@
|
|||||||
"title": "Have a question?",
|
"title": "Have a question?",
|
||||||
"content": "Just ask me",
|
"content": "Just ask me",
|
||||||
"tags": ["intro"]
|
"tags": ["intro"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "How?",
|
||||||
|
"content": "Please help!",
|
||||||
|
"tags": ["general"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "What is the capital of France?",
|
||||||
|
"content": "Need to know for a quiz!",
|
||||||
|
"tags": ["geography", "quiz"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Can you explain relativity?",
|
||||||
|
"content": "I'm confused about Einstein's theory.",
|
||||||
|
"tags": ["science", "physics"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "What's a good vegan recipe?",
|
||||||
|
"content": "Looking for dinner ideas.",
|
||||||
|
"tags": ["food", "recipes"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "How do I change a tire?",
|
||||||
|
"content": "My car tire is flat, what should I do?",
|
||||||
|
"tags": ["automotive", "DIY"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Best programming language for beginners?",
|
||||||
|
"content": "I'm new to coding.",
|
||||||
|
"tags": ["technology", "programming"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "Is coffee good for health?",
|
||||||
|
"content": "Can drinking coffee have health benefits?",
|
||||||
|
"tags": ["health", "nutrition"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "What's the current time in Tokyo?",
|
||||||
|
"content": "Need to make a call there.",
|
||||||
|
"tags": ["time", "world_clock"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "How to meditate?",
|
||||||
|
"content": "I want to start meditation.",
|
||||||
|
"tags": ["wellness", "mindfulness"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Why do cats purr?",
|
||||||
|
"content": "Curious about cat behaviors.",
|
||||||
|
"tags": ["animals", "cats"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "History of the Roman Empire?",
|
||||||
|
"content": "Looking for a brief overview.",
|
||||||
|
"tags": ["history", "education"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Effective study techniques?",
|
||||||
|
"content": "I need to improve my study habits.",
|
||||||
|
"tags": ["education", "learning"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"title": "Best way to save for retirement?",
|
||||||
|
"content": "Planning my financial future.",
|
||||||
|
"tags": ["finance", "retirement"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"title": "Symptoms of dehydration?",
|
||||||
|
"content": "What should I look out for?",
|
||||||
|
"tags": ["health", "hydration"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"title": "How to write a cover letter?",
|
||||||
|
"content": "Applying for a new job.",
|
||||||
|
"tags": ["career", "advice"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"title": "What is quantum computing?",
|
||||||
|
"content": "I've heard it's the future of tech.",
|
||||||
|
"tags": ["technology", "computing"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"title": "How to improve my credit score?",
|
||||||
|
"content": "Need advice on managing credit.",
|
||||||
|
"tags": ["finance", "credit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"title": "What causes earthquakes?",
|
||||||
|
"content": "Wondering how earthquakes happen.",
|
||||||
|
"tags": ["science", "geology"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"title": "How to make homemade bread?",
|
||||||
|
"content": "Interested in baking.",
|
||||||
|
"tags": ["cooking", "baking"]
|
||||||
}
|
}
|
||||||
]
|
]
|
25
src/api.rs
25
src/api.rs
@ -1,6 +1,8 @@
|
|||||||
/// All API route handler of the application
|
/// All API route handlers of the application
|
||||||
use self::{question::QuestionDTO, store::Store};
|
use self::{question::QuestionDTO, store::Store};
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
const DEFAULT_PAGE: usize = 0;
|
||||||
|
const DEFAULT_PAGE_SIZE: usize = 10;
|
||||||
|
|
||||||
/// Fetches a single question using a provided id
|
/// Fetches a single question using a provided id
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
@ -12,7 +14,7 @@ pub async fn read_question(
|
|||||||
State(store): State<Arc<RwLock<Store>>>,
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
Path(id): Path<u8>,
|
Path(id): Path<u8>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match store.read().await.fetch(id) {
|
match store.read().await.fetch_one(id) {
|
||||||
Ok(question) => question.to_dto(id).into_response(),
|
Ok(question) => question.to_dto(id).into_response(),
|
||||||
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
|
||||||
}
|
}
|
||||||
@ -20,11 +22,17 @@ pub async fn read_question(
|
|||||||
|
|
||||||
/// Fetches all questions
|
/// Fetches all questions
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
/// None (TODO - Support pagination from query parameters)
|
/// 'page' and 'size' as query paramateres
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A list of questions, empty if none exist
|
/// A list of questions, empty if none exist
|
||||||
pub async fn read_questions(State(store): State<Arc<RwLock<Store>>>) -> Response {
|
pub async fn read_questions(
|
||||||
(StatusCode::OK, Json(store.read().await.fetch_all())).into_response()
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
) -> Response {
|
||||||
|
let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE);
|
||||||
|
let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
|
||||||
|
let start: usize = page * size;
|
||||||
|
(StatusCode::OK, Json(store.read().await.fetch_many(start, size))).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new question
|
/// Creates a new question
|
||||||
@ -87,3 +95,10 @@ pub async fn delete_question(
|
|||||||
pub async fn create_answer(State(_store): State<Arc<RwLock<Store>>> /*TODO */) -> Response {
|
pub async fn create_answer(State(_store): State<Arc<RwLock<Store>>> /*TODO */) -> Response {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Pagination {
|
||||||
|
page: Option<usize>,
|
||||||
|
size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ mod api;
|
|||||||
mod question;
|
mod question;
|
||||||
mod store;
|
mod store;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
|
@ -81,14 +81,15 @@ impl Store {
|
|||||||
None => Err(format!("Question with id {} does not exist", id)),
|
None => Err(format!("Question with id {} does not exist", id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn fetch(&self, id: u8) -> Result<Question, String> {
|
pub fn fetch_one(&self, id: u8) -> Result<Question, String> {
|
||||||
match self.questions.get(&id) {
|
match self.questions.get(&id) {
|
||||||
Some(question) => Ok(question.clone()),
|
Some(question) => Ok(question.clone()),
|
||||||
None => Err(format!("Question with id {} does not exists", id)),
|
None => Err(format!("Question with id {} does not exists", id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn fetch_all(&self) -> Vec<Question> {
|
//by nature of the hashmap, pagination does not follow id order
|
||||||
self.questions.values().cloned().collect()
|
pub fn fetch_many(&self, start: usize, size: usize) -> Vec<QuestionDTO> {
|
||||||
|
self.questions.iter().map(|q|q.1.to_dto(*q.0)).skip(start).take(size).collect()
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
pub fn update(&mut self, id: u8, question: Question) -> Result<Question, String> {
|
||||||
if !self.questions.contains_key(&id) {
|
if !self.questions.contains_key(&id) {
|
||||||
|
Reference in New Issue
Block a user