waterui_cli/apple/
backend.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    backend::Backend,
7    project::Project,
8    templates::{self, TemplateContext},
9};
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
12// Warn: You cannot use both revision and local_path at the same time.
13/// Configuration for the Apple backend in a `WaterUI` project.
14///
15/// `[backend.apple]` in `Water.toml`
16pub struct AppleBackend {
17    #[serde(
18        default = "default_apple_project_path",
19        skip_serializing_if = "is_default_apple_project_path"
20    )]
21    /// Path to the Apple project within the `WaterUI` project.
22    pub project_path: PathBuf,
23    /// The scheme to use for building the Apple project.
24    pub scheme: String,
25    /// The branch of the Apple backend to use.
26    ///
27    /// You cannot use both branch and revision at the same time.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub branch: Option<String>,
30
31    /// The revision (commit hash or tag) of the Apple backend to use.
32    ///
33    /// You cannot use both revision and branch at the same time.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub revision: Option<String>,
36    /// Local path to the Apple backend for local dev.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub backend_path: Option<String>,
39}
40
41impl AppleBackend {
42    /// Create a new Apple backend configuration with the given scheme.
43    #[must_use]
44    pub fn new(scheme: impl Into<String>) -> Self {
45        Self {
46            project_path: default_apple_project_path(),
47            scheme: scheme.into(),
48            branch: None,
49            revision: None,
50            backend_path: None,
51        }
52    }
53
54    /// Set a custom project path (defaults to "apple").
55    #[must_use]
56    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
57        self.project_path = path.into();
58        self
59    }
60
61    /// Set the local backend path for development.
62    #[must_use]
63    pub fn with_backend_path(mut self, path: impl Into<String>) -> Self {
64        self.backend_path = Some(path.into());
65        self
66    }
67
68    /// Get the path to the Apple project within the `WaterUI` project.
69    #[must_use]
70    pub fn project_path(&self) -> &Path {
71        &self.project_path
72    }
73}
74
75fn default_apple_project_path() -> PathBuf {
76    PathBuf::from("apple")
77}
78
79fn is_default_apple_project_path(s: &Path) -> bool {
80    s == Path::new("apple")
81}
82
83impl Backend for AppleBackend {
84    const DEFAULT_PATH: &'static str = "apple";
85
86    fn path(&self) -> &Path {
87        &self.project_path
88    }
89
90    async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
91        let manifest = project.manifest();
92
93        // For playground projects, use fixed scheme name "WaterUIApp"
94        // For regular projects, scheme name must match the Xcode target name (crate name)
95        let is_playground =
96            manifest.package.package_type == crate::project::PackageType::Playground;
97
98        // For playground projects, use fixed names
99        // For regular projects, derive from crate name
100        let (scheme, app_name, crate_name_for_template) = if is_playground {
101            (
102                "WaterUIApp".to_string(),
103                "WaterUIApp".to_string(),
104                "WaterUIApp".to_string(),
105            )
106        } else {
107            let crate_name = project.crate_name().to_string();
108            // App name for Swift code must be a valid Swift identifier (no hyphens)
109            // Convert "video-player-example" to "VideoPlayerExample"
110            let app_name = crate_name
111                .split('-')
112                .map(|s| {
113                    let mut chars = s.chars();
114                    chars.next().map_or_else(String::new, |first| {
115                        first.to_uppercase().chain(chars).collect()
116                    })
117                })
118                .collect::<String>();
119            (crate_name.clone(), app_name, crate_name)
120        };
121
122        // Get the relative path to the backend from project root (e.g., "apple" or ".water/apple")
123        let backend_relative_path = project.backend_relative_path::<Self>();
124
125        let project_path = default_apple_project_path();
126
127        let ctx = TemplateContext {
128            app_display_name: manifest.package.name.clone(),
129            app_name,
130            crate_name: crate_name_for_template,
131            bundle_identifier: manifest.package.bundle_identifier.clone(),
132            author: String::new(),
133            android_backend_path: None,
134            use_remote_dev_backend: manifest.waterui_path.is_none(),
135            waterui_path: manifest.waterui_path.as_ref().map(PathBuf::from),
136            backend_project_path: Some(backend_relative_path),
137            android_permissions: Vec::new(),
138        };
139
140        templates::apple::scaffold(&project.backend_path::<Self>(), &ctx)
141            .await
142            .map_err(crate::backend::FailToInitBackend::Io)?;
143
144        Ok(Self {
145            project_path,
146            scheme,
147            branch: None,
148            revision: None,
149            backend_path: manifest.waterui_path.clone(),
150        })
151    }
152}