tessera-mobile 0.0.0

Rust on mobile made easy.
Documentation
use std::fmt::{self, Debug, Display};

use colored::{Color, Colorize as _};
use serde::{Deserialize, Serialize};

use crate::{
    apple::teams,
    util::{cli::TextWrapper, prompt},
};

#[derive(Debug)]
pub enum DetectError {
    DeveloperTeamLookupFailed(teams::Error),
}

impl Display for DetectError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DeveloperTeamLookupFailed(err) => {
                write!(f, "Failed to find Apple developer teams: {err}")
            }
        }
    }
}

#[derive(Debug)]
pub enum PromptError {
    DeveloperTeamLookupFailed(teams::Error),
    DeveloperTeamPromptFailed(std::io::Error),
}

impl Display for PromptError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DeveloperTeamLookupFailed(err) => {
                write!(f, "Failed to find Apple developer teams: {err}")
            }
            Self::DeveloperTeamPromptFailed(err) => {
                write!(f, "Failed to prompt for Apple developer team: {err}")
            }
        }
    }
}

fn value_to_string(value: &PlistValue) -> String {
    match value {
        PlistValue::Bool(bool) => bool.to_string(),
        PlistValue::String(string) => string.to_owned(),
        PlistValue::Array(array) => {
            let string = array
                .iter()
                .map(value_to_string)
                .collect::<Vec<_>>()
                .join(",");
            format!("[{string}]")
        }
        PlistValue::Dictionary(dict) => dictionary_to_string(dict),
    }
}

fn pair_to_string(key: &str, value: &PlistValue) -> String {
    format!("{}: {}", key, value_to_string(value))
}

fn dictionary_to_string(dict: &PlistDictionary) -> String {
    let joint = dict
        .dictionary
        .iter()
        .map(|pair| pair_to_string(&pair.0.key, &pair.0.value))
        .collect::<Vec<_>>()
        .join(",");
    format!("{{{joint}}}")
}

#[derive(Clone, Debug, Deserialize)]
#[serde(transparent)]
pub struct NestedPair(PListPair);

impl Serialize for NestedPair {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&pair_to_string(&self.0.key, &self.0.value))
    }
}

#[derive(Clone, Debug, Deserialize)]
pub struct PlistDictionary {
    dictionary: Vec<NestedPair>,
}

impl Serialize for PlistDictionary {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&dictionary_to_string(self))
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PlistValue {
    Bool(bool),
    String(String),
    Array(Vec<PlistValue>),
    Dictionary(PlistDictionary),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PListPair {
    key: String,
    value: PlistValue,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Raw {
    pub development_team: Option<String>,
    pub project_dir: Option<String>,
    pub ios_no_default_features: Option<bool>,
    pub ios_features: Option<Vec<String>>,
    pub macos_no_default_features: Option<bool>,
    pub macos_features: Option<Vec<String>>,
    pub bundle_version: Option<String>,
    pub bundle_version_short: Option<String>,
    pub ios_version: Option<String>,
    pub macos_version: Option<String>,
    pub use_legacy_build_system: Option<bool>,
    pub plist_pairs: Option<Vec<PListPair>>,
    pub enable_bitcode: Option<bool>,
    pub export_options_plist_path: Option<String>,
}

impl Raw {
    pub fn detect() -> Result<Self, DetectError> {
        let development_teams =
            teams::find_development_teams().map_err(DetectError::DeveloperTeamLookupFailed)?;
        Ok(Self {
            development_team: development_teams
                .first()
                .map(|development_team| development_team.id.clone()),
            project_dir: None,
            ios_no_default_features: None,
            ios_features: None,
            macos_no_default_features: None,
            macos_features: None,
            bundle_version: None,
            bundle_version_short: None,
            ios_version: None,
            macos_version: None,
            use_legacy_build_system: None,
            plist_pairs: None,
            enable_bitcode: None,
            export_options_plist_path: None,
        })
    }

    pub fn prompt(wrapper: &TextWrapper) -> Result<Self, PromptError> {
        let development_team = {
            let development_teams =
                teams::find_development_teams().map_err(PromptError::DeveloperTeamLookupFailed)?;
            let default_team = if !development_teams.is_empty() {
                Some("0")
            } else {
                None
            };
            println!("Detected development teams:");
            for (index, team) in development_teams.iter().enumerate() {
                if index == 0 {
                    println!(
                        "{}",
                        format!(
                            "  [{}] {} ({})",
                            index.to_string().bright_green(),
                            team.name,
                            team.id.bright_cyan(),
                        )
                        .bright_white()
                        .bold()
                    );
                } else {
                    println!(
                        "  [{}] {} ({})",
                        index.to_string().green(),
                        team.name,
                        team.id.cyan(),
                    );
                }
            }
            if development_teams.is_empty() {
                println!("  -- none --");
            }
            loop {
                println!(
                    "  Enter an {} for a team above, or enter a {} manually.",
                    "index".green(),
                    "team ID".cyan(),
                );
                let team_input = prompt::default(
                    "Apple development team",
                    default_team,
                    Some(Color::BrightGreen),
                )
                .map_err(PromptError::DeveloperTeamPromptFailed)?;
                let team_id = team_input
                    .parse::<usize>()
                    .ok()
                    .and_then(|index| development_teams.get(index))
                    .map(|team| team.id.clone())
                    .unwrap_or_else(|| team_input);
                if !team_id.is_empty() {
                    break team_id;
                } else {
                    println!(
                        "{}",
                        wrapper
                            .fill("Uh-oh, you need to specify a development team ID.")
                            .bright_magenta()
                    );
                }
            }
        };
        Ok(Self {
            development_team: Some(development_team),
            project_dir: None,
            ios_no_default_features: None,
            ios_features: None,
            macos_no_default_features: None,
            macos_features: None,
            bundle_version: None,
            bundle_version_short: None,
            ios_version: None,
            macos_version: None,
            use_legacy_build_system: None,
            plist_pairs: None,
            enable_bitcode: None,
            export_options_plist_path: None,
        })
    }
}