refactored logic from templates to rust, and added cli network config
This commit is contained in:
@@ -2,14 +2,6 @@
|
||||
|
||||
A lightweight application for generating dynamic form websites and storing user responses. The form fields, labels, and behavior are fully configurable via a TOML configuration file.
|
||||
|
||||
## Usage
|
||||
|
||||
The server will listen on `127.0.0.1:8081` by default. You can override with:
|
||||
|
||||
```bash
|
||||
SERVER_PORT=8082 form-generator
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application is configured via a TOML file (default: `config.toml`).
|
||||
@@ -20,13 +12,15 @@ See [config.toml](./config.toml) for an example.
|
||||
|
||||
* `json_output`: Path to the JSON file where responses are stored.
|
||||
|
||||
* `form_title`: Title of the page and form.
|
||||
|
||||
* `submit_button`: Text displayed on the form’s submit button.
|
||||
|
||||
* `fields`: List of form fields
|
||||
* `name`: Internal key for storage.
|
||||
* `title`: Label displayed in the form.
|
||||
* `description`: Short description displayed below the field.
|
||||
* `answer_type`: Type of input (`text`, `number`, `email`, `password`, `url`, `tel`, `textarea`).
|
||||
* `answer_type`: Type of input (`text`, `number`, `email`, `password`, `url`, `tel`, `textarea`, `select`, `checkbox`, etc).
|
||||
* `html_before` (optional): HTML snippet rendered before the field.
|
||||
* `html_after` (optional): HTML snippet rendered after the field.
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-02-19T12:46:14.811307031Z",
|
||||
"answers": {
|
||||
"experience_level": "",
|
||||
"full_name": "",
|
||||
"age": "",
|
||||
"newsletter": "true",
|
||||
"about_you": "",
|
||||
"comments": "",
|
||||
"department": "Design",
|
||||
"contact_number": "",
|
||||
"password": "!Y%5#yH^QcZmwHR",
|
||||
"favorite_color": "",
|
||||
"email": "foobar@mail.com",
|
||||
"website": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-02-19T12:46:37.950223451Z",
|
||||
"answers": {
|
||||
"full_name": "",
|
||||
"contact_number": "",
|
||||
"website": "",
|
||||
"favorite_color": "",
|
||||
"age": "",
|
||||
"password": "!Y%5#yH^QcZmwHR",
|
||||
"department": "Design",
|
||||
"comments": "",
|
||||
"experience_level": "Mid-level",
|
||||
"newsletter": "true",
|
||||
"about_you": "",
|
||||
"email": "foobar@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-02-19T12:50:56.697577391Z",
|
||||
"answers": {
|
||||
"website": "https://asdf.com",
|
||||
"password": "!Y%5#yH^QcZmwHR",
|
||||
"favorite_color": "aasdf",
|
||||
"about_you": "asdf",
|
||||
"terms": "true",
|
||||
"comments": "asdfasdf",
|
||||
"age": "3",
|
||||
"contact_number": "asdf",
|
||||
"full_name": "babro",
|
||||
"newsletter": "true",
|
||||
"department": "Engineering",
|
||||
"email": "foobar@mail.com",
|
||||
"experience_level": "Junior"
|
||||
}
|
||||
}
|
||||
]
|
||||
+37
-3
@@ -20,10 +20,28 @@ pub struct FieldDef {
|
||||
pub options: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub enum FieldWidget<'a> {
|
||||
Checkbox,
|
||||
Textarea,
|
||||
Select(&'a [String]),
|
||||
Input(&'a str),
|
||||
}
|
||||
|
||||
impl FieldDef {
|
||||
pub fn widget(&self) -> FieldWidget<'_> {
|
||||
match self.answer_type.as_str() {
|
||||
"checkbox" => FieldWidget::Checkbox,
|
||||
"textarea" => FieldWidget::Textarea,
|
||||
"select" => FieldWidget::Select(self.options.as_deref().unwrap_or(&[])),
|
||||
other => FieldWidget::Input(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct ResponseEntry {
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
answers: HashMap<String, String>,
|
||||
answers: HashMap<String, Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -65,11 +83,27 @@ pub async fn render_form(State(state): State<AppState>) -> impl IntoResponse {
|
||||
|
||||
pub async fn submit(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<HashMap<String, String>>,
|
||||
Form(mut form): Form<HashMap<String, String>>,
|
||||
) -> impl IntoResponse {
|
||||
let mut processed_answers = HashMap::new();
|
||||
|
||||
for field in &state.cfg.fields {
|
||||
let value = form
|
||||
.remove(&field.name)
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
processed_answers.insert(field.name.clone(), value);
|
||||
}
|
||||
|
||||
for (key, val) in form {
|
||||
let value = Some(val.trim().to_string()).filter(|s| !s.is_empty());
|
||||
processed_answers.insert(key, value);
|
||||
}
|
||||
|
||||
let entry = ResponseEntry {
|
||||
timestamp: Utc::now(),
|
||||
answers: form,
|
||||
answers: processed_answers,
|
||||
};
|
||||
|
||||
let _guard = state.file_lock.lock().await;
|
||||
|
||||
+12
-3
@@ -22,6 +22,12 @@ struct Cli {
|
||||
#[arg(short, long)]
|
||||
output_file: Option<String>,
|
||||
|
||||
#[arg(short, long, default_value = "8081")]
|
||||
port: u16,
|
||||
|
||||
#[arg(short = 'H', long)]
|
||||
host: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
@@ -55,7 +61,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// rate limiter
|
||||
let governor_conf = GovernorConfigBuilder::default()
|
||||
.per_second(3)
|
||||
.per_second(1)
|
||||
.burst_size(10)
|
||||
.finish()
|
||||
.unwrap();
|
||||
@@ -81,8 +87,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
.layer(cors)
|
||||
.layer(GovernorLayer::new(governor_conf));
|
||||
|
||||
let port = std::env::var("SERVER_PORT").unwrap_or("8081".to_string());
|
||||
let addr = format!("0.0.0.0:{port}");
|
||||
let addr = if cli.host {
|
||||
format!("0.0.0.0:{}", cli.port)
|
||||
} else {
|
||||
format!("127.0.0.1:{}", cli.port)
|
||||
};
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
tracing::info!("Listening on {}", addr);
|
||||
|
||||
@@ -69,6 +69,29 @@ h1 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.label-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.checkbox-desc {
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
|
||||
+47
-35
@@ -7,50 +7,62 @@
|
||||
<h1>{{ form_title }}</h1>
|
||||
|
||||
<form action="submit" method="POST" autocomplete="off">
|
||||
{% for f in fields %}
|
||||
{% if f.html_before.is_some() %}
|
||||
{{ f.html_before.as_ref().unwrap() | safe }}
|
||||
{% for f in fields %}
|
||||
|
||||
{% if let Some(html) = f.html_before %}
|
||||
{{ html | safe }}
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
{% if f.answer_type == "checkbox" %}
|
||||
<label for="{{ f.name }}" style="display: flex; align-items: flex-start; gap: 12px; cursor: pointer;">
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="checkbox" value="true">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-weight: 600; font-size: 0.95rem; color: var(--text-main);">{{ f.title }}</span>
|
||||
<span class="desc" style="margin: 4px 0 0 0;">{{ f.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% else %}
|
||||
<label for="{{ f.name }}">
|
||||
<span style="display: block;">{{ f.title }}</span>
|
||||
<span class="desc">{{ f.description }}</span>
|
||||
</label>
|
||||
{% match f.widget() %}
|
||||
|
||||
{% if f.answer_type == "textarea" %}
|
||||
<textarea id="{{ f.name }}" name="{{ f.name }}" rows="10" placeholder="{{ f.title|lower }}"></textarea>
|
||||
{% when FieldWidget::Checkbox %}
|
||||
<label for="{{ f.name }}" class="checkbox-label">
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="checkbox" value="true">
|
||||
<div class="label-content">
|
||||
<span class="label-title">{{ f.title }}</span>
|
||||
<span class="desc checkbox-desc">{{ f.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{% elif f.answer_type == "select" %}
|
||||
<select id="{{ f.name }}" name="{{ f.name }}">
|
||||
<option value="">Select an option</option>
|
||||
{% if f.options.is_some() %}
|
||||
{% for opt in f.options.as_ref().unwrap() %}
|
||||
<option value="{{ opt }}">{{ opt }}</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
{% when FieldWidget::Textarea %}
|
||||
<label for="{{ f.name }}">
|
||||
<span class="label-title">{{ f.title }}</span>
|
||||
<span class="desc">{{ f.description }}</span>
|
||||
</label>
|
||||
<textarea id="{{ f.name }}" name="{{ f.name }}" rows="10" placeholder="{{ f.title|lower }}"></textarea>
|
||||
|
||||
{% else %}
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="{{ f.answer_type }}" placeholder="{{ f.title|lower }}">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% when FieldWidget::Select with (options) %}
|
||||
<label for="{{ f.name }}">
|
||||
<span class="label-title">{{ f.title }}</span>
|
||||
<span class="desc">{{ f.description }}</span>
|
||||
</label>
|
||||
<select id="{{ f.name }}" name="{{ f.name }}">
|
||||
<option value="">Select an option</option>
|
||||
{% for opt in options %}
|
||||
<option value="{{ opt }}">{{ opt }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% when FieldWidget::Input with (input_type) %}
|
||||
<label for="{{ f.name }}">
|
||||
<span class="label-title">{{ f.title }}</span>
|
||||
<span class="desc">{{ f.description }}</span>
|
||||
</label>
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="{{ input_type }}" placeholder="{{ f.title|lower }}">
|
||||
|
||||
{% endmatch %}
|
||||
</div>
|
||||
|
||||
{% if f.html_after.is_some() %}
|
||||
{{ f.html_after.as_ref().unwrap() | safe }}
|
||||
{% if let Some(html) = f.html_after %}
|
||||
{{ html | safe }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="submit-container"><button type="submit">{{ submit_button }}</button></div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<div class="submit-container">
|
||||
<button type="submit">{{ submit_button }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user