torn-api-codegen 0.8.1

Contains the v2 torn API model descriptions and codegen for the bindings
Documentation
use std::{cell::RefCell, rc::Rc};

use indexmap::IndexMap;
use newtype::Newtype;
use object::Object;
use parameter::Parameter;
use path::{Path, PrettySegments};
use proc_macro2::TokenStream;
use r#enum::Enum;
use scope::Scope;

use crate::openapi::{r#type::OpenApiType, schema::OpenApiSchema};

pub mod r#enum;
pub mod newtype;
pub mod object;
pub mod parameter;
pub mod path;
pub mod scope;
pub mod union;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Model {
    Newtype(Newtype),
    Enum(Enum),
    Object(Object),
    Unresolved,
}

impl Model {
    pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
        match self {
            Self::Enum(r#enum) => r#enum.is_display(resolved),
            Self::Newtype(_) => true,
            _ => false,
        }
    }
}

#[derive(Default)]
pub struct ResolvedSchema {
    pub models: IndexMap<String, Model>,
    pub paths: IndexMap<String, Path>,
    pub parameters: Vec<Parameter>,

    pub warnings: WarningReporter,
}

#[derive(Clone)]
pub struct Warning {
    pub message: String,
    pub path: Vec<String>,
}

impl std::fmt::Display for Warning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "in {}: {}", self.path.join("."), self.message)
    }
}

#[derive(Default)]
struct WarningReporterState {
    warnings: Vec<Warning>,
    path: Vec<String>,
}

#[derive(Clone, Default)]
pub struct WarningReporter {
    state: Rc<RefCell<WarningReporterState>>,
}

impl WarningReporter {
    pub fn new() -> Self {
        Self::default()
    }

    fn push(&self, message: impl ToString) {
        let mut state = self.state.borrow_mut();
        let path = state.path.iter().map(ToString::to_string).collect();
        state.warnings.push(Warning {
            message: message.to_string(),
            path,
        });
    }

    fn child(&self, name: impl ToString) -> WarningReporter {
        self.state.borrow_mut().path.push(name.to_string());

        Self {
            state: self.state.clone(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.state.borrow().warnings.is_empty()
    }

    pub fn get_warnings(&self) -> Vec<Warning> {
        self.state.borrow().warnings.clone()
    }
}

impl Drop for WarningReporter {
    fn drop(&mut self) {
        self.state.borrow_mut().path.pop();
    }
}

impl ResolvedSchema {
    pub fn from_open_api(schema: &OpenApiSchema) -> Self {
        let mut result = Self::default();

        for (name, r#type) in &schema.components.schemas {
            result.models.insert(
                name.to_string(),
                resolve(r#type, name, &schema.components.schemas, &result.warnings),
            );
        }

        for (path, body) in &schema.paths {
            result.paths.insert(
                path.to_string(),
                Path::from_schema(
                    path,
                    body,
                    &schema.components.parameters,
                    result.warnings.child(path),
                )
                .unwrap(),
            );
        }

        for (name, param) in &schema.components.parameters {
            result.parameters.push(
                Parameter::from_schema(name, param, result.warnings.child(name.to_owned()))
                    .unwrap(),
            );
        }

        result
    }

    pub fn codegen_models(&self) -> TokenStream {
        let mut output = TokenStream::default();

        for model in self.models.values() {
            output.extend(model.codegen(self));
        }

        output
    }

    pub fn codegen_requests(&self) -> TokenStream {
        let mut output = TokenStream::default();

        for path in self.paths.values() {
            output.extend(
                path.codegen_request(self, self.warnings.child(PrettySegments(&path.segments))),
            );
        }

        output
    }

    pub fn codegen_parameters(&self) -> TokenStream {
        let mut output = TokenStream::default();

        for param in &self.parameters {
            output.extend(param.codegen(self));
        }

        output
    }

    pub fn codegen_scopes(&self) -> TokenStream {
        let mut output = TokenStream::default();

        let scopes = Scope::from_paths(self.paths.values().cloned().collect());

        for scope in scopes {
            output.extend(scope.codegen());
        }

        output
    }
}

pub fn resolve(
    r#type: &OpenApiType,
    name: &str,
    schemas: &IndexMap<&str, OpenApiType>,
    warnings: &WarningReporter,
) -> Model {
    match r#type {
        OpenApiType {
            r#enum: Some(_), ..
        } => Enum::from_schema(name, r#type, warnings.child(name))
            .map_or(Model::Unresolved, Model::Enum),
        OpenApiType {
            r#type: Some("object"),
            ..
        } => Model::Object(Object::from_schema_object(
            name,
            r#type,
            schemas,
            warnings.child(name),
        )),
        OpenApiType {
            r#type: Some(_), ..
        } => Newtype::from_schema(name, r#type).map_or(Model::Unresolved, Model::Newtype),
        OpenApiType {
            one_of: Some(types),
            ..
        } => Enum::from_one_of(name, types, warnings.child(name))
            .map_or(Model::Unresolved, Model::Enum),
        OpenApiType {
            all_of: Some(types),
            ..
        } => Model::Object(Object::from_all_of(
            name,
            types,
            schemas,
            warnings.child(name),
        )),
        _ => Model::Unresolved,
    }
}

impl Model {
    pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
        match self {
            Self::Newtype(newtype) => newtype.codegen(),
            Self::Enum(r#enum) => r#enum.codegen(resolved),
            Self::Object(object) => object.codegen(resolved),
            Self::Unresolved => None,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::openapi::schema::test::get_schema;

    #[test]
    fn resolve_newtypes() {
        let schema = get_schema();

        let user_id_schema = schema.components.schemas.get("UserId").unwrap();

        let reporter = WarningReporter::new();
        let user_id = resolve(
            user_id_schema,
            "UserId",
            &schema.components.schemas,
            &reporter,
        );
        assert!(reporter.is_empty());

        assert_eq!(
            user_id,
            Model::Newtype(Newtype {
                name: "UserId".to_owned(),
                description: None,
                inner: newtype::NewtypeInner::I32,
                copy: true,
                ord: true
            })
        );

        let attack_code_schema = schema.components.schemas.get("AttackCode").unwrap();

        let attack_code = resolve(
            attack_code_schema,
            "AttackCode",
            &schema.components.schemas,
            &reporter,
        );
        assert!(reporter.is_empty());

        assert_eq!(
            attack_code,
            Model::Newtype(Newtype {
                name: "AttackCode".to_owned(),
                description: None,
                inner: newtype::NewtypeInner::Str,
                copy: false,
                ord: false
            })
        );
    }

    #[test]
    fn resolve_all() {
        let schema = get_schema();

        let mut unresolved = vec![];
        let total = schema.components.schemas.len();

        for (name, desc) in &schema.components.schemas {
            let reporter = WarningReporter::new();
            if resolve(desc, name, &schema.components.schemas, &reporter) == Model::Unresolved
                || !reporter.is_empty()
            {
                unresolved.push(name);
            }
        }

        if !unresolved.is_empty() {
            panic!(
                "Failed to resolve {}/{} types. Could not resolve [{}]",
                unresolved.len(),
                total,
                unresolved
                    .into_iter()
                    .map(|u| format!("`{u}`"))
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        }
    }
}