pub use DatabaseConfig as Config;
use crate::error::{Error, Result};
use crate::model::dependency::DependencyManager;
use crate::model::query::{Parameter, Query, QueryLanguage};
use crate::parser::GoldFile;
use crate::parser::ast::{Block, QueryBlock, SchemaBlock};
use crate::schema::{
EmbeddingConfig, EmbeddingParadigm, Field, Schema, SchemaFormat, Type, parsers,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Project {
pub metadata: ProjectMetadata,
pub database: DatabaseConfig,
pub server: Option<ServerConfig>,
pub scaffold: Option<ScaffoldConfig>,
pub schemas: HashMap<String, Schema>,
pub queries: HashMap<String, Query>,
pub endpoints: Vec<Endpoint>,
pub tests: Vec<Test>,
pub docs: Option<DocsConfig>,
pub deployment: Option<DeploymentConfig>,
pub dependencies: DependencyManager,
}
impl Project {
pub fn new(name: String) -> Self {
Self {
metadata: ProjectMetadata {
name,
version: "0.1.0".to_string(),
authors: Vec::new(),
description: None,
},
database: DatabaseConfig::default(),
server: None,
scaffold: None,
schemas: HashMap::new(),
queries: HashMap::new(),
endpoints: Vec::new(),
tests: Vec::new(),
docs: None,
deployment: None,
dependencies: DependencyManager::new(),
}
}
pub fn from_gold_files(files: Vec<GoldFile>) -> Result<Self> {
let mut project = Project::new("unnamed".to_string());
for file in &files {
for block in &file.blocks {
match block {
Block::Schema(schema) => {
project.add_schema_from_ast(schema)?;
}
Block::Query(query) => {
project.add_query_from_ast(query)?;
}
Block::Config(config) => {
project.merge_config(config)?;
}
Block::Endpoint(endpoint) => {
project.add_endpoint_from_ast(endpoint)?;
}
Block::Data(_) => {
}
Block::Custom(_) => {
}
}
}
}
Ok(project)
}
fn add_schema_from_ast(&mut self, schema_block: &SchemaBlock) -> Result<()> {
let format = if let Some(ref fmt) = schema_block.format {
SchemaFormat::from_str(fmt)
} else {
SchemaFormat::Native
};
let schema = match format {
SchemaFormat::JsonSchema => {
if let Some(ref content) = schema_block.content {
parsers::parse_json_schema(&schema_block.name, content)?
} else {
return Err(Error::Schema {
schema_name: schema_block.name.clone(),
message: "JSON Schema format requires embedded content".to_string(),
});
}
}
SchemaFormat::TypeScript => {
if let Some(ref content) = schema_block.content {
parsers::parse_typescript(&schema_block.name, content)?
} else {
return Err(Error::Schema {
schema_name: schema_block.name.clone(),
message: "TypeScript format requires embedded content".to_string(),
});
}
}
SchemaFormat::Native | SchemaFormat::Rust => {
let mut schema = Schema::new(schema_block.name.clone(), format);
schema.crud = schema_block.crud;
for field in &schema_block.fields {
let field_type = Type::from_str(&field.field_type)?;
let mut model_field = Field::new(field.name.clone(), field_type);
model_field.nullable = field.nullable;
if let Some(ref embedding_ann) = field.embedding_annotation {
let paradigm = embedding_ann
.paradigm
.as_ref()
.and_then(|p| EmbeddingParadigm::from_str(p));
let embedding_config = if let Some(dim) = embedding_ann.dimension {
EmbeddingConfig::with_dimension(
embedding_ann.model.clone(),
embedding_ann.source_field.clone(),
dim,
)
} else {
EmbeddingConfig::new(
embedding_ann.model.clone(),
embedding_ann.source_field.clone(),
)
};
let embedding_config = if let Some(paradigm) = paradigm {
embedding_config.with_paradigm(paradigm)
} else {
embedding_config
};
embedding_config.validate()?;
let source_exists = schema_block
.fields
.iter()
.any(|f| f.name == embedding_ann.source_field);
if !source_exists {
return Err(Error::Validation {
message: format!(
"Embedding source_field '{}' not found in schema '{}'",
embedding_ann.source_field, schema_block.name
),
context: Some(format!("Field '{}'", field.name)),
});
}
model_field.embedding_config = Some(embedding_config);
}
schema.add_field(model_field);
}
schema
}
};
if self.schemas.contains_key(&schema.name) {
return Err(Error::Validation {
message: format!("Duplicate schema name '{}'", schema.name),
context: Some("Project::from_gold_files".to_string()),
});
}
self.schemas.insert(schema.name.clone(), schema);
Ok(())
}
fn add_query_from_ast(&mut self, query_block: &QueryBlock) -> Result<()> {
let query = Query {
name: query_block.name.clone(),
params: query_block
.params
.iter()
.map(|p| Parameter {
name: p.name.clone(),
param_type: Type::from_str(&p.param_type)
.unwrap_or(Type::Custom(p.param_type.clone())),
})
.collect(),
return_type: Type::from_str(&query_block.return_type)
.unwrap_or(Type::Custom(query_block.return_type.clone())),
language: QueryLanguage::from_str(&query_block.language),
source: query_block.source.clone(),
doc_comment: None,
};
if self.queries.contains_key(&query.name) {
return Err(Error::Validation {
message: format!("Duplicate query name '{}'", query.name),
context: Some("Project::from_gold_files".to_string()),
});
}
self.queries.insert(query.name.clone(), query);
Ok(())
}
fn merge_config(&mut self, config: &crate::parser::ast::ConfigBlock) -> Result<()> {
match config.name.as_str() {
"deployment" => {
let deployment_config = parse_deployment_config(config)?;
self.deployment = Some(deployment_config);
}
"database" => {
for (key, value) in &config.attributes {
match key.as_str() {
"engine" => {
if let Some(s) = value.as_string() {
self.database.engine = s.to_string();
}
}
"path" => {
if let Some(s) = value.as_string() {
self.database.path = s.to_string();
}
}
_ => {}
}
}
}
"server" => {
let mut port = 8080;
let mut host = "0.0.0.0".to_string();
let mut framework = "axum".to_string();
for (key, value) in &config.attributes {
match key.as_str() {
"port" => {
if let Some(i) = value.as_int() {
port = i as u16;
}
}
"host" => {
if let Some(s) = value.as_string() {
host = s.to_string();
}
}
"framework" => {
if let Some(s) = value.as_string() {
framework = s.to_string();
}
}
_ => {}
}
}
self.server = Some(ServerConfig {
framework,
port,
host,
middleware: Vec::new(),
});
}
"project" => {
for (key, value) in &config.attributes {
match key.as_str() {
"name" => {
if let Some(s) = value.as_string() {
self.metadata.name = s.to_string();
}
}
"version" => {
if let Some(s) = value.as_string() {
self.metadata.version = s.to_string();
}
}
"description" => {
if let Some(s) = value.as_string() {
self.metadata.description = Some(s.to_string());
}
}
_ => {}
}
}
}
_ => {
}
}
Ok(())
}
fn add_endpoint_from_ast(
&mut self,
endpoint: &crate::parser::ast::EndpointBlock,
) -> Result<()> {
let auth = if endpoint.auth {
AuthRequirement::Required
} else {
AuthRequirement::None
};
let model_endpoint = Endpoint {
method: endpoint.method.clone(),
path: endpoint.path.clone(),
query: Some(endpoint.query.clone()),
auth,
roles: Vec::new(),
rate_limit: None,
validation: HashMap::new(),
doc_comment: None,
crud_schema: None,
};
self.endpoints.push(model_endpoint);
Ok(())
}
pub fn add_dependency(&mut self, dep: crate::model::dependency::DependencySpec) {
self.dependencies.add(dep);
}
pub fn remove_dependency(
&mut self,
name: &str,
) -> Option<crate::model::dependency::DependencySpec> {
self.dependencies.remove(name)
}
pub fn list_dependencies(&self) -> Vec<&str> {
self.dependencies.list()
}
pub fn get_schema(&self, name: &str) -> Option<&Schema> {
self.schemas.get(name)
}
pub fn get_query(&self, name: &str) -> Option<&Query> {
self.queries.get(name)
}
pub fn validate(&self) -> Result<()> {
for schema in self.schemas.values() {
schema.validate()?;
}
for query in self.queries.values() {
let schema_names = extract_schema_names_from_type(&query.return_type);
for base_type in schema_names {
if !is_primitive_type(&base_type) && !self.schemas.contains_key(&base_type) {
return Err(Error::Validation {
message: format!(
"Query '{}' return type references undefined schema '{}'",
query.name, base_type
),
context: Some("Project validation".to_string()),
});
}
}
}
for endpoint in &self.endpoints {
if let Some(ref query_name) = endpoint.query {
if !self.queries.contains_key(query_name) {
return Err(Error::Validation {
message: format!(
"Endpoint '{}' references undefined query '{}'",
endpoint.path, query_name
),
context: Some("Project validation".to_string()),
});
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ProjectMetadata {
pub name: String,
pub version: String,
pub authors: Vec<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DatabaseConfig {
pub engine: String,
pub path: String,
pub collections: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub framework: String,
pub port: u16,
pub host: String,
pub middleware: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ScaffoldConfig {
pub structure: Vec<ScaffoldItem>,
}
#[derive(Debug, Clone)]
pub enum ScaffoldItem {
File {
path: String,
generated: bool,
},
Directory {
path: String,
children: Vec<ScaffoldItem>,
},
}
#[derive(Debug, Clone)]
pub struct Endpoint {
pub method: String,
pub path: String,
pub query: Option<String>,
pub auth: AuthRequirement,
pub roles: Vec<String>,
pub rate_limit: Option<String>,
pub validation: HashMap<String, Vec<String>>,
pub doc_comment: Option<String>,
pub crud_schema: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthRequirement {
Required,
Optional,
None,
}
#[derive(Debug, Clone)]
pub struct Test {
pub name: String,
pub steps: Vec<TestStep>,
pub doc_comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TestStep {
pub description: String,
pub action: String,
pub expectations: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DocsConfig {
pub openapi: Option<String>,
pub api_docs: Option<String>,
pub deployment: Option<String>,
pub validation_reference: Option<String>,
pub schema_docs: Option<String>,
}
fn extract_schema_names_from_type(typ: &Type) -> Vec<String> {
match typ {
Type::Named(name) | Type::Custom(name) => vec![name.clone()],
Type::Option(inner) | Type::Vec(inner) => extract_schema_names_from_type(inner),
_ => vec![],
}
}
fn is_primitive_type(type_name: &str) -> bool {
matches!(
type_name,
"String"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "f32"
| "f64"
| "bool"
| "EntityId"
| "Timestamp"
)
}
#[derive(Debug, Clone)]
pub struct DeploymentConfig {
pub target: String,
pub persist: bool,
pub port: Option<u16>,
pub environment: HashMap<String, String>,
pub volumes: HashMap<String, String>,
pub healthcheck: Option<HealthCheckConfig>,
pub restart: String,
}
#[derive(Debug, Clone)]
pub struct HealthCheckConfig {
pub endpoint: String,
pub interval: String,
pub timeout: String,
pub retries: u32,
}
fn parse_deployment_config(config: &crate::parser::ast::ConfigBlock) -> Result<DeploymentConfig> {
use crate::parser::ast::Value;
let mut target = "docker".to_string();
let mut persist = true;
let mut port = None;
let mut environment = HashMap::new();
let mut volumes = HashMap::new();
let mut healthcheck = None;
let mut restart = "unless-stopped".to_string();
for (key, value) in &config.attributes {
match key.as_str() {
"target" => {
if let Some(s) = value.as_string() {
target = s.to_string();
}
}
"persist" => {
if let Value::Boolean(b) = value {
persist = *b;
}
}
"port" => {
if let Value::Integer(i) = value {
port = Some(*i as u16);
}
}
"restart" => {
if let Some(s) = value.as_string() {
restart = s.to_string();
}
}
"environment" => {
if let Value::Map(map) = value {
for (k, v) in map {
if let Some(s) = v.as_string() {
environment.insert(k.clone(), s.to_string());
}
}
}
}
"volumes" => {
if let Value::Map(map) = value {
for (k, v) in map {
if let Some(s) = v.as_string() {
volumes.insert(k.clone(), s.to_string());
}
}
}
}
"healthcheck" => {
if let Value::Map(map) = value {
let mut endpoint = "/health".to_string();
let mut interval = "30s".to_string();
let mut timeout = "60s".to_string();
let mut retries = 3;
for (hc_key, hc_value) in map {
match hc_key.as_str() {
"endpoint" => {
if let Some(s) = hc_value.as_string() {
endpoint = s.to_string();
}
}
"interval" => {
if let Some(s) = hc_value.as_string() {
interval = s.to_string();
}
}
"timeout" => {
if let Some(s) = hc_value.as_string() {
timeout = s.to_string();
}
}
"retries" => {
if let Value::Integer(i) = hc_value {
retries = *i as u32;
}
}
_ => {}
}
}
healthcheck = Some(HealthCheckConfig {
endpoint,
interval,
timeout,
retries,
});
}
}
_ => {}
}
}
Ok(DeploymentConfig {
target,
persist,
port,
environment,
volumes,
healthcheck,
restart,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::dependency::DependencySpec;
#[test]
fn test_project_creation() {
let project = Project::new("test-project".to_string());
assert_eq!(project.metadata.name, "test-project");
assert_eq!(project.schemas.len(), 0);
}
#[test]
fn test_dependency_management() {
let mut project = Project::new("test".to_string());
project.add_dependency(DependencySpec::new("serde").version("1.0"));
project.add_dependency(DependencySpec::new("tokio").version("1.0"));
assert_eq!(project.list_dependencies().len(), 2);
assert!(project.list_dependencies().contains(&"serde"));
assert!(project.list_dependencies().contains(&"tokio"));
}
#[test]
fn test_extract_schema_names_from_type() {
assert_eq!(
extract_schema_names_from_type(&Type::Named("User".to_string())),
vec!["User"]
);
assert_eq!(
extract_schema_names_from_type(&Type::Custom("User".to_string())),
vec!["User"]
);
assert_eq!(
extract_schema_names_from_type(&Type::Vec(Box::new(Type::Named("User".to_string())))),
vec!["User"]
);
assert_eq!(
extract_schema_names_from_type(&Type::Option(Box::new(Type::Named(
"String".to_string()
)))),
vec!["String"]
);
assert_eq!(
extract_schema_names_from_type(&Type::String),
Vec::<String>::new()
);
}
#[test]
fn test_is_primitive_type() {
assert!(is_primitive_type("String"));
assert!(is_primitive_type("i64"));
assert!(is_primitive_type("EntityId"));
assert!(!is_primitive_type("User"));
}
#[test]
fn test_embedding_annotation_conversion() {
use crate::parser::GoldFile;
use crate::parser::ast::{EmbeddingAnnotation, SchemaField};
let mut gold_file = GoldFile::new();
let schema_block = SchemaBlock {
name: "Document".to_string(),
format: None,
fields: vec![
SchemaField {
name: "id".to_string(),
field_type: "EntityId".to_string(),
nullable: false,
default: None,
embedding_annotation: None,
},
SchemaField {
name: "content".to_string(),
field_type: "String".to_string(),
nullable: false,
default: None,
embedding_annotation: None,
},
SchemaField {
name: "embedding".to_string(),
field_type: "Vector".to_string(),
nullable: false,
default: None,
embedding_annotation: Some(EmbeddingAnnotation {
model: "bge-base-en-v1.5".to_string(),
source_field: "content".to_string(),
dimension: Some(768),
paradigm: Some("dense".to_string()),
}),
},
],
content: None,
};
gold_file.add_block(Block::Schema(schema_block));
let project = Project::from_gold_files(vec![gold_file]).unwrap();
assert_eq!(project.schemas.len(), 1);
let schema = project.schemas.get("Document").unwrap();
assert_eq!(schema.fields.len(), 3);
let embedding_field = &schema.fields[2];
assert_eq!(embedding_field.name, "embedding");
assert!(embedding_field.embedding_config.is_some());
let config = embedding_field.embedding_config.as_ref().unwrap();
assert_eq!(config.model, "bge-base-en-v1.5");
assert_eq!(config.source_field, "content");
assert_eq!(config.dimension, Some(768));
assert_eq!(config.paradigm, Some(EmbeddingParadigm::Dense));
}
#[test]
fn test_embedding_invalid_source_field() {
use crate::parser::GoldFile;
use crate::parser::ast::{EmbeddingAnnotation, SchemaField};
let mut gold_file = GoldFile::new();
let schema_block = SchemaBlock {
name: "Document".to_string(),
format: None,
fields: vec![
SchemaField {
name: "id".to_string(),
field_type: "EntityId".to_string(),
nullable: false,
default: None,
embedding_annotation: None,
},
SchemaField {
name: "embedding".to_string(),
field_type: "Vector".to_string(),
nullable: false,
default: None,
embedding_annotation: Some(EmbeddingAnnotation {
model: "bge-base-en-v1.5".to_string(),
source_field: "nonexistent".to_string(), dimension: Some(768),
paradigm: None,
}),
},
],
content: None,
};
gold_file.add_block(Block::Schema(schema_block));
let result = Project::from_gold_files(vec![gold_file]);
assert!(result.is_err());
if let Err(Error::Validation { message, .. }) = result {
assert!(message.contains("source_field 'nonexistent' not found"));
} else {
panic!("Expected validation error for invalid source_field");
}
}
}