use std::collections::HashMap;
use std::fs;
use std::path::Path;
use convert_case::{Case, Casing};
use once_cell::sync::Lazy;
use regex_lite::Regex;
use smol_str::SmolStr;
use crate::converter::{FieldBuilder, ModelBuilder, SchemaBuilder, dummy_span};
use crate::error::ImportResult;
use crate::prisma::types::*;
use prax_schema::ast::*;
static DATASOURCE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"(?s)datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"[^}]*url\s*=\s*[^"}]*"([^"]+)""#,
)
.unwrap()
});
static MODEL_START_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"model\s+(\w+)\s*\{").unwrap());
static MAP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"@map\("([^"]+)"\)"#).unwrap());
static RELATION_NAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"name:\s*"([^"]+)""#).unwrap());
static RELATION_FIELDS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"fields:\s*\[([^\]]+)\]").unwrap());
static RELATION_REFS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"references:\s*\[([^\]]+)\]").unwrap());
static MODEL_ID_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"@@id\(\[([^\]]+)\]\)").unwrap());
static MODEL_UNIQUE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"@@unique\(\[([^\]]+)\]").unwrap());
static MODEL_INDEX_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"@@index\(\[([^\]]+)\]").unwrap());
static MODEL_MAP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"@@map\("([^"]+)"\)"#).unwrap());
static ATTR_NAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"name:\s*"([^"]+)""#).unwrap());
static ENUM_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?s)enum\s+(\w+)\s*\{([^}]+)\}").unwrap());
pub fn parse_prisma_schema(input: &str) -> ImportResult<PrismaSchema> {
let mut schema = PrismaSchema {
datasource: None,
models: vec![],
enums: vec![],
};
if let Some(datasource) = parse_datasource(input)? {
schema.datasource = Some(datasource);
}
for model in parse_models(input)? {
schema.models.push(model);
}
for enum_def in parse_enums(input)? {
schema.enums.push(enum_def);
}
Ok(schema)
}
pub fn parse_prisma_file<P: AsRef<Path>>(path: P) -> ImportResult<PrismaSchema> {
let content = fs::read_to_string(path)?;
parse_prisma_schema(&content)
}
pub fn import_prisma_schema(input: &str) -> ImportResult<Schema> {
let prisma_schema = parse_prisma_schema(input)?;
convert_prisma_to_prax(prisma_schema)
}
pub fn import_prisma_schema_file<P: AsRef<Path>>(path: P) -> ImportResult<Schema> {
let prisma_schema = parse_prisma_file(path)?;
convert_prisma_to_prax(prisma_schema)
}
fn parse_datasource(input: &str) -> ImportResult<Option<PrismaDatasource>> {
if let Some(caps) = DATASOURCE_RE.captures(input) {
let provider = caps.get(1).unwrap().as_str().to_string();
let url = caps.get(2).unwrap().as_str().to_string();
Ok(Some(PrismaDatasource { provider, url }))
} else {
Ok(None)
}
}
fn parse_models(input: &str) -> ImportResult<Vec<PrismaModel>> {
let mut models = vec![];
let mut search_from = 0;
while let Some(caps) = MODEL_START_RE.captures(&input[search_from..]) {
let m = caps.get(0).unwrap();
let name = caps.get(1).unwrap().as_str().to_string();
let brace_start = search_from + m.end();
let mut depth = 1;
let mut end = brace_start;
let bytes = input.as_bytes();
let mut in_string = false;
while end < bytes.len() && depth > 0 {
match bytes[end] {
b'"' if !in_string => in_string = true,
b'"' if in_string => in_string = false,
b'{' if !in_string => depth += 1,
b'}' if !in_string => depth -= 1,
_ => {}
}
if depth > 0 {
end += 1;
}
}
let body = &input[brace_start..end];
let fields = parse_fields(body)?;
let attributes = parse_model_attributes(body)?;
models.push(PrismaModel {
name,
fields,
attributes,
documentation: None,
});
search_from = end + 1;
}
Ok(models)
}
fn parse_fields(body: &str) -> ImportResult<Vec<PrismaField>> {
let mut fields = vec![];
for line in body.lines() {
let line = line.trim();
if line.starts_with("@@") || line.is_empty() || line.starts_with("//") {
continue;
}
if let Some((name, type_str)) = split_field_line(line) {
let (field_type, is_optional, is_list) = parse_field_type(&type_str)?;
let attributes = parse_field_attributes(line)?;
fields.push(PrismaField {
name,
field_type,
is_optional,
is_list,
attributes,
documentation: None,
});
}
}
Ok(fields)
}
fn extract_paren_content<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
let start = line.find(prefix)?;
let after_prefix = &line[start + prefix.len()..];
if !after_prefix.starts_with('(') {
return None;
}
let mut depth = 0;
let mut in_string = false;
for (i, c) in after_prefix.char_indices() {
match c {
'"' => in_string = !in_string,
'(' if !in_string => depth += 1,
')' if !in_string => {
depth -= 1;
if depth == 0 {
return Some(&after_prefix[1..i]);
}
}
_ => {}
}
}
None
}
fn split_field_line(line: &str) -> Option<(String, String)> {
let trimmed = line.trim();
if trimmed.starts_with('@') || trimmed.starts_with("///") {
return None;
}
let mut chars = trimmed.chars().peekable();
let name: String = chars
.by_ref()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.is_empty() {
return None;
}
let rest: String = chars.collect();
let rest = rest.trim_start();
let type_str = if rest.starts_with("Unsupported(") {
let mut depth = 0;
let mut in_string = false;
let mut end = 0;
for (i, c) in rest.char_indices() {
match c {
'"' => in_string = !in_string,
'(' if !in_string => depth += 1,
')' if !in_string => {
depth -= 1;
if depth == 0 {
end = i + 1;
break;
}
}
_ => {}
}
}
&rest[..end]
} else {
let end = rest
.find(|c: char| c.is_whitespace() || c == '@')
.unwrap_or(rest.len());
&rest[..end]
};
if type_str.is_empty() {
return None;
}
Some((name, type_str.to_string()))
}
fn parse_field_type(type_str: &str) -> ImportResult<(PrismaFieldType, bool, bool)> {
let is_optional = type_str.contains('?');
let is_list = type_str.contains("[]");
let base_type = type_str.replace('?', "").replace("[]", "");
let field_type = if base_type.starts_with("Unsupported(") {
let inner = base_type
.strip_prefix("Unsupported(\"")
.and_then(|s| s.strip_suffix("\")"))
.unwrap_or(&base_type);
PrismaFieldType::Custom(format!("Unsupported:{}", inner))
} else {
match base_type.as_str() {
"String" => PrismaFieldType::String,
"Boolean" => PrismaFieldType::Boolean,
"Int" => PrismaFieldType::Int,
"BigInt" => PrismaFieldType::BigInt,
"Float" => PrismaFieldType::Float,
"Decimal" => PrismaFieldType::Decimal,
"DateTime" => PrismaFieldType::DateTime,
"Json" => PrismaFieldType::Json,
"Bytes" => PrismaFieldType::Bytes,
custom => PrismaFieldType::Custom(custom.to_string()),
}
};
Ok((field_type, is_optional, is_list))
}
fn parse_field_attributes(line: &str) -> ImportResult<Vec<PrismaFieldAttribute>> {
let mut attributes = vec![];
if line.contains("@id") {
attributes.push(PrismaFieldAttribute::Id);
}
if line.contains("@unique") {
attributes.push(PrismaFieldAttribute::Unique);
}
if line.contains("@updatedAt") {
attributes.push(PrismaFieldAttribute::UpdatedAt);
}
if let Some(default_val) = extract_paren_content(line, "@default") {
let default = if default_val.contains('(') {
PrismaDefaultValue::Function(default_val.to_string())
} else {
PrismaDefaultValue::Literal(default_val.to_string())
};
attributes.push(PrismaFieldAttribute::Default(default));
}
if let Some(caps) = MAP_RE.captures(line) {
let map_val = caps.get(1).unwrap().as_str().to_string();
attributes.push(PrismaFieldAttribute::Map(map_val));
}
if line.contains("@relation") {
let relation = parse_relation_attribute(line)?;
attributes.push(relation);
}
Ok(attributes)
}
fn parse_relation_attribute(line: &str) -> ImportResult<PrismaFieldAttribute> {
if let Some(args) = extract_paren_content(line, "@relation") {
let name = extract_relation_name(args);
let fields = extract_relation_fields(args);
let references = extract_relation_references(args);
let on_delete = extract_relation_action(args, "onDelete");
let on_update = extract_relation_action(args, "onUpdate");
let map = extract_relation_map(args);
Ok(PrismaFieldAttribute::Relation {
name,
fields,
references,
on_delete,
on_update,
map,
})
} else {
Ok(PrismaFieldAttribute::Relation {
name: None,
fields: None,
references: None,
on_delete: None,
on_update: None,
map: None,
})
}
}
fn extract_relation_name(args: &str) -> Option<String> {
if let Some(caps) = RELATION_NAME_RE.captures(args) {
return caps.get(1).map(|m| m.as_str().to_string());
}
let inside = args.trim().strip_prefix('"')?;
let end = inside.find('"')?;
Some(inside[..end].to_string())
}
fn extract_relation_fields(args: &str) -> Option<Vec<String>> {
RELATION_FIELDS_RE.captures(args).map(|caps| {
caps.get(1)
.unwrap()
.as_str()
.split(',')
.map(|s| s.trim().to_string())
.collect()
})
}
fn extract_relation_references(args: &str) -> Option<Vec<String>> {
RELATION_REFS_RE.captures(args).map(|caps| {
caps.get(1)
.unwrap()
.as_str()
.split(',')
.map(|s| s.trim().to_string())
.collect()
})
}
fn extract_relation_action(args: &str, action: &str) -> Option<String> {
let pattern = format!(r"{}:\s*(\w+)", action);
let re = Regex::new(&pattern).unwrap();
re.captures(args)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
fn extract_relation_map(args: &str) -> Option<String> {
let re = Regex::new(r#"map:\s*"([^"]+)""#).unwrap();
re.captures(args)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
fn parse_model_attributes(body: &str) -> ImportResult<Vec<PrismaModelAttribute>> {
let mut attributes = vec![];
for line in body.lines() {
let line = line.trim();
if line.starts_with("@@id")
&& let Some(caps) = MODEL_ID_RE.captures(line)
{
let fields = caps
.get(1)
.unwrap()
.as_str()
.split(',')
.map(|s| s.trim().to_string())
.collect();
attributes.push(PrismaModelAttribute::Id(fields));
}
if line.starts_with("@@unique")
&& let Some(caps) = MODEL_UNIQUE_RE.captures(line)
{
let fields = caps
.get(1)
.unwrap()
.as_str()
.split(',')
.map(|s| s.trim().to_string())
.collect();
let name = ATTR_NAME_RE
.captures(line)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
attributes.push(PrismaModelAttribute::Unique { fields, name });
}
if line.starts_with("@@index")
&& let Some(caps) = MODEL_INDEX_RE.captures(line)
{
let fields = caps
.get(1)
.unwrap()
.as_str()
.split(',')
.map(|s| s.trim().to_string())
.collect();
let name = ATTR_NAME_RE
.captures(line)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
attributes.push(PrismaModelAttribute::Index { fields, name });
}
if line.starts_with("@@map")
&& let Some(caps) = MODEL_MAP_RE.captures(line)
{
let map_val = caps.get(1).unwrap().as_str().to_string();
attributes.push(PrismaModelAttribute::Map(map_val));
}
}
Ok(attributes)
}
fn parse_enums(input: &str) -> ImportResult<Vec<PrismaEnum>> {
let mut enums = vec![];
for caps in ENUM_RE.captures_iter(input) {
let name = caps.get(1).unwrap().as_str().to_string();
let body = caps.get(2).unwrap().as_str();
let values = body
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with("//"))
.map(|l| l.to_string())
.collect();
enums.push(PrismaEnum {
name,
values,
documentation: None,
});
}
Ok(enums)
}
fn convert_prisma_to_prax(prisma_schema: PrismaSchema) -> ImportResult<Schema> {
let mut builder = SchemaBuilder::new();
if let Some(datasource) = prisma_schema.datasource {
builder = builder.with_datasource(datasource.provider, datasource.url);
}
for model in prisma_schema.models {
let prax_model = convert_model(model)?;
builder.add_model(prax_model);
}
for enum_def in prisma_schema.enums {
let prax_enum = convert_enum(enum_def);
builder.add_enum(prax_enum);
}
Ok(builder.build())
}
fn convert_model(model: PrismaModel) -> ImportResult<Model> {
let mut model_builder = ModelBuilder::new(&model.name);
for attr in &model.attributes {
if let PrismaModelAttribute::Map(table_name) = attr {
model_builder = model_builder.with_db_name(table_name);
}
}
let field_name_map: HashMap<String, String> = model
.fields
.iter()
.map(|f| {
let prax_name = f
.attributes
.iter()
.find_map(|a| {
if let PrismaFieldAttribute::Map(col) = a {
Some(col.clone())
} else {
None
}
})
.unwrap_or_else(|| f.name.to_case(Case::Snake));
(f.name.clone(), prax_name)
})
.collect();
for field in model.fields {
let prax_field = convert_field(field, &field_name_map)?;
model_builder.add_field(prax_field);
}
for attr in model.attributes {
match attr {
PrismaModelAttribute::Unique { fields, name } => {
let mapped: Vec<String> = fields
.into_iter()
.map(|f| remap_field_name(&f, &field_name_map))
.collect();
model_builder.add_unique(mapped, name);
}
PrismaModelAttribute::Index { fields, name } => {
let mapped: Vec<String> = fields
.into_iter()
.map(|f| remap_field_name(&f, &field_name_map))
.collect();
model_builder.add_index(mapped, name);
}
PrismaModelAttribute::Map(_) => {
}
PrismaModelAttribute::Id(fields) => {
let mapped: Vec<String> = fields
.into_iter()
.map(|f| remap_field_name(&f, &field_name_map))
.collect();
model_builder.add_unique(mapped, Some("PRIMARY".to_string()));
}
}
}
Ok(model_builder.build())
}
fn remap_field_name(prisma_name: &str, map: &HashMap<String, String>) -> String {
map.get(prisma_name)
.cloned()
.unwrap_or_else(|| prisma_name.to_case(Case::Snake))
}
fn convert_field(
field: PrismaField,
field_name_map: &HashMap<String, String>,
) -> ImportResult<Field> {
let (prax_type, modifier) =
convert_field_type(&field.field_type, field.is_optional, field.is_list)?;
let prax_name = field_name_map
.get(&field.name)
.cloned()
.unwrap_or_else(|| field.name.to_case(Case::Snake));
let mut field_builder = FieldBuilder::new(&prax_name, prax_type, modifier);
for attr in field.attributes {
match attr {
PrismaFieldAttribute::Id => {
field_builder = field_builder.with_id();
}
PrismaFieldAttribute::Unique => {
field_builder = field_builder.with_unique();
}
PrismaFieldAttribute::Default(default_val) => match default_val {
PrismaDefaultValue::Function(ref func)
if func == "autoincrement()" || func == "autoincrement" =>
{
field_builder = field_builder.with_auto();
}
_ => {
let prax_default = convert_default_value(default_val);
field_builder = field_builder.with_default(prax_default);
}
},
PrismaFieldAttribute::Map(_) => {
}
PrismaFieldAttribute::UpdatedAt => {
field_builder = field_builder.with_updated_at();
}
PrismaFieldAttribute::Relation {
name,
fields,
references,
on_delete,
on_update,
map,
} => {
if let (Some(fk_fields), Some(ref_fields)) = (fields, references) {
let mapped_fields: Vec<String> = fk_fields
.into_iter()
.map(|f| remap_field_name(&f, field_name_map))
.collect();
let mapped_refs: Vec<String> = ref_fields
.into_iter()
.map(|f| remap_field_name(&f, field_name_map))
.collect();
field_builder = field_builder.with_relation(
name,
mapped_fields,
mapped_refs,
on_delete,
on_update,
map,
);
}
}
}
}
Ok(field_builder.build())
}
fn convert_field_type(
field_type: &PrismaFieldType,
is_optional: bool,
is_list: bool,
) -> ImportResult<(FieldType, TypeModifier)> {
let base_type = match field_type {
PrismaFieldType::String => FieldType::Scalar(ScalarType::String),
PrismaFieldType::Boolean => FieldType::Scalar(ScalarType::Boolean),
PrismaFieldType::Int => FieldType::Scalar(ScalarType::Int),
PrismaFieldType::BigInt => FieldType::Scalar(ScalarType::BigInt),
PrismaFieldType::Float => FieldType::Scalar(ScalarType::Float),
PrismaFieldType::Decimal => FieldType::Scalar(ScalarType::Decimal),
PrismaFieldType::DateTime => FieldType::Scalar(ScalarType::DateTime),
PrismaFieldType::Json => FieldType::Scalar(ScalarType::Json),
PrismaFieldType::Bytes => FieldType::Scalar(ScalarType::Bytes),
PrismaFieldType::Custom(name) => {
if let Some(inner) = name.strip_prefix("Unsupported:") {
FieldType::Unsupported(SmolStr::from(inner))
} else {
FieldType::Model(SmolStr::from(name.as_str()))
}
}
};
let modifier = match (is_optional, is_list) {
(true, true) => TypeModifier::OptionalList,
(false, true) => TypeModifier::List,
(true, false) => TypeModifier::Optional,
(false, false) => TypeModifier::Required,
};
Ok((base_type, modifier))
}
fn convert_default_value(default: PrismaDefaultValue) -> AttributeValue {
match default {
PrismaDefaultValue::Literal(val) => {
if val == "true" {
AttributeValue::Boolean(true)
} else if val == "false" {
AttributeValue::Boolean(false)
} else if let Ok(n) = val.parse::<i64>() {
AttributeValue::Int(n)
} else if let Ok(f) = val.parse::<f64>() {
AttributeValue::Float(f)
} else {
AttributeValue::String(val)
}
}
PrismaDefaultValue::Function(func) => {
if let Some(paren_pos) = func.find('(') {
let name = &func[..paren_pos];
let args_str = func[paren_pos + 1..].trim_end_matches(')');
let args = if args_str.is_empty() {
vec![]
} else {
vec![AttributeValue::String(args_str.to_string())]
};
AttributeValue::Function(SmolStr::from(name), args)
} else {
AttributeValue::Function(SmolStr::from(func.as_str()), vec![])
}
}
}
}
fn convert_enum(enum_def: PrismaEnum) -> Enum {
let mut prax_enum = Enum::new(Ident::new(&enum_def.name, dummy_span()), dummy_span());
for variant_name in enum_def.values {
let variant = EnumVariant {
name: Ident::new(&variant_name, dummy_span()),
attributes: vec![],
documentation: None,
span: dummy_span(),
};
prax_enum.variants.push(variant);
}
if let Some(doc) = enum_def.documentation {
prax_enum.documentation = Some(Documentation::new(doc, dummy_span()));
}
prax_enum
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_model() {
let schema = r#"
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
"#;
let result = parse_prisma_schema(schema);
assert!(result.is_ok());
let prisma_schema = result.unwrap();
assert_eq!(prisma_schema.models.len(), 1);
assert_eq!(prisma_schema.models[0].fields.len(), 3);
}
#[test]
fn test_import_simple_model() {
let schema = r#"
model User {
id Int @id @default(autoincrement())
email String @unique
}
"#;
let result = import_prisma_schema(schema);
assert!(result.is_ok());
let prax_schema = result.unwrap();
assert_eq!(prax_schema.models.len(), 1);
}
}