use crate::faker::EnhancedFaker;
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub name: String,
pub field_type: String,
pub required: bool,
pub default: Option<Value>,
pub constraints: HashMap<String, Value>,
pub faker_template: Option<String>,
pub description: Option<String>,
}
impl FieldDefinition {
pub fn new(name: String, field_type: String) -> Self {
Self {
name,
field_type,
required: true,
default: None,
constraints: HashMap::new(),
faker_template: None,
description: None,
}
}
pub fn optional(mut self) -> Self {
self.required = false;
self
}
pub fn with_default(mut self, default: Value) -> Self {
self.default = Some(default);
self
}
pub fn with_constraint(mut self, key: String, value: Value) -> Self {
self.constraints.insert(key, value);
self
}
pub fn with_faker_template(mut self, template: String) -> Self {
self.faker_template = Some(template);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn generate_value(&self, faker: &mut EnhancedFaker) -> Value {
if let Some(template) = &self.faker_template {
return faker.generate_by_type(template);
}
if !self.required {
if let Some(default) = &self.default {
return default.clone();
}
}
faker.generate_by_type(&self.field_type)
}
pub fn validate_value(&self, value: &Value) -> Result<()> {
if self.required && value.is_null() {
return Err(Error::generic(format!("Required field '{}' is null", self.name)));
}
let expected_type = self
.constraints
.get("type")
.and_then(|v| v.as_str())
.unwrap_or(&self.field_type);
let actual_type = match value {
Value::String(_) => "string",
Value::Number(num) => {
if num.is_i64() || num.is_u64() {
"integer"
} else {
"number"
}
}
Value::Bool(_) => "boolean",
Value::Object(_) => "object",
Value::Array(_) => "array",
Value::Null => "null",
};
let normalized_expected = match expected_type {
"int" | "integer" => "integer",
"float" | "number" => "number",
other => other,
};
if normalized_expected != actual_type
&& !(normalized_expected == "number" && actual_type == "integer")
&& !(normalized_expected == "float" && actual_type == "number")
&& !(normalized_expected == "integer" && actual_type == "integer")
&& !(normalized_expected == "int" && actual_type == "integer")
&& !(normalized_expected == "uuid" && actual_type == "string")
&& !(normalized_expected == "email" && actual_type == "string")
&& !(normalized_expected == "name" && actual_type == "string")
&& !(normalized_expected == "address" && actual_type == "string")
&& !(normalized_expected == "phone" && actual_type == "string")
&& !(normalized_expected == "company" && actual_type == "string")
&& !(normalized_expected == "url" && actual_type == "string")
&& !(normalized_expected == "ip" && actual_type == "string")
&& !(normalized_expected == "color" && actual_type == "string")
&& !(normalized_expected == "date" && actual_type == "string")
&& !(normalized_expected == "datetime" && actual_type == "string")
&& !(normalized_expected == "paragraph" && actual_type == "string")
{
return Err(Error::generic(format!(
"Field '{}' type mismatch: expected {}, got {}",
self.name, normalized_expected, actual_type
)));
}
if let Value::Number(num) = value {
if let Some(min_val) = self.constraints.get("minimum") {
if let Some(min_num) = min_val.as_f64() {
if num.as_f64().unwrap_or(0.0) < min_num {
return Err(Error::generic(format!(
"Field '{}' value {} is less than minimum {}",
self.name, num, min_num
)));
}
}
}
if let Some(max_val) = self.constraints.get("maximum") {
if let Some(max_num) = max_val.as_f64() {
if num.as_f64().unwrap_or(0.0) > max_num {
return Err(Error::generic(format!(
"Field '{}' value {} is greater than maximum {}",
self.name, num, max_num
)));
}
}
}
}
if let Value::String(s) = value {
if let Some(min_len) = self.constraints.get("minLength") {
if let Some(min_len_num) = min_len.as_u64() {
if s.len() < min_len_num as usize {
return Err(Error::generic(format!(
"Field '{}' length {} is less than minimum {}",
self.name,
s.len(),
min_len_num
)));
}
}
}
if let Some(max_len) = self.constraints.get("maxLength") {
if let Some(max_len_num) = max_len.as_u64() {
if s.len() > max_len_num as usize {
return Err(Error::generic(format!(
"Field '{}' length {} is greater than maximum {}",
self.name,
s.len(),
max_len_num
)));
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDefinition {
pub name: String,
pub description: Option<String>,
pub fields: Vec<FieldDefinition>,
pub relationships: HashMap<String, Relationship>,
pub metadata: HashMap<String, Value>,
}
impl SchemaDefinition {
pub fn new(name: String) -> Self {
Self {
name,
description: None,
fields: Vec::new(),
relationships: HashMap::new(),
metadata: HashMap::new(),
}
}
pub fn with_field(mut self, field: FieldDefinition) -> Self {
self.fields.push(field);
self
}
pub fn with_fields(mut self, fields: Vec<FieldDefinition>) -> Self {
self.fields.extend(fields);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn with_relationship(mut self, name: String, relationship: Relationship) -> Self {
self.relationships.insert(name, relationship);
self
}
pub fn with_metadata(mut self, key: String, value: Value) -> Self {
self.metadata.insert(key, value);
self
}
pub fn generate_row(&self, faker: &mut EnhancedFaker) -> Result<Value> {
let mut row = serde_json::Map::new();
for field in &self.fields {
let value = field.generate_value(faker);
field.validate_value(&value)?;
row.insert(field.name.clone(), value);
}
Ok(Value::Object(row))
}
pub fn get_field(&self, name: &str) -> Option<&FieldDefinition> {
self.fields.iter().find(|field| field.name == name)
}
pub fn from_json_schema(json_schema: &Value) -> Result<Self> {
let title = json_schema
.get("title")
.and_then(|v| v.as_str())
.unwrap_or("GeneratedSchema")
.to_string();
let description =
json_schema.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
let mut schema = Self::new(title);
if let Some(desc) = description {
schema = schema.with_description(desc);
}
if let Some(properties) = json_schema.get("properties") {
if let Some(props_obj) = properties.as_object() {
for (name, prop_def) in props_obj {
let field_type = extract_type_from_json_schema(prop_def);
let mut field = FieldDefinition::new(name.clone(), field_type);
if let Some(required) = json_schema.get("required") {
if let Some(required_arr) = required.as_array() {
let is_required = required_arr.iter().any(|v| v.as_str() == Some(name));
if !is_required {
field = field.optional();
}
}
}
if let Some(desc) = prop_def.get("description").and_then(|v| v.as_str()) {
field = field.with_description(desc.to_string());
}
if let Some(minimum) = prop_def.get("minimum") {
field = field.with_constraint("minimum".to_string(), minimum.clone());
}
if let Some(maximum) = prop_def.get("maximum") {
field = field.with_constraint("maximum".to_string(), maximum.clone());
}
if let Some(min_length) = prop_def.get("minLength") {
field = field.with_constraint("minLength".to_string(), min_length.clone());
}
if let Some(max_length) = prop_def.get("maxLength") {
field = field.with_constraint("maxLength".to_string(), max_length.clone());
}
if let Some(enum_vals) = prop_def.get("enum") {
if let Some(_enum_arr) = enum_vals.as_array() {
field = field.with_constraint("enum".to_string(), enum_vals.clone());
}
}
if field.field_type == "array" {
if let Some(items) = prop_def.get("items") {
if items.is_object() {
field =
field.with_constraint("itemsSchema".to_string(), items.clone());
if let Some(items_type) = items.get("type") {
if let Some(items_type_str) = items_type.as_str() {
field = field.with_constraint(
"itemsType".to_string(),
json!(items_type_str),
);
}
}
} else if let Some(items_type) = items.as_str() {
field = field
.with_constraint("itemsType".to_string(), json!(items_type));
}
}
}
if field.field_type == "object" {
if let Some(properties) = prop_def.get("properties") {
field =
field.with_constraint("properties".to_string(), properties.clone());
if let Some(required) = prop_def.get("required") {
field =
field.with_constraint("required".to_string(), required.clone());
}
}
}
if let Some(min_items) = prop_def.get("minItems") {
field = field.with_constraint("minItems".to_string(), min_items.clone());
}
if let Some(max_items) = prop_def.get("maxItems") {
field = field.with_constraint("maxItems".to_string(), max_items.clone());
}
schema = schema.with_field(field);
}
}
}
Ok(schema)
}
pub fn from_openapi_spec(openapi_spec: &Value) -> Result<Self> {
if !openapi_spec.is_object() {
return Err(Error::generic("OpenAPI spec must be a JSON object"));
}
let spec = openapi_spec.as_object().unwrap();
let title = spec
.get("info")
.and_then(|info| info.get("title"))
.and_then(|title| title.as_str())
.unwrap_or("OpenAPI Generated Schema")
.to_string();
let description = spec
.get("info")
.and_then(|info| info.get("description"))
.and_then(|desc| desc.as_str())
.map(|s| s.to_string());
let mut schema = Self::new(title);
if let Some(desc) = description {
schema = schema.with_description(desc);
}
if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
for (path, path_item) in paths {
if let Some(path_obj) = path_item.as_object() {
for (method, operation) in path_obj {
if let Some(op_obj) = operation.as_object() {
if let Some(request_body) = op_obj.get("requestBody") {
if let Some(rb_obj) = request_body.as_object() {
if let Some(content) = rb_obj.get("content") {
if let Some(json_content) = content.get("application/json")
{
if let Some(schema_obj) = json_content.get("schema") {
let field_name = format!(
"{}_{}_request",
path.replace("/", "_").trim_start_matches("_"),
method
);
if let Some(field) =
Self::create_field_from_openapi_schema(
&field_name,
schema_obj,
)
{
schema = schema.with_field(field);
}
}
}
}
}
}
if let Some(responses) = op_obj.get("responses") {
if let Some(resp_obj) = responses.as_object() {
for (status_code, response) in resp_obj {
if status_code == "200"
|| status_code == "201"
|| status_code.starts_with("2")
{
if let Some(resp_obj) = response.as_object() {
if let Some(content) = resp_obj.get("content") {
if let Some(json_content) =
content.get("application/json")
{
if let Some(schema_obj) =
json_content.get("schema")
{
let field_name = format!(
"{}_{}_response_{}",
path.replace("/", "_")
.trim_start_matches("_"),
method,
status_code
);
if let Some(field) = Self::create_field_from_openapi_schema(&field_name, schema_obj) {
schema = schema.with_field(field);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
if let Some(components) = spec.get("components") {
if let Some(comp_obj) = components.as_object() {
if let Some(schemas) = comp_obj.get("schemas") {
if let Some(schema_obj) = schemas.as_object() {
for (name, schema_def) in schema_obj {
if let Some(field) =
Self::create_field_from_openapi_schema(name, schema_def)
{
schema = schema.with_field(field);
}
}
}
}
}
}
Ok(schema)
}
fn create_field_from_openapi_schema(name: &str, schema: &Value) -> Option<FieldDefinition> {
if !schema.is_object() {
return None;
}
let schema_obj = schema.as_object().unwrap();
let field_type = if let Some(type_val) = schema_obj.get("type") {
if let Some(type_str) = type_val.as_str() {
match type_str {
"string" => "string".to_string(),
"number" => "float".to_string(),
"integer" => "int".to_string(),
"boolean" => "boolean".to_string(),
"object" => "object".to_string(),
"array" => "array".to_string(),
_ => "string".to_string(),
}
} else {
"string".to_string()
}
} else {
"string".to_string()
};
let mut field = FieldDefinition::new(name.to_string(), field_type);
if let Some(desc) = schema_obj.get("description").and_then(|d| d.as_str()) {
field = field.with_description(desc.to_string());
}
if let Some(required) = schema_obj.get("required") {
if let Some(required_arr) = required.as_array() {
if !required_arr.iter().any(|v| v.as_str() == Some(name)) {
field = field.optional();
}
}
}
if let Some(minimum) = schema_obj.get("minimum") {
field = field.with_constraint("minimum".to_string(), minimum.clone());
}
if let Some(maximum) = schema_obj.get("maximum") {
field = field.with_constraint("maximum".to_string(), maximum.clone());
}
if let Some(min_length) = schema_obj.get("minLength") {
field = field.with_constraint("minLength".to_string(), min_length.clone());
}
if let Some(max_length) = schema_obj.get("maxLength") {
field = field.with_constraint("maxLength".to_string(), max_length.clone());
}
if let Some(enum_vals) = schema_obj.get("enum") {
if let Some(_enum_arr) = enum_vals.as_array() {
field = field.with_constraint("enum".to_string(), enum_vals.clone());
}
}
Some(field)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relationship {
pub target_schema: String,
pub relationship_type: RelationshipType,
pub foreign_key: String,
pub required: bool,
}
impl Relationship {
pub fn new(
target_schema: String,
relationship_type: RelationshipType,
foreign_key: String,
) -> Self {
Self {
target_schema,
relationship_type,
foreign_key,
required: true,
}
}
pub fn optional(mut self) -> Self {
self.required = false;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RelationshipType {
OneToOne,
OneToMany,
ManyToOne,
ManyToMany,
}
fn extract_type_from_json_schema(prop_def: &Value) -> String {
if let Some(type_val) = prop_def.get("type") {
if let Some(type_str) = type_val.as_str() {
return match type_str {
"string" => "string".to_string(),
"number" => "float".to_string(),
"integer" => "int".to_string(),
"boolean" => "boolean".to_string(),
"object" => "object".to_string(),
"array" => "array".to_string(),
"null" => "null".to_string(),
_ => "string".to_string(),
};
}
}
"string".to_string()
}
pub mod templates {
use super::*;
pub fn user_schema() -> SchemaDefinition {
SchemaDefinition::new("User".to_string())
.with_description("User account information".to_string())
.with_fields(vec![
FieldDefinition::new("id".to_string(), "uuid".to_string()),
FieldDefinition::new("email".to_string(), "email".to_string()),
FieldDefinition::new("name".to_string(), "name".to_string()),
FieldDefinition::new("created_at".to_string(), "date".to_string()),
FieldDefinition::new("active".to_string(), "boolean".to_string()),
])
}
pub fn product_schema() -> SchemaDefinition {
SchemaDefinition::new("Product".to_string())
.with_description("Product catalog item".to_string())
.with_fields(vec![
FieldDefinition::new("id".to_string(), "uuid".to_string()),
FieldDefinition::new("name".to_string(), "string".to_string()),
FieldDefinition::new("description".to_string(), "paragraph".to_string()),
FieldDefinition::new("price".to_string(), "float".to_string())
.with_constraint("minimum".to_string(), Value::Number(0.into())),
FieldDefinition::new("category".to_string(), "string".to_string()),
FieldDefinition::new("in_stock".to_string(), "boolean".to_string()),
])
}
pub fn order_schema() -> SchemaDefinition {
SchemaDefinition::new("Order".to_string())
.with_description("Customer order".to_string())
.with_fields(vec![
FieldDefinition::new("id".to_string(), "uuid".to_string()),
FieldDefinition::new("user_id".to_string(), "uuid".to_string()),
FieldDefinition::new("total_amount".to_string(), "float".to_string())
.with_constraint("minimum".to_string(), Value::Number(0.into())),
FieldDefinition::new("status".to_string(), "string".to_string()),
FieldDefinition::new("created_at".to_string(), "date".to_string()),
])
.with_relationship(
"user".to_string(),
Relationship::new(
"User".to_string(),
RelationshipType::ManyToOne,
"user_id".to_string(),
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_definition_new() {
let field = FieldDefinition::new("test".to_string(), "string".to_string());
assert_eq!(field.name, "test");
assert_eq!(field.field_type, "string");
assert!(field.required);
assert!(field.default.is_none());
}
#[test]
fn test_field_definition_optional() {
let field = FieldDefinition::new("test".to_string(), "string".to_string()).optional();
assert!(!field.required);
}
#[test]
fn test_field_definition_with_default() {
let field = FieldDefinition::new("test".to_string(), "string".to_string())
.with_default(Value::String("default".to_string()));
assert_eq!(field.default, Some(Value::String("default".to_string())));
}
#[test]
fn test_field_definition_with_constraint() {
let field = FieldDefinition::new("age".to_string(), "int".to_string())
.with_constraint("minimum".to_string(), Value::Number(0.into()));
assert!(field.constraints.contains_key("minimum"));
}
#[test]
fn test_field_definition_with_faker_template() {
let field = FieldDefinition::new("email".to_string(), "string".to_string())
.with_faker_template("email".to_string());
assert_eq!(field.faker_template, Some("email".to_string()));
}
#[test]
fn test_field_definition_with_description() {
let field = FieldDefinition::new("test".to_string(), "string".to_string())
.with_description("Test field".to_string());
assert_eq!(field.description, Some("Test field".to_string()));
}
#[test]
fn test_schema_definition_new() {
let schema = SchemaDefinition::new("TestSchema".to_string());
assert_eq!(schema.name, "TestSchema");
assert!(schema.description.is_none());
assert_eq!(schema.fields.len(), 0);
}
#[test]
fn test_schema_definition_with_field() {
let field = FieldDefinition::new("id".to_string(), "uuid".to_string());
let schema = SchemaDefinition::new("Test".to_string()).with_field(field);
assert_eq!(schema.fields.len(), 1);
assert_eq!(schema.fields[0].name, "id");
}
#[test]
fn test_schema_definition_with_fields() {
let fields = vec![
FieldDefinition::new("id".to_string(), "uuid".to_string()),
FieldDefinition::new("name".to_string(), "string".to_string()),
];
let schema = SchemaDefinition::new("Test".to_string()).with_fields(fields);
assert_eq!(schema.fields.len(), 2);
}
#[test]
fn test_schema_definition_with_description() {
let schema =
SchemaDefinition::new("Test".to_string()).with_description("Test schema".to_string());
assert_eq!(schema.description, Some("Test schema".to_string()));
}
#[test]
fn test_schema_definition_with_metadata() {
let schema = SchemaDefinition::new("Test".to_string())
.with_metadata("version".to_string(), Value::String("1.0".to_string()));
assert!(schema.metadata.contains_key("version"));
}
#[test]
fn test_schema_definition_get_field() {
let field = FieldDefinition::new("email".to_string(), "email".to_string());
let schema = SchemaDefinition::new("Test".to_string()).with_field(field);
assert!(schema.get_field("email").is_some());
assert!(schema.get_field("unknown").is_none());
}
#[test]
fn test_relationship_new() {
let rel = Relationship::new(
"User".to_string(),
RelationshipType::ManyToOne,
"user_id".to_string(),
);
assert_eq!(rel.target_schema, "User");
assert_eq!(rel.foreign_key, "user_id");
assert!(rel.required);
}
#[test]
fn test_relationship_optional() {
let rel = Relationship::new(
"User".to_string(),
RelationshipType::ManyToOne,
"user_id".to_string(),
)
.optional();
assert!(!rel.required);
}
#[test]
fn test_relationship_types() {
let one_to_one = RelationshipType::OneToOne;
let one_to_many = RelationshipType::OneToMany;
let many_to_one = RelationshipType::ManyToOne;
let many_to_many = RelationshipType::ManyToMany;
assert!(matches!(one_to_one, RelationshipType::OneToOne));
assert!(matches!(one_to_many, RelationshipType::OneToMany));
assert!(matches!(many_to_one, RelationshipType::ManyToOne));
assert!(matches!(many_to_many, RelationshipType::ManyToMany));
}
#[test]
fn test_extract_type_from_json_schema_string() {
let prop = serde_json::json!({"type": "string"});
let type_str = extract_type_from_json_schema(&prop);
assert_eq!(type_str, "string");
}
#[test]
fn test_extract_type_from_json_schema_number() {
let prop = serde_json::json!({"type": "number"});
let type_str = extract_type_from_json_schema(&prop);
assert_eq!(type_str, "float");
}
#[test]
fn test_extract_type_from_json_schema_integer() {
let prop = serde_json::json!({"type": "integer"});
let type_str = extract_type_from_json_schema(&prop);
assert_eq!(type_str, "int");
}
#[test]
fn test_extract_type_from_json_schema_boolean() {
let prop = serde_json::json!({"type": "boolean"});
let type_str = extract_type_from_json_schema(&prop);
assert_eq!(type_str, "boolean");
}
#[test]
fn test_extract_type_from_json_schema_default() {
let prop = serde_json::json!({});
let type_str = extract_type_from_json_schema(&prop);
assert_eq!(type_str, "string");
}
#[test]
fn test_user_schema_template() {
let schema = templates::user_schema();
assert_eq!(schema.name, "User");
assert_eq!(schema.fields.len(), 5);
assert!(schema.get_field("id").is_some());
assert!(schema.get_field("email").is_some());
}
#[test]
fn test_product_schema_template() {
let schema = templates::product_schema();
assert_eq!(schema.name, "Product");
assert!(schema.get_field("price").is_some());
}
#[test]
fn test_order_schema_template() {
let schema = templates::order_schema();
assert_eq!(schema.name, "Order");
assert_eq!(schema.relationships.len(), 1);
assert!(schema.relationships.contains_key("user"));
}
}