use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectSketch {
pub domain: &'static str,
pub headline: &'static str,
pub user_description: String,
pub models: Vec<ModelSketch>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelSketch {
pub struct_name: &'static str,
pub table: &'static str,
pub fields: Vec<FieldSketch>,
pub rationale: &'static str,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldSketch {
pub name: &'static str,
pub ty: &'static str,
#[serde(default)]
pub nullable: bool,
#[serde(default)]
pub belongs_to: Option<&'static str>,
}
pub fn sketch(description: &str) -> Option<ProjectSketch> {
let lower = description.to_lowercase();
for (keywords, build) in DOMAIN_TABLE {
if keywords.iter().any(|k| lower.contains(k)) {
return Some(build(description.to_string()));
}
}
None
}
type DomainBuilder = fn(String) -> ProjectSketch;
const DOMAIN_TABLE: &[(&[&str], DomainBuilder)] = &[
(
&[
"clinic",
"patient",
"doctor",
"appointment",
"hospital",
"medical",
],
clinic_sketch,
),
(
&["blog", "post", "article", "comment", "publish"],
blog_sketch,
),
(
&[
"shop",
"store",
"product",
"inventory",
"stock",
"sku",
"order",
],
shop_sketch,
),
(
&[
"crm",
"customer",
"lead",
"deal",
"contact",
"sales pipeline",
],
crm_sketch,
),
(
&["task", "todo", "project", "ticket", "issue", "kanban"],
tasks_sketch,
),
];
fn clinic_sketch(description: String) -> ProjectSketch {
ProjectSketch {
domain: "clinic",
headline: "A small clinic — patients, doctors, appointments.",
user_description: description,
models: vec![
ModelSketch {
struct_name: "Patient",
table: "patients",
rationale: "Each person you treat. Name is required; date of birth is useful for the chart, phone for reminders.",
fields: vec![
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "date_of_birth", ty: "DateTime", nullable: true, belongs_to: None },
FieldSketch { name: "phone", ty: "String", nullable: true, belongs_to: None },
],
},
ModelSketch {
struct_name: "Doctor",
table: "doctors",
rationale: "The staff who see patients. Specialty helps when scheduling.",
fields: vec![
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "specialty", ty: "String", nullable: true, belongs_to: None },
],
},
ModelSketch {
struct_name: "Appointment",
table: "appointments",
rationale: "A scheduled visit — links a patient to a doctor with a time.",
fields: vec![
FieldSketch { name: "patient_id", ty: "i64", nullable: false, belongs_to: Some("Patient") },
FieldSketch { name: "doctor_id", ty: "i64", nullable: false, belongs_to: Some("Doctor") },
FieldSketch { name: "scheduled_for", ty: "DateTime", nullable: false, belongs_to: None },
FieldSketch { name: "notes", ty: "String", nullable: true, belongs_to: None },
],
},
],
}
}
fn blog_sketch(description: String) -> ProjectSketch {
ProjectSketch {
domain: "blog",
headline: "A blog — authors and posts.",
user_description: description,
models: vec![
ModelSketch {
struct_name: "Author",
table: "authors",
rationale: "The people who write. Name is required; bio is optional.",
fields: vec![
FieldSketch {
name: "name",
ty: "String",
nullable: false,
belongs_to: None,
},
FieldSketch {
name: "bio",
ty: "String",
nullable: true,
belongs_to: None,
},
],
},
ModelSketch {
struct_name: "Post",
table: "posts",
rationale: "One article. Title, body, and a publication timestamp.",
fields: vec![
FieldSketch {
name: "author_id",
ty: "i64",
nullable: false,
belongs_to: Some("Author"),
},
FieldSketch {
name: "title",
ty: "String",
nullable: false,
belongs_to: None,
},
FieldSketch {
name: "body",
ty: "String",
nullable: false,
belongs_to: None,
},
FieldSketch {
name: "published_at",
ty: "DateTime",
nullable: true,
belongs_to: None,
},
],
},
],
}
}
fn shop_sketch(description: String) -> ProjectSketch {
ProjectSketch {
domain: "shop",
headline: "A small shop — products and orders.",
user_description: description,
models: vec![
ModelSketch {
struct_name: "Product",
table: "products",
rationale: "What you sell. SKU is the unique identifier; stock is what's on hand.",
fields: vec![
FieldSketch { name: "sku", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "price_cents",ty: "i64", nullable: false, belongs_to: None },
FieldSketch { name: "stock", ty: "i64", nullable: false, belongs_to: None },
],
},
ModelSketch {
struct_name: "Order",
table: "orders",
rationale: "A single transaction. Carries the buyer's email so you can reach them without a separate Customer table on day one.",
fields: vec![
FieldSketch { name: "product_id", ty: "i64", nullable: false, belongs_to: Some("Product") },
FieldSketch { name: "quantity", ty: "i64", nullable: false, belongs_to: None },
FieldSketch { name: "buyer_email", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "placed_at", ty: "DateTime", nullable: false, belongs_to: None },
],
},
],
}
}
fn crm_sketch(description: String) -> ProjectSketch {
ProjectSketch {
domain: "crm",
headline: "A small CRM — companies, contacts, deals.",
user_description: description,
models: vec![
ModelSketch {
struct_name: "Company",
table: "companies",
rationale: "An organisation you might sell to.",
fields: vec![
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "website", ty: "String", nullable: true, belongs_to: None },
],
},
ModelSketch {
struct_name: "Contact",
table: "contacts",
rationale: "A person at a company. Belongs to one Company.",
fields: vec![
FieldSketch { name: "company_id", ty: "i64", nullable: false, belongs_to: Some("Company") },
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "email", ty: "String", nullable: true, belongs_to: None },
FieldSketch { name: "phone", ty: "String", nullable: true, belongs_to: None },
],
},
ModelSketch {
struct_name: "Deal",
table: "deals",
rationale: "An opportunity. Linked to a Contact; status tracks stage; amount is in cents to keep arithmetic clean.",
fields: vec![
FieldSketch { name: "contact_id", ty: "i64", nullable: false, belongs_to: Some("Contact") },
FieldSketch { name: "title", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "status", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "amount_cents",ty: "i64", nullable: true, belongs_to: None },
FieldSketch { name: "closed_at", ty: "DateTime", nullable: true, belongs_to: None },
],
},
],
}
}
fn tasks_sketch(description: String) -> ProjectSketch {
ProjectSketch {
domain: "tasks",
headline: "A task tracker — projects and tasks.",
user_description: description,
models: vec![
ModelSketch {
struct_name: "Project",
table: "projects",
rationale: "A container for related tasks.",
fields: vec![
FieldSketch { name: "name", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "description", ty: "String", nullable: true, belongs_to: None },
],
},
ModelSketch {
struct_name: "Task",
table: "tasks",
rationale: "One thing to do. Status moves from todo → in_progress → done; priority is a small integer.",
fields: vec![
FieldSketch { name: "project_id", ty: "i64", nullable: false, belongs_to: Some("Project") },
FieldSketch { name: "title", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "status", ty: "String", nullable: false, belongs_to: None },
FieldSketch { name: "priority", ty: "i64", nullable: true, belongs_to: None },
FieldSketch { name: "due_at", ty: "DateTime", nullable: true, belongs_to: None },
],
},
],
}
}
use crate::ai::{AddModel, AddRelation, FieldSpec, Plan, Primitive, RelationKind};
pub fn primitives_for(model: &ModelSketch) -> Vec<Primitive> {
let fields: Vec<FieldSpec> = model
.fields
.iter()
.map(|f| FieldSpec {
name: f.name.to_string(),
ty: f.ty.to_string(),
nullable: f.nullable,
editable: true,
})
.collect();
let mut out: Vec<Primitive> = Vec::with_capacity(1 + model.fields.len());
out.push(Primitive::AddModel(AddModel {
name: model.struct_name.to_string(),
table: model.table.to_string(),
fields,
}));
for f in &model.fields {
if let Some(target) = f.belongs_to {
out.push(Primitive::AddRelation(AddRelation {
from: model.struct_name.to_string(),
kind: RelationKind::BelongsTo,
to: target.to_string(),
via: f.name.to_string(),
required: false,
on_delete: Default::default(),
}));
}
}
out
}
pub fn plan_for(accepted: &[ModelSketch]) -> Plan {
let mut steps: Vec<Primitive> = Vec::new();
for m in accepted {
steps.extend(primitives_for(m));
}
Plan { steps }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clinic_keyword_yields_clinic_sketch() {
let s = sketch("a small clinic with patients and appointments").unwrap();
assert_eq!(s.domain, "clinic");
let names: Vec<&str> = s.models.iter().map(|m| m.struct_name).collect();
assert_eq!(names, vec!["Patient", "Doctor", "Appointment"]);
}
#[test]
fn ambiguous_input_refuses() {
assert!(sketch("I want to build something").is_none());
assert!(sketch("").is_none());
}
#[test]
fn shop_template_uses_only_valid_types() {
use crate::schema::VALID_TYPE_NAMES;
let s = sketch("a shop with products and orders").unwrap();
for m in &s.models {
for f in &m.fields {
assert!(
VALID_TYPE_NAMES.contains(&f.ty),
"field {}.{} has invalid type `{}`",
m.struct_name,
f.name,
f.ty
);
}
}
}
#[test]
fn belongs_to_targets_an_earlier_model() {
for descr in [
"clinic",
"blog",
"shop with products",
"crm with deals",
"tasks",
] {
let s = sketch(descr).unwrap();
let mut seen: Vec<&str> = Vec::new();
for m in &s.models {
for f in &m.fields {
if let Some(target) = f.belongs_to {
assert!(
seen.contains(&target),
"{}.{} → `{}` references a model not yet introduced",
m.struct_name,
f.name,
target
);
}
}
seen.push(m.struct_name);
}
}
}
#[test]
fn primitives_for_emits_add_model_then_relations() {
let s = sketch("clinic").unwrap();
let appointment = s
.models
.iter()
.find(|m| m.struct_name == "Appointment")
.unwrap();
let ops = primitives_for(appointment);
assert!(matches!(ops.first(), Some(Primitive::AddModel(_))));
let n_relations = ops
.iter()
.filter(|p| matches!(p, Primitive::AddRelation(_)))
.count();
assert_eq!(n_relations, 2);
}
#[test]
fn plan_for_full_sketch_validates_against_empty_schema() {
use crate::schema::{Schema, SCHEMA_VERSION};
let empty = Schema {
version: SCHEMA_VERSION,
rustio_version: env!("CARGO_PKG_VERSION").to_string(),
models: vec![],
};
let sk = sketch("a small clinic").unwrap();
let plan = plan_for(&sk.models);
plan.validate(&empty)
.expect("clinic sketch should simulate cleanly against empty schema");
}
}