use std::{fs, path::Path};
use console::style;
use heck::ToUpperCamelCase;
use serde_json::Value;
use crate::commands::crud::{self, FieldDef};
fn infer_field_type(_key: &str, value: &Value) -> String {
match value {
Value::String(_) => "string".to_string(),
Value::Bool(_) => "bool".to_string(),
Value::Null => "json".to_string(),
Value::Array(_) => "json".to_string(),
Value::Number(n) => {
if n.is_f64() && n.as_f64().map(|f| f.fract() != 0.0).unwrap_or(false) {
"float".to_string()
} else {
"int".to_string()
}
}
Value::Object(_) => {
"json".to_string()
}
}
}
fn json_to_fields(obj: &serde_json::Map<String, Value>) -> Vec<FieldDef> {
obj.iter()
.filter(|(k, _)| k.as_str() != "id")
.map(|(k, v)| FieldDef {
name: k.clone(),
field_type: infer_field_type(k, v),
optional: false,
})
.collect()
}
pub fn from_json(
name: &str,
json_input: &str,
from_file: bool,
dry_run: bool,
force: bool,
) -> anyhow::Result<()> {
let raw = if from_file {
if !Path::new(json_input).exists() {
anyhow::bail!("File not found: {json_input}");
}
fs::read_to_string(json_input)?
} else {
json_input.to_owned()
};
let parsed: Value =
serde_json::from_str(&raw).map_err(|e| anyhow::anyhow!("Invalid JSON: {e}"))?;
let obj = match &parsed {
Value::Object(m) => m,
_ => anyhow::bail!("JSON must be an object {{ ... }}"),
};
let fields = json_to_fields(obj);
if fields.is_empty() {
anyhow::bail!("No fields inferred from JSON (object was empty or only had 'id')");
}
println!(
"{} Inferred {} fields from JSON:",
style("rok").green().bold(),
fields.len()
);
for f in &fields {
println!(
" {:<20} → {}",
style(&f.name).cyan(),
style(&f.field_type).yellow()
);
}
if dry_run {
println!("\n{} Dry-run mode — no files written.", style("→").bold());
return Ok(());
}
println!();
crud::crud(
name,
Some(
&fields
.iter()
.map(|f| format!("{}:{}", f.name, f.field_type))
.collect::<Vec<_>>()
.join(","),
),
None, false, false, false, true, force,
)
}
pub fn from_schema(
name: &str,
schema_file: &str,
dry_run: bool,
force: bool,
) -> anyhow::Result<()> {
if !Path::new(schema_file).exists() {
anyhow::bail!("Schema file not found: {schema_file}");
}
let raw = fs::read_to_string(schema_file)?;
let schema: Value =
serde_json::from_str(&raw).map_err(|e| anyhow::anyhow!("Invalid JSON Schema: {e}"))?;
let props = schema
.get("properties")
.and_then(|v| v.as_object())
.ok_or_else(|| anyhow::anyhow!("Schema must have a 'properties' object"))?;
let fields: Vec<FieldDef> = props
.iter()
.filter(|(k, _)| k.as_str() != "id")
.map(|(k, v)| {
let ft = schema_type_to_field_type(v);
FieldDef {
name: k.clone(),
field_type: ft,
optional: false,
}
})
.collect();
if fields.is_empty() {
anyhow::bail!("No fields found in schema properties");
}
let pascal = name.to_upper_camel_case();
println!(
"{} Inferred {} fields from JSON Schema:",
style("rok").green().bold(),
fields.len()
);
for f in &fields {
println!(
" {:<20} → {}",
style(&f.name).cyan(),
style(&f.field_type).yellow()
);
}
if dry_run {
println!("\n{} Dry-run mode — no files written.", style("→").bold());
return Ok(());
}
let _ = pascal;
println!();
crud::crud(
name,
Some(
&fields
.iter()
.map(|f| format!("{}:{}", f.name, f.field_type))
.collect::<Vec<_>>()
.join(","),
),
None,
false,
false,
false,
true,
force,
)
}
fn schema_type_to_field_type(prop: &Value) -> String {
let type_str = prop
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("string");
let format = prop.get("format").and_then(|v| v.as_str()).unwrap_or("");
match (type_str, format) {
("string", "date-time") => "datetime".to_string(),
("string", "date") => "date".to_string(),
("string", "uuid") => "uuid".to_string(),
("string", _) => "string".to_string(),
("integer", _) => "int".to_string(),
("number", _) => "float".to_string(),
("boolean", _) => "bool".to_string(),
("array", _) => "json".to_string(),
("object", _) => "json".to_string(),
_ => "string".to_string(),
}
}