waterui-cli 0.1.3

A modern UI framework for Rust
Documentation
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::{
    backend::Backend,
    project::Project,
    templates::{self, TemplateContext},
};

#[derive(Debug, Serialize, Deserialize, Clone)]
// Warn: You cannot use both revision and local_path at the same time.
/// Configuration for the Apple backend in a `WaterUI` project.
///
/// `[backend.apple]` in `Water.toml`
pub struct AppleBackend {
    #[serde(
        default = "default_apple_project_path",
        skip_serializing_if = "is_default_apple_project_path"
    )]
    /// Path to the Apple project within the `WaterUI` project.
    pub project_path: PathBuf,
    /// The scheme to use for building the Apple project.
    pub scheme: String,
    /// The branch of the Apple backend to use.
    ///
    /// You cannot use both branch and revision at the same time.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,

    /// The revision (commit hash or tag) of the Apple backend to use.
    ///
    /// You cannot use both revision and branch at the same time.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revision: Option<String>,
    /// Local path to the Apple backend for local dev.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backend_path: Option<String>,
}

impl AppleBackend {
    /// Create a new Apple backend configuration with the given scheme.
    #[must_use]
    pub fn new(scheme: impl Into<String>) -> Self {
        Self {
            project_path: default_apple_project_path(),
            scheme: scheme.into(),
            branch: None,
            revision: None,
            backend_path: None,
        }
    }

    /// Set a custom project path (defaults to "apple").
    #[must_use]
    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.project_path = path.into();
        self
    }

    /// Set the local backend path for development.
    #[must_use]
    pub fn with_backend_path(mut self, path: impl Into<String>) -> Self {
        self.backend_path = Some(path.into());
        self
    }

    /// Get the path to the Apple project within the `WaterUI` project.
    #[must_use]
    pub fn project_path(&self) -> &Path {
        &self.project_path
    }
}

fn default_apple_project_path() -> PathBuf {
    PathBuf::from("apple")
}

fn is_default_apple_project_path(s: &Path) -> bool {
    s == Path::new("apple")
}

impl Backend for AppleBackend {
    const DEFAULT_PATH: &'static str = "apple";

    fn path(&self) -> &Path {
        &self.project_path
    }

    async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
        let manifest = project.manifest();

        // For playground projects, use fixed scheme name "WaterUIApp"
        // For regular projects, scheme name must match the Xcode target name (crate name)
        let is_playground =
            manifest.package.package_type == crate::project::PackageType::Playground;

        // For playground projects, use fixed names
        // For regular projects, derive from crate name
        let (scheme, app_name, crate_name_for_template) = if is_playground {
            (
                "WaterUIApp".to_string(),
                "WaterUIApp".to_string(),
                "WaterUIApp".to_string(),
            )
        } else {
            let crate_name = project.crate_name().to_string();
            // App name for Swift code must be a valid Swift identifier (no hyphens)
            // Convert "video-player-example" to "VideoPlayerExample"
            let app_name = crate_name
                .split('-')
                .map(|s| {
                    let mut chars = s.chars();
                    chars.next().map_or_else(String::new, |first| {
                        first.to_uppercase().chain(chars).collect()
                    })
                })
                .collect::<String>();
            (crate_name.clone(), app_name, crate_name)
        };

        // Get the relative path to the backend from project root (e.g., "apple" or ".water/apple")
        let backend_relative_path = project.backend_relative_path::<Self>();

        let project_path = default_apple_project_path();

        let ctx = TemplateContext {
            app_display_name: manifest.package.name.clone(),
            app_name,
            crate_name: crate_name_for_template,
            bundle_identifier: manifest.package.bundle_identifier.clone(),
            author: String::new(),
            android_backend_path: None,
            use_remote_dev_backend: manifest.waterui_path.is_none(),
            waterui_path: manifest.waterui_path.as_ref().map(PathBuf::from),
            backend_project_path: Some(backend_relative_path),
            android_permissions: Vec::new(),
        };

        templates::apple::scaffold(&project.backend_path::<Self>(), &ctx)
            .await
            .map_err(crate::backend::FailToInitBackend::Io)?;

        Ok(Self {
            project_path,
            scheme,
            branch: None,
            revision: None,
            backend_path: manifest.waterui_path.clone(),
        })
    }
}