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, &current_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, &current_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.


©2025 SaaS RS | Website | GitHub | GitLab | Contact