thanix 1.1.0

A yaml-to-rust code generator for generating Rust code from yaml config files e.g. as found in openAPI.
//! Generate structs from API objects.

use crate::bindgen;
use check_keyword::CheckKeyword;
use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};

/// Generate the structs to be used as API request payloads.
///
/// If `workaround_mode` is enabled, will check if the current struct matches with the names listed
/// in `unsanitary_data` and make all fields of these structs optional.
/// This can help when normally generated API clients crash with serialization issues due to
/// NetBox's response data having some fileds set to `null`, despite the YAML stating that they are
/// not nullable.
///
/// > [!Note]
/// > The workaround mentioned above is *only* valid and useful when creating an API client with
/// NetBox.
/// > Using the `--workaround` flag with any other use case is **not advised** because it weakens
/// data validation.
///
/// # Parameters
///
/// * `name: &str` - The name of the struct to generate.
/// * `schema: &Schema` - The schema this struct follows.
/// * `workaround_mode: bool` - Whether `--workaround` flag has been set or not.
/// * `patch_prefix: &str` - Prefix for PATCH request schemas (e.g. "Patched").
///   Fields of schemas with this prefix are made optional.
///
/// # Returns
///
/// * `Option<String>` - The string represnetation of the given struct.
pub fn generate(
    name: &str,
    schema: &Schema,
    workaround_mode: bool,
    patch_prefix: &str,
) -> Option<String> {
    let typ = match &schema.schema_kind {
        SchemaKind::Type(x) => x,
        _ => return None,
    };

    // Assemble struct string.
    let mut result =
        "#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct ".to_owned();
    result += name;

    // If not an ObjectType or an Array of objects, return None.
    match &typ {
        Type::Object(obj) => {
            result += " {\n";

            // For every component property.
            for (prop_name, prop) in &obj.properties {
                let p = prop.clone().unbox();
                // Assemble a field declaration in the struct.
                let type_name = bindgen::type_to_string(&p);

                // If the property has a description, prepend a doc string.
                if let ReferenceOr::Item(item) = &p {
                    if let Some(desc) = &item.schema_data.description {
                        result += bindgen::make_comment(Some(desc.clone()), 1).as_str();
                    }
                }
                result += "\t";

                // Patch requests need to accept partial data.
                if !patch_prefix.is_empty() && name.starts_with(patch_prefix) {
                    result += "#[serde(skip_serializing_if = \"Option::is_none\")]\n\t";
                }

                result += "pub ";
                result += &prop_name.clone().into_safe();
                result += ": ";

                // Patch schema fields must be optional so partial updates can omit unchanged fields.
                let typ = if !patch_prefix.is_empty() && name.starts_with(patch_prefix) {
                    format!("Option<{}>", type_name)
                } else if workaround_mode
                    && !name.ends_with("Request")
                    && !type_name.contains("Option")
                    && prop_name != "id"
                {
                    format!("Option<{}>", type_name)
                } else {
                    type_name
                };

                result += &typ;
                result += ",\n";
            }

            result += "}\n";
        }
        Type::Array(obj) => {
            let p = obj.items.clone().unwrap().clone().unbox();
            // Assemble a field declaration in the struct.
            let type_name = bindgen::type_to_string(&p);

            // If the property has a description, prepend a doc string.
            if let ReferenceOr::Item(item) = &p {
                if let Some(desc) = &item.schema_data.description {
                    result += bindgen::make_comment(Some(desc.clone()), 1).as_str();
                }
            }
            result += "(pub ";
            result += &type_name;
            result += ");\n";
        }
        _ => {
            return None;
        }
    }

    Some(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    use openapiv3::{Schema, SchemaKind, StringType, Type};

    #[test]
    fn test_generate_with_non_object_schema() {
        let schema = Schema {
            schema_data: Default::default(),
            schema_kind: SchemaKind::Type(Type::String(StringType {
                ..Default::default()
            })), // Not an object
        };
        let result = generate("InvalidStruct", &schema, false, "Patched");
        assert_eq!(result, None);
    }

    // TODO: Expand these tests.
}