use crate::v2;
use super::v1;
pub const SCHEMA: &str = "ridl.codegen.v1";
pub trait Backend {
fn language(&self) -> &str;
fn generate(&self, request: &v1::CodegenRequest) -> v1::CodegenResponse;
}
#[derive(Clone, Copy)]
pub struct RawIr<'a> {
pub package: &'a v2::Package,
pub others: &'a [&'a v2::Package],
}
pub struct ModelBackend;
impl Backend for ModelBackend {
fn language(&self) -> &str {
"model"
}
fn generate(&self, request: &v1::CodegenRequest) -> v1::CodegenResponse {
if let Some(option) = request.options.first() {
return v1::CodegenResponse {
files: Vec::new(),
diagnostics: vec![error(format!(
"the model backend takes no option; `{}` is not one it knows",
option.key
))],
};
}
let Some(model) = &request.model else {
return v1::CodegenResponse {
files: Vec::new(),
diagnostics: vec![error("the request carries no model".to_string())],
};
};
match super::to_json_pretty(model) {
Ok(json) => v1::CodegenResponse {
files: vec![text_file(
format!("{}.codegen.json", request.artifact_base),
json,
)],
diagnostics: Vec::new(),
},
Err(err) => v1::CodegenResponse {
files: Vec::new(),
diagnostics: vec![error(err.to_string())],
},
}
}
}
pub fn text_file(path: String, text: String) -> v1::GeneratedFile {
v1::GeneratedFile {
path,
content: Some(v1::generated_file::Content::Text(text)),
}
}
pub const GENERATED_MARKER_PREFIX: &str = "@generated by ridl";
pub fn generated_marker(package: Option<&str>) -> String {
match package {
Some(package) => format!("{GENERATED_MARKER_PREFIX} from package {package}. Do not edit."),
None => format!("{GENERATED_MARKER_PREFIX}. Do not edit."),
}
}
pub fn header_control_character(text: &str) -> Option<char> {
text.chars().find(|c| {
(c.is_control() && !matches!(c, '\t' | '\n' | '\r')) || matches!(c, '\u{2028}' | '\u{2029}')
})
}
pub fn normalise_header(text: &str) -> Option<String> {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
let first = lines.iter().position(|line| !line.is_empty())?;
let last = lines.iter().rposition(|line| !line.is_empty())?;
Some(lines[first..=last].join("\n"))
}
pub fn comment_preamble(marker: &str, header: &str, comment: &str) -> String {
if marker.is_empty() && header.is_empty() {
return String::new();
}
let mut out = String::new();
for line in std::iter::once(marker).chain(header.lines()) {
if line.is_empty() {
out.push_str(comment);
} else {
out.push_str(comment);
out.push(' ');
out.push_str(line);
}
out.push('\n');
}
out.push('\n');
out
}
pub fn error(message: String) -> v1::Diagnostic {
v1::Diagnostic {
severity: v1::DiagnosticSeverity::Error as i32,
message,
}
}
pub fn has_error(response: &v1::CodegenResponse) -> bool {
response.diagnostics.iter().any(|diagnostic| {
!matches!(
v1::DiagnosticSeverity::try_from(diagnostic.severity),
Ok(v1::DiagnosticSeverity::Warning | v1::DiagnosticSeverity::Info)
)
})
}
pub fn check_path(path: &str) -> Result<(), String> {
if path.is_empty() {
return Err("the path is empty".to_string());
}
if path.contains('\0') {
return Err("the path contains a NUL byte".to_string());
}
if path.contains('\\') {
return Err("the path contains `\\`; components are separated by `/`".to_string());
}
if path.starts_with('/') {
return Err("the path is absolute".to_string());
}
if path.as_bytes().get(1).is_some_and(|byte| *byte == b':')
&& path.as_bytes()[0].is_ascii_alphabetic()
{
return Err("the path starts with a drive letter".to_string());
}
for component in path.split('/') {
match component {
"" => return Err("the path has an empty component".to_string()),
"." | ".." => {
return Err(format!("the path has a `{component}` component"));
}
_ => {}
}
}
Ok(())
}
pub fn request_to_json(request: &v1::CodegenRequest) -> Result<String, super::SerializeError> {
v2::render_json(request).map_err(super::SerializeError::from_v2)
}
pub fn request_from_json(text: &str) -> Result<v1::CodegenRequest, serde_json::Error> {
v2::read_json_ignoring_unknown(v2::codegen_request_descriptor(), text)
}
pub fn response_to_json(response: &v1::CodegenResponse) -> Result<String, super::SerializeError> {
v2::render_json(response).map_err(super::SerializeError::from_v2)
}
pub fn response_from_json(text: &str) -> Result<v1::CodegenResponse, serde_json::Error> {
v2::read_json(text)
}