sofie-model 0.3.0

The app model: modules, areas and routes, read from source without compiling it.
Documentation
use std::path::Path;

use syn::{Expr, ImplItem, ItemImpl, Type};

use crate::syntax::{last_name, line_of, segments, string_literal, type_last_name};
use crate::{AreaClient, AreaDefinition, ModelError};

pub(crate) fn area_definition(
    implementation: &ItemImpl,
    file: &Path,
    module_path: &[String],
) -> Result<Option<AreaDefinition>, ModelError> {
    if implemented_trait(implementation).as_deref() != Some("Area") {
        return Ok(None);
    }
    let line = line_of(implementation);
    let error = |message: &str| ModelError::new(file, line, message);
    let Type::Path(self_type) = implementation.self_ty.as_ref() else {
        return Err(error("write `impl Area for TypeName` beside the type"));
    };
    let Some(type_name) = self_type.path.get_ident() else {
        return Err(error(
            "write `impl Area for TypeName` beside the type, naming it without a path",
        ));
    };
    let name = literal_constant(implementation, "NAME").ok_or_else(|| {
        error("an area needs `const NAME: &'static str = \"folder\";` with a literal name")
    })?;
    let client = match constant_value(implementation, "CLIENT") {
        None => AreaClient::Html,
        Some(Expr::Path(path)) => match last_name(&path.path).as_deref() {
            Some("Html") => AreaClient::Html,
            Some("Svelte") => AreaClient::Svelte,
            Some("React") => {
                return Err(error(
                    "`Client::React` is not supported: the framework's web toolchain has no \
                     React adapter; use `Client::Svelte`",
                ));
            }
            _ => return Err(error("`CLIENT` is `Client::Html` or `Client::Svelte`")),
        },
        Some(_) => return Err(error("write `CLIENT` as `Client::Svelte`")),
    };
    let mut type_path = module_path.to_vec();
    type_path.push(type_name.to_string());
    Ok(Some(AreaDefinition {
        name,
        type_path,
        prefix: literal_constant(implementation, "PREFIX"),
        client,
        principal: associated_type(implementation, "Principal")
            .map(segments)
            .unwrap_or_default(),
        implements_member_area: false,
        opens_sign_up: false,
        file: file.to_path_buf(),
        line,
    }))
}

pub(crate) fn implementing(implementation: &ItemImpl, trait_name: &str) -> Option<String> {
    if implemented_trait(implementation).as_deref() != Some(trait_name) {
        return None;
    }
    type_last_name(&implementation.self_ty)
}

pub(crate) fn implemented_trait(implementation: &ItemImpl) -> Option<String> {
    last_name(&implementation.trait_.as_ref()?.0)
}

fn literal_constant(implementation: &ItemImpl, name: &str) -> Option<String> {
    constant_value(implementation, name).and_then(string_literal)
}

pub(crate) fn constant_value<'file>(
    implementation: &'file ItemImpl,
    name: &str,
) -> Option<&'file Expr> {
    implementation.items.iter().find_map(|item| match item {
        ImplItem::Const(constant) if constant.ident == name => Some(&constant.expr),
        _ => None,
    })
}

pub(crate) fn associated_type<'file>(
    implementation: &'file ItemImpl,
    name: &str,
) -> Option<&'file syn::Path> {
    implementation.items.iter().find_map(|item| match item {
        ImplItem::Type(alias) if alias.ident == name => match &alias.ty {
            Type::Path(path) => Some(&path.path),
            _ => None,
        },
        _ => None,
    })
}

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

    use super::area_definition;
    use crate::AreaClient;

    fn client_of(constant: &str) -> Result<AreaClient, String> {
        let implementation: syn::ItemImpl = syn::parse_str(&format!(
            "impl Area for Member {{ const NAME: &'static str = \"member\"; {constant} }}"
        ))
        .unwrap();
        area_definition(&implementation, Path::new("area.rs"), &[])
            .map(|definition| definition.unwrap().client)
            .map_err(|error| error.to_string())
    }

    #[test]
    fn an_area_names_its_client_or_is_html() {
        assert_eq!(client_of(""), Ok(AreaClient::Html));
        assert_eq!(
            client_of("const CLIENT: Client = Client::Svelte;"),
            Ok(AreaClient::Svelte)
        );
        let react = client_of("const CLIENT: sofie::http::Client = sofie::http::Client::React;");
        assert!(react.unwrap_err().contains("no React adapter"));
        assert!(client_of("const CLIENT: Client = Client::Vue;").is_err());
    }
}