Skip to content

Commit

Permalink
impl gen controller (#751)
Browse files Browse the repository at this point in the history
* impl gen controller

* align path separator

* improve route uri building

* align tests
  • Loading branch information
jondot authored Sep 16, 2024
1 parent 3840b8b commit 9451924
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-generators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test

- name: controller
run: cargo run -- generate controller post && cargo build && cargo test requests::post
run: cargo run -- generate controller pages about && cargo build && cargo test pages
working-directory: ./examples/demo
env:
REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}}
Expand Down
23 changes: 20 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ enum Commands {
/// Run jobs that are associated with a specific tag.
#[arg(short, long, action)]
tag: Option<String>,
/// Specify a path to a dedicated scheduler configuration file. by default load schedulers job setting from environment config.
/// Specify a path to a dedicated scheduler configuration file. by
/// default load schedulers job setting from environment config.
#[clap(value_parser)]
#[arg(short, long, action)]
config: Option<PathBuf>,
Expand Down Expand Up @@ -179,6 +180,13 @@ enum ComponentArg {
Controller {
/// Name of the thing to generate
name: String,

/// Actions
actions: Vec<String>,

/// The kind of scaffold to generate
#[clap(short, long, value_enum, default_value_t = gen::ScaffoldKind::Api)]
kind: gen::ScaffoldKind,
},
/// Generate a Task based on the given name
Task {
Expand Down Expand Up @@ -244,7 +252,15 @@ impl TryFrom<ComponentArg> for Component {

Ok(Self::Scaffold { name, fields, kind })
}
ComponentArg::Controller { name } => Ok(Self::Controller { name }),
ComponentArg::Controller {
name,
actions,
kind,
} => Ok(Self::Controller {
name,
actions,
kind,
}),
ComponentArg::Task { name } => Ok(Self::Task { name }),
ComponentArg::Scheduler {} => Ok(Self::Scheduler {}),
ComponentArg::Worker { name } => Ok(Self::Worker { name }),
Expand All @@ -260,7 +276,8 @@ enum DbCommands {
Create,
/// Migrate schema (up)
Migrate,
/// Run one down migration, or add a number to run multiple down migrations (i.e. `down 2`)
/// Run one down migration, or add a number to run multiple down migrations
/// (i.e. `down 2`)
Down {
/// The number of migrations to rollback
#[arg(default_value_t = 1)]
Expand Down
40 changes: 23 additions & 17 deletions src/controller/app_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,32 +100,38 @@ impl AppRoutes {

#[must_use]
pub fn collect(&self) -> Vec<ListRoutes> {
let base_url_prefix = self.get_prefix().map_or("/", |url| url.as_str());
let base_url_prefix = self
.get_prefix()
// add a leading slash forcefully. Axum routes must start with a leading slash.
// if we have double leading slashes - it will get normalized into a single slash later
.map_or("/".to_string(), |url| format!("/{}", url.as_str()));

self.get_routes()
.iter()
.flat_map(|router| {
let mut uri_parts = vec![base_url_prefix];
if let Some(prefix) = router.prefix.as_ref() {
uri_parts.push(prefix);
.flat_map(|controller| {
let mut uri_parts = vec![base_url_prefix.clone()];
if let Some(prefix) = controller.prefix.as_ref() {
uri_parts.push(prefix.to_string());
}
router.handlers.iter().map(move |controller| {
let uri = format!("{}{}", uri_parts.join("/"), &controller.uri);
let binding = NORMALIZE_URL.replace_all(&uri, "/");

let uri = if binding.len() > 1 {
NORMALIZE_URL
.replace_all(&uri, "/")
.strip_suffix('/')
.map_or_else(|| binding.to_string(), std::string::ToString::to_string)
controller.handlers.iter().map(move |handler| {
let mut parts = uri_parts.clone();
parts.push(handler.uri.to_string());
let joined_parts = parts.join("/");

let normalized = NORMALIZE_URL.replace_all(&joined_parts, "/");
let uri = if normalized == "/" {
normalized.to_string()
} else {
binding.to_string()
normalized.strip_suffix('/').map_or_else(
|| normalized.to_string(),
std::string::ToString::to_string,
)
};

ListRoutes {
uri,
actions: controller.actions.clone(),
method: controller.method.clone(),
actions: handler.actions.clone(),
method: handler.method.clone(),
}
})
})
Expand Down
53 changes: 53 additions & 0 deletions src/gen/controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use rrgen::RRgen;
use serde_json::json;

use crate::{app::Hooks, gen};

const API_CONTROLLER_CONTROLLER_T: &str = include_str!("templates/controller/api/controller.t");
const API_CONTROLLER_TEST_T: &str = include_str!("templates/controller/api/test.t");

const HTMX_CONTROLLER_CONTROLLER_T: &str = include_str!("templates/controller/htmx/controller.t");
const HTMX_VIEW_T: &str = include_str!("templates/controller/htmx/view.t");

const HTML_CONTROLLER_CONTROLLER_T: &str = include_str!("templates/controller/html/controller.t");
const HTML_VIEW_T: &str = include_str!("templates/controller/html/view.t");

use super::collect_messages;
use crate::Result;

pub fn generate<H: Hooks>(
rrgen: &RRgen,
name: &str,
actions: &[String],
kind: &gen::ScaffoldKind,
) -> Result<String> {
let vars = json!({"name": name, "actions": actions, "pkg_name": H::app_name()});
match kind {
gen::ScaffoldKind::Api => {
let res1 = rrgen.generate(API_CONTROLLER_CONTROLLER_T, &vars)?;
let res2 = rrgen.generate(API_CONTROLLER_TEST_T, &vars)?;
let messages = collect_messages(vec![res1, res2]);
Ok(messages)
}
gen::ScaffoldKind::Html => {
let mut messages = Vec::new();
let res = rrgen.generate(HTML_CONTROLLER_CONTROLLER_T, &vars)?;
messages.push(res);
for action in actions {
let vars = json!({"name": name, "action": action, "pkg_name": H::app_name()});
messages.push(rrgen.generate(HTML_VIEW_T, &vars)?);
}
Ok(collect_messages(messages))
}
gen::ScaffoldKind::Htmx => {
let mut messages = Vec::new();
let res = rrgen.generate(HTMX_CONTROLLER_CONTROLLER_T, &vars)?;
messages.push(res);
for action in actions {
let vars = json!({"name": name, "action": action, "pkg_name": H::app_name()});
messages.push(rrgen.generate(HTMX_VIEW_T, &vars)?);
}
Ok(collect_messages(messages))
}
}
}
20 changes: 16 additions & 4 deletions src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use rrgen::{GenResult, RRgen};
use serde::{Deserialize, Serialize};
use serde_json::json;

mod controller;
#[cfg(feature = "with-db")]
mod model;
#[cfg(feature = "with-db")]
Expand Down Expand Up @@ -154,6 +155,12 @@ pub enum Component {
Controller {
/// Name of the thing to generate
name: String,

/// Action names
actions: Vec<String>,

// kind
kind: ScaffoldKind,
},
Task {
/// Name of the thing to generate
Expand Down Expand Up @@ -198,10 +205,15 @@ pub fn generate<H: Hooks>(component: Component, config: &Config) -> Result<()> {
let vars = json!({ "name": name, "ts": chrono::Utc::now(), "pkg_name": H::app_name()});
rrgen.generate(MIGRATION_T, &vars)?;
}
Component::Controller { name } => {
let vars = json!({ "name": name, "pkg_name": H::app_name()});
rrgen.generate(CONTROLLER_T, &vars)?;
rrgen.generate(CONTROLLER_TEST_T, &vars)?;
Component::Controller {
name,
actions,
kind,
} => {
println!(
"{}",
controller::generate::<H>(&rrgen, &name, &actions, &kind)?
);
}
Component::Task { name } => {
let vars = json!({"name": name, "pkg_name": H::app_name()});
Expand Down
5 changes: 3 additions & 2 deletions src/gen/scaffold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde_json::json;
use crate::{app::Hooks, gen};

const API_CONTROLLER_SCAFFOLD_T: &str = include_str!("templates/scaffold/api/controller.t");
const API_CONTROLLER_TEST_T: &str = include_str!("templates/scaffold/api/test.t");

const HTMX_CONTROLLER_SCAFFOLD_T: &str = include_str!("templates/scaffold/htmx/controller.t");
const HTMX_BASE_SCAFFOLD_T: &str = include_str!("templates/scaffold/htmx/base.t");
Expand All @@ -21,7 +22,7 @@ const HTML_VIEW_CREATE_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/
const HTML_VIEW_SHOW_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/view_show.t");
const HTML_VIEW_LIST_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/view_list.t");

use super::{collect_messages, model, CONTROLLER_TEST_T, MAPPINGS};
use super::{collect_messages, model, MAPPINGS};
use crate::{errors::Error, Result};

pub fn generate<H: Hooks>(
Expand Down Expand Up @@ -59,7 +60,7 @@ pub fn generate<H: Hooks>(
match kind {
gen::ScaffoldKind::Api => {
let res1 = rrgen.generate(API_CONTROLLER_SCAFFOLD_T, &vars)?;
let res2 = rrgen.generate(CONTROLLER_TEST_T, &vars)?;
let res2 = rrgen.generate(API_CONTROLLER_TEST_T, &vars)?;
let messages = collect_messages(vec![res1, res2]);
Ok(format!("{model_messages}{messages}"))
}
Expand Down
40 changes: 40 additions & 0 deletions src/gen/templates/controller/api/controller.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{% set file_name = name | snake_case -%}
{% set module_name = file_name | pascal_case -%}
to: src/controllers/{{ file_name }}.rs
skip_exists: true
message: "Controller `{{module_name}}` was added successfully."
injections:
- into: src/controllers/mod.rs
append: true
content: "pub mod {{ file_name }};"
- into: src/app.rs
after: "AppRoutes::"
content: " .add_route(controllers::{{ file_name }}::routes())"
---
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use axum::debug_handler;

#[debug_handler]
pub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {
format::empty()
}

{% for action in actions -%}
#[debug_handler]
pub async fn {{action}}(State(_ctx): State<AppContext>) -> Result<Response> {
format::empty()
}

{% endfor -%}

pub fn routes() -> Routes {
Routes::new()
.prefix("{{file_name | plural}}/")
.add("/", get(index))
{%- for action in actions %}
.add("{{action}}", get({{action}}))
{%- endfor %}
}
26 changes: 26 additions & 0 deletions src/gen/templates/controller/api/test.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% set file_name = name | snake_case -%}
{% set module_name = file_name | pascal_case -%}
to: tests/requests/{{ file_name }}.rs
skip_exists: true
message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo run test`."
injections:
- into: tests/requests/mod.rs
append: true
content: "pub mod {{ file_name }};"
---
use {{pkg_name}}::app::App;
use loco_rs::testing;
use serial_test::serial;

{% for action in actions -%}
#[tokio::test]
#[serial]
async fn can_get_{{action}}() {
testing::request::<App, _, _>(|request, _ctx| async move {
let res = request.get("/{{ name | snake_case }}/{{action}}").await;
assert_eq!(res.status_code(), 200);
})
.await;
}

{% endfor -%}
37 changes: 37 additions & 0 deletions src/gen/templates/controller/html/controller.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% set file_name = name | snake_case -%}
{% set module_name = file_name | pascal_case -%}
to: src/controllers/{{ file_name }}.rs
skip_exists: true
message: "Controller `{{module_name}}` was added successfully."
injections:
- into: src/controllers/mod.rs
append: true
content: "pub mod {{ file_name }};"
- into: src/app.rs
after: "AppRoutes::"
content: " .add_route(controllers::{{ file_name }}::routes())"
---
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use axum::debug_handler;

{% for action in actions -%}
#[debug_handler]
pub async fn {{action}}(
ViewEngine(v): ViewEngine<TeraView>,
State(_ctx): State<AppContext>
) -> Result<Response> {
format::render().view(&v, "{{file_name}}/{{action}}.html", serde_json::json!({}))
}

{% endfor -%}

pub fn routes() -> Routes {
Routes::new()
.prefix("{{file_name | plural}}/")
{%- for action in actions %}
.add("{{action}}", get({{action}}))
{%- endfor %}
}
19 changes: 19 additions & 0 deletions src/gen/templates/controller/html/view.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% set file_name = name | snake_case -%}
{% set module_name = file_name | pascal_case -%}
to: assets/views/{{file_name}}/{{action}}.html
skip_exists: true
message: "{{file_name}}/{{action}} view was added successfully."
---
<!DOCTYPE html>
<html lang="en">

<head>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
</head>

<body class="prose p-10">
<h1>View {{action}}</h1>
Find me in <code>{{file_name}}/{{action}}</code>
</body>

</html>
Loading

0 comments on commit 9451924

Please sign in to comment.