nexgen 0.2.5

Generate language-specific Nexus operation bindings from WIT
Documentation
mod json_schema;
mod wit;

use std::path::{Path, PathBuf};

use crate::error::{Error, Result};
use crate::language::Language;
use crate::spec::{ApiSpec, ApiSpecLeaf, ApiSpecNode, ApiSpecTree, ModulePath};

pub(crate) use json_schema::strip_json_schema_extension;
pub use json_schema::{
    load_api_spec_from_json_schema_for_language_with_inputs,
    load_api_spec_tree_from_json_schema_for_language_with_inputs,
};

pub(crate) use json_schema::{
    ManifestModel, ManifestService, NameManifest, build_name_manifest,
    ts_transfer_type_converter_name,
};
pub use wit::{load_api_spec_from_wit_for_language_with_inputs, write_prepared_wit_directory};

pub(crate) use wit::{
    directive, directive_value, find_proto_name_for_type, find_proto_name_for_type_def,
    parse_directives, parse_wit_with_inputs, resolve_function_signature_args, select_world,
    wire_operation_name_from_docs,
};

#[cfg(test)]
pub(crate) use wit::{
    parse_api_spec_from_wit_for_language, parse_api_spec_from_wit_for_language_with_inputs,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InputFormat {
    Wit,
    JsonSchema,
}

pub fn load_api_spec_for_language_with_inputs(
    language: Language,
    input_paths: &[PathBuf],
) -> Result<ApiSpec> {
    let tree = load_api_spec_tree_for_language_with_inputs(language, input_paths)?;
    tree.into_single_spec()
        .ok_or_else(|| Error::InvalidJsonSchema {
            path: PathBuf::from("<input>"),
            reason: "multiple input modules require tree generation".to_string(),
        })
}

pub fn load_api_spec_tree_for_language_with_inputs(
    language: Language,
    input_paths: &[PathBuf],
) -> Result<ApiSpecTree> {
    let format = detect_input_format(input_paths)?;
    match format {
        InputFormat::Wit => {
            let spec = load_api_spec_from_wit_for_language_with_inputs(language, input_paths)?;
            let source_path = input_paths
                .first()
                .cloned()
                .expect("WIT input paths were validated during format detection");
            Ok(ApiSpecTree {
                root: ApiSpecNode::Leaf(ApiSpecLeaf {
                    module_path: ModulePath::default(),
                    source_root: source_path
                        .parent()
                        .map(Path::to_path_buf)
                        .unwrap_or_default(),
                    source_path,
                    spec,
                }),
            })
        }
        InputFormat::JsonSchema => {
            load_api_spec_tree_from_json_schema_for_language_with_inputs(language, input_paths)
        }
    }
}

fn detect_input_format(input_paths: &[PathBuf]) -> Result<InputFormat> {
    let Some((first, rest)) = input_paths.split_first() else {
        return Err(Error::InvalidWit {
            path: PathBuf::from("<input>"),
            reason: "at least one input path is required".to_string(),
        });
    };
    let format = input_format(first)?;
    if format == InputFormat::Wit {
        return Ok(format);
    }
    for path in rest {
        let found = input_format(path)?;
        if found != format {
            return Err(Error::MixedInputFormats {
                first: input_format_name(format),
                path: path.clone(),
                found: input_format_name(found),
            });
        }
    }
    Ok(format)
}

fn input_format(path: &Path) -> Result<InputFormat> {
    if path.is_dir() {
        return Ok(InputFormat::JsonSchema);
    }
    match path.extension().and_then(|extension| extension.to_str()) {
        Some("wit") => Ok(InputFormat::Wit),
        Some("json" | "yaml" | "yml") => Ok(InputFormat::JsonSchema),
        _ => Err(Error::UnsupportedInputFormat {
            path: path.to_path_buf(),
        }),
    }
}

fn input_format_name(format: InputFormat) -> &'static str {
    match format {
        InputFormat::Wit => "wit",
        InputFormat::JsonSchema => "json-schema",
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn generic_loader_detects_json_schema_inputs() {
        let temp_dir = TempDir::new().unwrap();
        let path = temp_dir.path().join("api.yaml");
        fs::write(
            &path,
            r##"
nexusrpc: "1.0.0"
services:
  ChatService:
    operations:
      getRoom:
        input:
          type: object
          properties: {}
"##,
        )
        .unwrap();

        let spec =
            load_api_spec_for_language_with_inputs(Language::Python, std::slice::from_ref(&path))
                .unwrap();

        assert_eq!(spec.services[0].name, "ChatService");
        assert!(spec.records().next().is_none());
    }

    #[test]
    fn generic_loader_rejects_mixed_input_formats() {
        let temp_dir = TempDir::new().unwrap();
        let json_path = temp_dir.path().join("api.json");
        let wit_path = temp_dir.path().join("api.wit");

        let error =
            load_api_spec_for_language_with_inputs(Language::Python, &[json_path, wit_path])
                .unwrap_err();

        assert!(error.to_string().contains("mixed input formats"));
    }

    #[test]
    fn generic_loader_rejects_unsupported_input_format() {
        let error =
            load_api_spec_for_language_with_inputs(Language::Python, &[PathBuf::from("api.txt")])
                .unwrap_err();

        assert!(error.to_string().contains("unsupported input format"));
    }
}