The generate controller command
The generate controller
command is typically used after generating a resource, in order to replace
a resource's Not Implemented Yet stubs with a default implementation.
When it is used like this:
$ saas-rs generate controller invoice --service user --version 1
$ make
$ git add -A
$ git commit -m "saas-rs generate controller invoice --service user --version 1"
The following content is added or changed in your Rust workspace:
crates/
└── user_server/
└── src/
└── v1/
├── controllers/
│ ├── invoice.rs
│ └── mod.rs
└── mod.rs
And the following things occurred:
- in
crates/user_server/src/v1/mod.rs
, the main service implementation functions for the invoice resource were rewritten, replacing any former function body with a new line that delegates to a function of the same name in the controller module - in
crates/user_server/src/v1/controllers/invoice.rs
, default CRUD handling functions were generated
An examination of the crates/user_server/src/v1/mod.rs
shows the service functions that were rewritten to now
delegate to the new controller module:
impl User for UserGrpcServerV1 {
...
async fn create_invoice(&self, req: Request<CreateInvoiceRequest>) -> Result<Response<CreateInvoiceResponse>, Status> {
controllers::invoice::create(self.app_state.clone(), req).await
}
async fn delete_invoice(&self, req: Request<DeleteInvoiceRequest>) -> Result<Response<DeleteInvoiceResponse>, Status> {
controllers::invoice::delete(self.app_state.clone(), req).await
}
...
}
An examination of the crates/user_server/src/v1/controllers/invoice.rs
module shows the default implementation for
CRUD handling:
use crate::v1::{validation_errors, AppState};
use bson::{doc, DateTime};
use common::metadata::require_authorization;
use config_store::Bucket;
use log::debug;
use protocol::acme::user::v1::{
invoice, CreateInvoiceRequest, CreateInvoiceResponse, DeleteInvoiceRequest, DeleteInvoiceResponse,
FindInvoiceRequest, FindInvoiceResponse, FindManyInvoicesRequest, FindManyInvoicesResponse, Invoice,
UpdateInvoiceRequest, UpdateInvoiceResponse, ValidateInvoiceRequest, ValidateInvoiceResponse,
};
use saas_rs_sdk::pbbson::Model;
use std::sync::Arc;
use tonic::{Request, Response, Status};
pub async fn create(
app_state: Arc<AppState>,
req: Request<CreateInvoiceRequest>,
) -> Result<Response<CreateInvoiceResponse>, Status> {
let current_account_id = require_authorization(&req)?;
let mut invoice = req.into_inner().invoice.unwrap();
invoice.created_by_account_id = Some(current_account_id.clone());
invoice.owner = Some(invoice::Owner::OwnerAccountId(current_account_id));
// Store
let invoice: Invoice = app_state
.config_store
.create(Bucket::Invoices, common::model_from_message(&invoice)?)
.await
.map_err(|e| Status::internal(e.to_string()))?
.try_into()
.map_err(|e| Status::internal(e.to_string()))?;
// Return result
Ok(Response::new(CreateInvoiceResponse { invoice: Some(invoice) }))
}
pub async fn delete(
app_state: Arc<AppState>,
req: Request<DeleteInvoiceRequest>,
) -> Result<Response<DeleteInvoiceResponse>, Status> {
// Authorize
let current_account_id = require_authorization(&req)?;
// Find
let req = req.into_inner();
let _existing_invoice: Invoice = app_state
.config_store
.find(Bucket::Invoices, &req.id)
.await
.map_err(|e| Status::internal(e.to_string()))?
.try_into()
.map_err(|e| Status::internal(e.to_string()))?;
// Check access
// check_can_write(self, &existing_invoice, ¤t_account_id).await?;
// Delete
app_state
.config_store
.delete(Bucket::Invoices, &req.id, Some(current_account_id))
.await
.map_err(|e| Status::internal(e.to_string()))?;
// Return result
Ok(Response::new(DeleteInvoiceResponse {}))
}
pub async fn find(
app_state: Arc<AppState>,
req: Request<FindInvoiceRequest>,
) -> Result<Response<FindInvoiceResponse>, Status> {
let id = req.into_inner().id.clone();
let invoice: Invoice = app_state
.config_store
.find(Bucket::Invoices, &id)
.await
.map_err(|e| Status::internal(e.to_string()))?
.try_into()
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(FindInvoiceResponse { invoice: Some(invoice) }))
}
pub async fn find_many(
app_state: Arc<AppState>,
req: Request<FindManyInvoicesRequest>,
) -> Result<Response<FindManyInvoicesResponse>, Status> {
let req = req.into_inner();
let filter = Model::from({
let req_filter = req.filter;
let mut filter = doc! {"deletedAt": None::<DateTime>};
if let Some(req_filter) = req_filter {
if let Some(ref id) = req_filter.id {
filter.insert("id", id.clone());
}
//if let Some(ref owner_account_id) = req_filter.owner_account_id {
// filter.insert("ownerAccountId", owner_account_id.clone());
//}
}
filter
});
let invoices: Vec<_> = app_state
.config_store
.find_many(Bucket::Invoices, filter, None)
.await
.map_err(|e| Status::internal(e.to_string()))?
.into_iter()
.map(|model| model.try_into().unwrap())
.collect();
Ok(Response::new(FindManyInvoicesResponse { invoices }))
}
pub async fn update(
app_state: Arc<AppState>,
req: Request<UpdateInvoiceRequest>,
) -> Result<Response<UpdateInvoiceResponse>, Status> {
// Authorize
let current_account_id = require_authorization(&req)?;
// Find existing
let req_metadata = req.metadata().clone();
let req_invoice = req.into_inner().invoice.unwrap();
let existing_invoice: Invoice = app_state
.config_store
.find(Bucket::Invoices, &req_invoice.id)
.await
.map_err(|e| Status::internal(e.to_string()))?
.try_into()
.map_err(|e| Status::internal(e.to_string()))?;
// Validate
let validate_invoice_req = common::metadata::forward(
ValidateInvoiceRequest {
invoice: Some(req_invoice.clone()),
existing: true,
},
req_metadata,
)?;
let validate_invoice_res = validate(app_state.clone(), validate_invoice_req).await?.into_inner();
if !validate_invoice_res.errors.is_empty() {
let status = validation_errors::to_grpc(validate_invoice_res.errors);
debug!(
validation_errs = status.message();
"Failure validating invoice"
);
return Err(status);
}
// Check access
// check_can_write(self, &existing_invoice, ¤t_account_id).await?;
// Apply updates
let mut invoice = existing_invoice.clone();
saas_rs_sdk::storage::models::assign_message(&mut invoice, &req_invoice)?;
invoice.updated_by_account_id = Some(current_account_id);
// Store
let invoice: Invoice = app_state
.config_store
.update(Bucket::Invoices, common::model_from_message(&invoice)?)
.await
.map_err(|e| Status::internal(e.to_string()))?
.try_into()
.map_err(|e| Status::internal(e.to_string()))?;
// Return result
Ok(Response::new(UpdateInvoiceResponse { invoice: Some(invoice) }))
}
pub async fn validate(
_app_state: Arc<AppState>,
req: Request<ValidateInvoiceRequest>,
) -> Result<Response<ValidateInvoiceResponse>, Status> {
// Authorize
let _current_account_id = require_authorization(&req)?;
let req = req.into_inner();
let _invoice = req.invoice.unwrap();
let /*mut*/ errors = vec![];
// TODO
// if invoice.__some_field__.is_empty() {
// errors.push(ErrorObject {
// status: format!("{:03}", http::StatusCode::BAD_REQUEST.as_u16()),
// title: "Validation Error".to_string(),
// detail: "A __some_field__ is required".to_string(),
// ..Default::default()
// });
// }
Ok(Response::new(ValidateInvoiceResponse { errors }))
}
Notice that placeholders were created to show you how to add validations for required fields and other business rules specific to your vertical.