use crate::ctx::GenContext;
#[path = "field_gen.rs"]
mod field_gen;
use field_gen::field_capabilities;
pub use field_gen::{field_modifiers, GROUPS, REGISTRY};
pub type GenFn = for<'a> fn(&mut GenContext<'a>, &mut String);
pub type ParsedSpec<'a> = (&'a str, &'a str, Transform, Option<RangeSpec>, Ordering, Option<u8>);
pub struct Field {
pub id: &'static str,
pub name: &'static str,
pub group: &'static str,
pub description: &'static str,
pub gen: GenFn,
}
impl std::fmt::Debug for Field {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Field")
.field("id", &self.id)
.field("name", &self.name)
.field("group", &self.group)
.field("description", &self.description)
.finish_non_exhaustive()
}
}
impl Field {
pub const fn new(
id: &'static str,
name: &'static str,
group: &'static str,
description: &'static str,
gen: GenFn,
) -> Self {
Self { id, name, group, description, gen }
}
#[inline]
pub fn generate(&self, ctx: &mut GenContext<'_>, buf: &mut String) -> Option<f64> {
ctx.numeric = None;
(self.gen)(ctx, buf);
ctx.numeric
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RangeSpec {
pub from: Option<i64>,
pub to: Option<i64>,
}
const RANGE_FIELDS: &[&str] =
&["integer", "float", "amount", "date", "birthdate", "timestamp", "age", "digits"];
fn parse_range(s: &str) -> Result<RangeSpec, String> {
if let Some(to_str) = s.strip_prefix("..") {
let to = to_str.parse::<i64>().map_err(|_| format!("invalid range bound: '{to_str}'"))?;
Ok(RangeSpec { from: None, to: Some(to) })
} else if let Some(from_str) = s.strip_suffix("..") {
let from =
from_str.parse::<i64>().map_err(|_| format!("invalid range bound: '{from_str}'"))?;
Ok(RangeSpec { from: Some(from), to: None })
} else if let Some((from_str, to_str)) = s.split_once("..") {
let from =
from_str.parse::<i64>().map_err(|_| format!("invalid range bound: '{from_str}'"))?;
let to = to_str.parse::<i64>().map_err(|_| format!("invalid range bound: '{to_str}'"))?;
if from >= to {
return Err(format!("invalid range: {from}..{to}"));
}
Ok(RangeSpec { from: Some(from), to: Some(to) })
} else {
Err(format!("invalid range: '{s}'"))
}
}
fn is_range_segment(s: &str) -> bool {
s.contains("..")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Transform {
None,
Upper,
Lower,
Capitalize,
}
const TRANSFORMS: &[(&str, Transform)] = &[
("upper", Transform::Upper),
("lower", Transform::Lower),
("capitalize", Transform::Capitalize),
];
fn parse_transform(s: &str) -> Option<Transform> {
TRANSFORMS.iter().find(|&&(k, _)| k == s).map(|&(_, t)| t)
}
impl Transform {
pub fn apply(self, s: &str) -> String {
match self {
Transform::None => s.to_string(),
Transform::Upper => s.to_uppercase(),
Transform::Lower => s.to_lowercase(),
Transform::Capitalize => {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => {
let mut out = c.to_uppercase().to_string();
for ch in chars {
out.extend(ch.to_lowercase());
}
out
}
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Ordering {
None,
Asc,
Desc,
}
pub struct ResolvedField {
pub field: &'static Field,
pub modifier: String,
pub transform: Transform,
pub range: Option<RangeSpec>,
pub ordering: Ordering,
pub alias: Option<String>,
pub omit_pct: Option<u8>,
}
impl ResolvedField {
pub fn column_name(&self) -> String {
if let Some(ref a) = self.alias {
return a.clone();
}
self.display_name()
}
pub fn display_name(&self) -> String {
let base = self.field.name.replace('-', "_");
if self.modifier.is_empty() {
base
} else {
format!("{base}_{}", self.modifier)
}
}
pub fn domain_key(&self) -> String {
if self.modifier.is_empty() {
self.field.id.to_string()
} else {
format!("{}_{}", self.field.id, self.modifier)
}
}
}
pub fn lookup(name: &str) -> Option<&'static Field> {
REGISTRY.iter().find(|f| f.name == name)
}
pub fn all_names() -> Vec<&'static str> {
REGISTRY.iter().map(|f| f.name).collect()
}
fn is_group(name: &str) -> bool {
name == "all" || GROUPS.contains(&name)
}
fn expand_group(name: &str) -> Vec<ResolvedField> {
let fields: Vec<&Field> = if name == "all" {
REGISTRY.iter().collect()
} else {
REGISTRY.iter().filter(|f| f.group == name).collect()
};
fields
.into_iter()
.map(|f| ResolvedField {
field: f,
modifier: String::new(),
transform: Transform::None,
range: None,
ordering: Ordering::None,
alias: None,
omit_pct: None,
})
.collect()
}
const LENGTH_FIELDS: &[&str] = &["digits", "letters", "alnum", "base64", "hex", "password"];
fn validate_modifier(field: &Field, m: &str) -> Result<(), String> {
if m.is_empty() {
return Ok(());
}
if !m.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
return Err(format!("modifier '{m}' must contain only a-z, 0-9 and -"));
}
if LENGTH_FIELDS.contains(&field.name) && m.parse::<usize>().is_ok() {
return Ok(());
}
let known = field_modifiers(field.id);
if !known.is_empty() {
let valid: Vec<&str> = known.split(", ").collect();
if !valid.contains(&m) {
return Err(format!("unknown modifier '{}:{m}'; available: {known}", field.name));
}
} else if parse_transform(m).is_none() {
return Err(format!(
"field '{}' has no modifiers; did you mean a transform? available: upper, lower, capitalize",
field.name
));
}
Ok(())
}
fn parse_ordering(s: &str) -> Option<Ordering> {
match s {
"asc" => Some(Ordering::Asc),
"desc" => Some(Ordering::Desc),
_ => None,
}
}
pub fn parse_field_spec(token: &str) -> Result<ParsedSpec<'_>, String> {
let mut parts = token.splitn(7, ':');
let name = parts.next().unwrap_or("");
let mut modifier: Option<&str> = None;
let mut transform = Transform::None;
let mut range: Option<RangeSpec> = None;
let mut ordering = Ordering::None;
let mut omit_pct: Option<u8> = None;
for seg in parts {
if let Some(pct) = parse_omit_pct(seg) {
if omit_pct.is_some() {
return Err("duplicate omit in field descriptor".into());
}
omit_pct = Some(pct);
} else if is_range_segment(seg) {
if range.is_some() {
return Err("duplicate range in field descriptor".into());
}
range = Some(parse_range(seg)?);
} else if let Some(t) = parse_transform(seg) {
if transform != Transform::None {
return Err("duplicate transform in field descriptor".into());
}
transform = t;
} else if let Some(o) = parse_ordering(seg) {
if ordering != Ordering::None {
return Err("duplicate ordering in field descriptor".into());
}
ordering = o;
} else {
if modifier.is_some() {
return Err("duplicate modifier in field descriptor".into());
}
modifier = Some(seg);
}
}
Ok((name, modifier.unwrap_or(""), transform, range, ordering, omit_pct))
}
fn parse_omit_pct(s: &str) -> Option<u8> {
let rest = s.strip_prefix("omit=")?;
let n: u8 = rest.parse().ok()?;
if n > 100 {
return None;
}
Some(n)
}
fn validate_range(field: &Field, range: &Option<RangeSpec>) -> Result<(), String> {
if let Some(r) = range {
if !RANGE_FIELDS.contains(&field.name) {
return Err(format!("field '{}' does not support range", field.name));
}
if let (Some(from), Some(to)) = (r.from, r.to) {
if from >= to {
return Err(format!("invalid range: {from}..{to}"));
}
}
}
Ok(())
}
pub fn resolve_range(
range: &Option<RangeSpec>,
field_name: &str,
since: i64,
until: i64,
) -> Option<(i64, i64)> {
let r = range.as_ref()?;
let is_date = matches!(field_name, "date" | "birthdate" | "timestamp");
let (default_min, default_max) = if is_date { (since, until) } else { (0, 999_999) };
let from = r.from.unwrap_or(default_min);
let to = r.to.unwrap_or(default_max);
if is_date {
let from_e = if from > 0 && from <= 9999 {
crate::temporal::parse(&from.to_string()).unwrap_or(from)
} else {
from
};
let to_e = if to > 0 && to <= 9999 {
crate::temporal::parse_until(&to.to_string()).unwrap_or(to)
} else {
to
};
Some((from_e, to_e))
} else {
Some((from, to))
}
}
pub fn resolve(tokens: &[String]) -> Result<Vec<ResolvedField>, String> {
let mut result = Vec::new();
for token in tokens {
let (alias, spec) = if let Some(eq_pos) = token.find('=') {
let colon_pos = token.find(':').unwrap_or(token.len());
if eq_pos < colon_pos {
let (a, s) = token.split_at(eq_pos);
(Some(a.to_string()), &s[1..])
} else {
(None, token.as_str())
}
} else {
(None, token.as_str())
};
let (name, modifier, transform, range, ordering, omit_pct) = parse_field_spec(spec)?;
if let Some(field) = lookup(name) {
if name == "enum" {
super::gen::validate_enum(modifier)?;
} else {
validate_modifier(field, modifier)?;
validate_range(field, &range)?;
}
result.push(ResolvedField {
field,
modifier: modifier.to_string(),
transform,
range,
ordering,
alias,
omit_pct,
});
} else if is_group(name) {
if alias.is_some() {
return Err(format!("alias not supported on groups: '{token}'"));
}
if !modifier.is_empty() || transform != Transform::None {
return Err(format!("modifiers and transforms not supported on groups: '{token}'"));
}
result.extend(expand_group(name));
} else {
return Err(format!("unknown field or group '{name}'; run 'seedfaker --list'"));
}
}
if result.is_empty() {
return Err("no fields specified".into());
}
Ok(result)
}
pub fn print_list() {
for group in GROUPS {
println!("\n {group}:");
for f in REGISTRY.iter().filter(|f| f.group == *group) {
let caps = field_capabilities(f.id);
if caps.is_empty() {
println!(" {:<24} {}", f.name, f.description);
} else {
println!(" {:<24} {} {{{}}}", f.name, f.description, caps);
}
}
}
println!();
println!("All fields support :upper, :lower, and :capitalize transforms.");
}
pub fn print_list_json() {
use std::io::Write;
let stdout = std::io::stdout();
let mut out = stdout.lock();
let _ = out.write_all(b"[");
let mut first = true;
for f in REGISTRY {
if !first {
let _ = out.write_all(b",");
}
first = false;
let mods = field_modifiers(f.id);
let mods_arr: Vec<&str> =
if mods.is_empty() { Vec::new() } else { mods.split(", ").map(str::trim).collect() };
let _ = out.write_all(b"{\"id\":");
write_json_string(&mut out, f.id);
let _ = out.write_all(b",\"name\":");
write_json_string(&mut out, f.name);
let _ = out.write_all(b",\"group\":");
write_json_string(&mut out, f.group);
let _ = out.write_all(b",\"description\":");
write_json_string(&mut out, f.description);
let _ = out.write_all(b",\"modifiers\":[");
for (i, m) in mods_arr.iter().enumerate() {
if i > 0 {
let _ = out.write_all(b",");
}
write_json_string(&mut out, m);
}
let _ = out.write_all(b"]}");
}
let _ = out.write_all(b"]\n");
}
fn write_json_string(out: &mut impl std::io::Write, s: &str) {
let _ = out.write_all(b"\"");
for ch in s.chars() {
match ch {
'"' => {
let _ = out.write_all(b"\\\"");
}
'\\' => {
let _ = out.write_all(b"\\\\");
}
'\n' => {
let _ = out.write_all(b"\\n");
}
'\r' => {
let _ = out.write_all(b"\\r");
}
'\t' => {
let _ = out.write_all(b"\\t");
}
c if c < '\x20' => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => {
let mut buf = [0u8; 4];
let _ = out.write_all(c.encode_utf8(&mut buf).as_bytes());
}
}
}
let _ = out.write_all(b"\"");
}
pub fn print_field_help(name: &str) {
let Some(field) = lookup(name) else {
eprintln!("Unknown field: \"{name}\"");
let suggestions: Vec<&str> = all_names()
.into_iter()
.filter(|n| n.contains(name) || name.contains(*n))
.take(5)
.collect();
if !suggestions.is_empty() {
eprintln!("\nDid you mean?");
for s in &suggestions {
if let Some(f) = lookup(s) {
eprintln!(" {:<24} {}", f.name, f.description);
}
}
}
eprintln!("\nSee all: seedfaker --list");
return;
};
println!("{} ({})", field.name, field.group);
println!();
println!(" {}", field.description);
let mods = field_modifiers(field.id);
if !mods.is_empty() {
println!();
println!(" Modifiers: {mods}");
}
println!();
println!(" Usage:");
println!(" seedfaker {}", field.name);
println!(" seedfaker {} -n 1000 --seed prod", field.name);
if !mods.is_empty() {
let first_mod = mods.split(", ").next().unwrap_or("");
println!(" seedfaker {}:{}", field.name, first_mod);
}
let related: Vec<&str> = REGISTRY
.iter()
.filter(|f| f.group == field.group && f.name != field.name)
.map(|f| f.name)
.take(6)
.collect();
if !related.is_empty() {
println!();
println!(" Related: {}", related.join(", "));
}
}
pub fn print_fields_table() {
for group in GROUPS {
let fields: Vec<&str> =
REGISTRY.iter().filter(|f| f.group == *group).map(|f| f.name).collect();
let count = fields.len();
println!("| `{}` | {} | {} |", group, count, fields.join(", "));
}
}