use std::collections::BTreeMap;
use std::path::PathBuf;
pub(crate) mod dotnet;
pub(crate) mod go;
pub(crate) mod java;
pub(crate) mod json_schema;
pub(crate) mod proto;
pub(crate) mod python;
mod resource_plan;
pub(crate) mod typescript;
use crate::SupportFiles;
use crate::descriptors::DescriptorIndex;
use crate::error::{Error, Result};
use crate::language::Language;
use crate::planning::{PlannedFamily, PlannedSpec, PlannedType};
use crate::spec::{ApiSpec, RecordSpec};
use crate::spec::{ApiSpecNode, ApiSpecTree};
pub(crate) use resource_plan::render_request_plan;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneratedOutputLayout {
SingleFile,
Directory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedFiles {
pub layout: GeneratedOutputLayout,
pub files: BTreeMap<PathBuf, String>,
pub warnings: Vec<String>,
}
impl GeneratedFiles {
pub fn single_file(contents: String) -> Self {
let mut files = BTreeMap::new();
files.insert(PathBuf::from("output"), contents);
Self {
layout: GeneratedOutputLayout::SingleFile,
files,
warnings: Vec::new(),
}
}
pub fn directory(files: BTreeMap<PathBuf, String>) -> Self {
Self {
layout: GeneratedOutputLayout::Directory,
files,
warnings: Vec::new(),
}
}
pub fn single_file_contents(&self) -> Option<&str> {
(self.layout == GeneratedOutputLayout::SingleFile)
.then(|| self.files.values().next().map(String::as_str))
.flatten()
}
}
pub(crate) trait ExternalModelBackend<ModelType = PlannedType> {
type ModelFragments;
type WireConversion;
fn prepare(&mut self, api_plan: &PlannedSpec) -> Result<()>;
fn render_models(&self) -> Result<Self::ModelFragments>;
fn render_support_files(&self) -> Result<BTreeMap<PathBuf, String>> {
Ok(BTreeMap::new())
}
fn model_type_annotation(&self, model_type: &ModelType) -> Option<String>;
fn wire_type_identifier(&self, model_type: &ModelType) -> Option<String>;
fn wire_conversion(
&self,
model_type: &ModelType,
planned_record: Option<&RecordSpec<PlannedFamily>>,
) -> Option<Self::WireConversion>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GenerationMode {
#[default]
NativeApi,
DefinitionsOnly,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TsDateTimeTypes {
#[default]
String,
Date,
Temporal,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct GenerateFilesOptions {
pub(crate) go_output_dir_name: String,
pub(crate) java_package_root: Option<String>,
pub(crate) ts_date_time_types: TsDateTimeTypes,
}
pub(crate) fn generate_files_for_tree_with_mode_and_options(
language: Language,
tree: ApiSpecTree,
descriptors: &DescriptorIndex,
support: &SupportFiles,
mode: GenerationMode,
options: GenerateFilesOptions,
) -> Result<GeneratedFiles> {
crate::compile_tree_to_files(language, tree, descriptors, support, mode, options)
}
pub(crate) fn generate_files_from_planned_tree(
language: Language,
tree: &ApiSpecTree<PlannedFamily>,
support: &SupportFiles,
mode: GenerationMode,
options: GenerateFilesOptions,
) -> Result<GeneratedFiles> {
let mut generated = match language {
Language::Dotnet => dotnet::generate(tree, support, mode),
Language::Go => generate_go_tree(tree, support, mode, options),
Language::Java => java::generate(tree, support, mode, options.java_package_root.as_deref()),
Language::Python => python::generate(tree, support, mode),
Language::TypeScript => {
typescript::generate(tree, support, mode, options.ts_date_time_types)
}
language => Err(Error::UnsupportedLanguage { language }),
}?;
generated.warnings = if mode == GenerationMode::NativeApi {
generation_warnings_for_tree(tree)
} else {
Vec::new()
};
Ok(generated)
}
fn generate_go_tree(
tree: &ApiSpecTree<PlannedFamily>,
support: &SupportFiles,
mode: GenerationMode,
options: GenerateFilesOptions,
) -> Result<GeneratedFiles> {
go::generate_tree(
tree,
support,
&go::GoOptions {
output_dir_name: options.go_output_dir_name,
..go::GoOptions::default()
},
mode,
)
}
pub fn generate_source(
language: Language,
spec: ApiSpec,
descriptors: &DescriptorIndex,
support: &SupportFiles,
) -> Result<String> {
generate_source_with_mode(
language,
spec,
descriptors,
support,
GenerationMode::NativeApi,
)
}
pub fn generate_source_with_mode(
language: Language,
spec: ApiSpec,
descriptors: &DescriptorIndex,
support: &SupportFiles,
mode: GenerationMode,
) -> Result<String> {
let generated = generate_files_for_tree_with_mode_and_options(
language,
ApiSpecTree::single(spec),
descriptors,
support,
mode,
GenerateFilesOptions::default(),
)?;
Ok(match generated.layout {
GeneratedOutputLayout::SingleFile => generated
.single_file_contents()
.expect("single-file output should contain one file")
.to_string(),
GeneratedOutputLayout::Directory => generated
.files
.iter()
.map(|(path, contents)| format!("### {}\n{contents}", path.display()))
.collect::<Vec<_>>()
.join("\n\n"),
})
}
fn generation_warnings(plan: &PlannedSpec) -> Vec<String> {
plan.services
.iter()
.flat_map(|service| {
service.resources.iter().flat_map(|resource| {
resource.data.methods.iter().filter_map(|method| {
matches!(
method.binding,
crate::planning::PlannedResourceMethodBindingSpec::Stub
)
.then(|| {
format!(
"resource method `{}.{}` generated as a stub because no operation could be bound",
resource.data.type_name, method.name
)
})
})
})
})
.collect()
}
fn generation_warnings_for_tree(tree: &ApiSpecTree<PlannedFamily>) -> Vec<String> {
fn collect(node: &ApiSpecNode<PlannedFamily>, warnings: &mut Vec<String>) {
match node {
ApiSpecNode::Leaf(leaf) => warnings.extend(generation_warnings(&leaf.spec)),
ApiSpecNode::Branch(branch) => {
for child in branch.children.values() {
collect(child, warnings);
}
}
}
}
let mut warnings = Vec::new();
collect(&tree.root, &mut warnings);
warnings
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use prost_types::FileDescriptorSet;
use crate::SupportFiles;
use crate::descriptors::DescriptorIndex;
use crate::language::Language;
use super::{
GenerateFilesOptions, GenerationMode, generate_files_for_tree_with_mode_and_options,
};
use crate::spec::ApiSpecTree;
#[test]
fn warns_when_resource_method_generates_as_stub() {
let wit = r#"
package temporal:users@1.0.0;
world system {
export user-service;
}
/// @nexus.endpoint "__user_service"
interface user-service {
resource user {
constructor(user-id: string, email: string);
update-email: func(email: string) -> user-result;
}
type user-result = own<user>;
record update-email-request {
users-id: string,
email: string,
}
update-email: func(request: update-email-request) -> user-result;
}
"#;
let spec = crate::parser::parse_api_spec_from_wit_for_language(
Language::Python,
wit,
PathBuf::from("inline.wit"),
)
.unwrap();
let descriptors =
DescriptorIndex::from_descriptor_set(FileDescriptorSet { file: Vec::new() }).unwrap();
let generated = generate_files_for_tree_with_mode_and_options(
Language::Python,
ApiSpecTree::single(spec),
&descriptors,
&SupportFiles::default(),
GenerationMode::NativeApi,
GenerateFilesOptions::default(),
)
.unwrap();
assert_eq!(
generated.warnings,
vec![
"resource method `User.update-email` generated as a stub because no operation could be bound"
.to_string()
]
);
}
#[test]
fn definitions_only_generation_does_not_require_endpoint() {
let wit = r#"
package temporal:example@1.0.0;
world system {
export example-service;
}
interface example-service {
record request {
name: string,
}
record response {
message: string,
}
example-operation: func(request: request) -> response;
}
"#;
let spec = crate::parser::parse_api_spec_from_wit_for_language(
Language::Python,
wit,
PathBuf::from("inline.wit"),
)
.unwrap();
let descriptors =
DescriptorIndex::from_descriptor_set(FileDescriptorSet { file: Vec::new() }).unwrap();
let generated = generate_files_for_tree_with_mode_and_options(
Language::Python,
ApiSpecTree::single(spec),
&descriptors,
&SupportFiles::default(),
GenerationMode::DefinitionsOnly,
GenerateFilesOptions::default(),
)
.unwrap();
assert!(generated.files.contains_key(&PathBuf::from("models.py")));
assert!(generated.files.contains_key(&PathBuf::from("services.py")));
assert!(
!generated
.files
.contains_key(&PathBuf::from("operations/example_operation.py"))
);
assert!(generated.files[&PathBuf::from("services.py")].contains("class ExampleService"));
}
}