Skip to main content

objectiveai_sdk/cli/command/
path_ref.rs

1//! Typed paths shared across cli leaves.
2//!
3//! Every cli leaf that takes a docker-style `key=value,key=value` ref
4//! parses it at `TryFrom<Args>` time so each leaf's `Request` carries
5//! the pre-parsed form. There are **two distinct parsers** because
6//! different leaves accept different sets of keys:
7//!
8//! - [`FromStr for crate::RemotePathCommitOptional`] — for leaves
9//!   that take a *path only* (e.g. `config/<domain>/favorites/add`).
10//!   Accepts `remote=<github|filesystem|mock>` plus the matching
11//!   owner/repository/name/commit fields. Any other key (including
12//!   `favorite=`) is an `unknown key` error.
13//! - [`FromStr for RemotePathCommitOptionalOrFavorite`] — for leaves
14//!   that take *either* a favorite name *or* a path (e.g. the `*get`
15//!   leaves). A string of the exact form `favorite=<name>` becomes
16//!   `Favorite(name)`; otherwise the string is parsed by the
17//!   remote-only parser and lifted into `Resolved(path)`.
18//!
19//! Mixing `favorite=` with remote keys is an error in the
20//! favorite-or-path parser.
21
22use std::str::FromStr;
23
24/// Wire-level source enum: which remote backend a path references.
25/// Same shape as `crate::Remote` but lives under `cli::command` so it
26/// can be `clap::ValueEnum` without dragging clap into the SDK's
27/// non-cli surface.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
29pub enum Remote {
30    Github,
31    Filesystem,
32    Mock,
33}
34
35impl Remote {
36    /// Combine `(remote, owner?, repository?, name?, commit?)` into a
37    /// concrete `RemotePathCommitOptional`. Returns `None` if a
38    /// required field is missing for the chosen backend.
39    pub fn into_path(
40        self,
41        owner: Option<String>,
42        repository: Option<String>,
43        name: Option<String>,
44        commit: Option<String>,
45    ) -> Option<crate::RemotePathCommitOptional> {
46        match self {
47            Remote::Github => Some(crate::RemotePathCommitOptional::Github {
48                owner: owner?,
49                repository: repository?,
50                commit,
51            }),
52            Remote::Filesystem => Some(crate::RemotePathCommitOptional::Filesystem {
53                owner: owner?,
54                repository: repository?,
55                commit,
56            }),
57            Remote::Mock => Some(crate::RemotePathCommitOptional::Mock { name: name? }),
58        }
59    }
60
61    fn parse_keyword(s: &str) -> Result<Self, String> {
62        match s {
63            "github" => Ok(Self::Github),
64            "filesystem" => Ok(Self::Filesystem),
65            "mock" => Ok(Self::Mock),
66            other => Err(format!(
67                "unknown remote: {other} (expected github, filesystem, or mock)"
68            )),
69        }
70    }
71}
72
73/// Tokenize a `key=value,key=value` string into a `Vec<(key, value)>`,
74/// trimming whitespace around each token. Returns an error if any
75/// pair lacks an `=`.
76pub(crate) fn tokenize(s: &str) -> Result<Vec<(&str, &str)>, String> {
77    s.split(',')
78        .map(|pair| {
79            pair.split_once('=')
80                .map(|(k, v)| (k.trim(), v.trim()))
81                .ok_or_else(|| format!("expected key=value, got: {pair}"))
82        })
83        .collect()
84}
85
86impl FromStr for crate::RemotePathCommitOptional {
87    type Err = String;
88
89    /// Parse a remote-path ref. Accepted keys: `remote`, `owner`,
90    /// `repository`, `name`, `commit`. Anything else (including
91    /// `favorite=`) is an unknown-key error.
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        let mut remote: Option<Remote> = None;
94        let mut owner: Option<String> = None;
95        let mut repository: Option<String> = None;
96        let mut name: Option<String> = None;
97        let mut commit: Option<String> = None;
98        for (k, v) in tokenize(s)? {
99            match k {
100                "remote" => remote = Some(Remote::parse_keyword(v)?),
101                "owner" => owner = Some(v.to_string()),
102                "repository" => repository = Some(v.to_string()),
103                "name" => name = Some(v.to_string()),
104                "commit" => commit = Some(v.to_string()),
105                other => return Err(format!("unknown key: {other}")),
106            }
107        }
108        let remote = remote.ok_or_else(|| "remote is required".to_string())?;
109        remote
110            .into_path(owner, repository, name, commit)
111            .ok_or_else(|| {
112                "owner and repository are required for github/filesystem, name for mock"
113                    .to_string()
114            })
115    }
116}
117
118/// Either a fully resolved `RemotePathCommitOptional` or a favorite
119/// name that the cli host resolves at handler time. Parsed from CLI
120/// input via [`FromStr`] (docker-style `key=value,...`); after that
121/// the typed enum is carried around in-process — `Request`s are never
122/// actually serialized at runtime. The `serde` + `schemars::JsonSchema`
123/// derives exist solely so the SDK can generate the JSON Schema for
124/// this type (untagged so the schema describes either the path's
125/// object form or a bare string).
126#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
127#[serde(untagged)]
128#[schemars(rename = "cli.command.RemotePathCommitOptionalOrFavorite")]
129pub enum RemotePathCommitOptionalOrFavorite {
130    #[schemars(title = "Resolved")]
131    Resolved(crate::RemotePathCommitOptional),
132    #[schemars(title = "Favorite")]
133    Favorite(String),
134}
135
136impl RemotePathCommitOptionalOrFavorite {
137    /// Reconstruct a docker-style `key=value,...` string from this
138    /// typed value. Used by `into_command` to round-trip the
139    /// `Request` back through argv for subprocess dispatch.
140    pub fn into_arg_string(&self) -> String {
141        match self {
142            Self::Favorite(name) => format!("favorite={name}"),
143            Self::Resolved(path) => remote_path_to_arg_string(path),
144        }
145    }
146}
147
148impl FromStr for RemotePathCommitOptionalOrFavorite {
149    type Err = String;
150
151    /// Parse a favorite-or-path ref. Two accepted forms:
152    /// - `favorite=<name>` on its own — combining `favorite=` with
153    ///   any other key is an error.
154    /// - Anything else — delegated to
155    ///   [`FromStr for crate::RemotePathCommitOptional`].
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        let pairs = tokenize(s)?;
158        if let Some((_, value)) = pairs.iter().find(|(k, _)| *k == "favorite") {
159            if pairs.len() > 1 {
160                return Err(
161                    "favorite= cannot be combined with other keys".to_string(),
162                );
163            }
164            return Ok(Self::Favorite((*value).to_string()));
165        }
166        s.parse::<crate::RemotePathCommitOptional>().map(Self::Resolved)
167    }
168}
169
170/// Serialize a `RemotePathCommitOptional` back into its docker-style
171/// `key=value,...` form for round-tripping through argv.
172pub fn remote_path_to_arg_string(path: &crate::RemotePathCommitOptional) -> String {
173    match path {
174        crate::RemotePathCommitOptional::Github {
175            owner,
176            repository,
177            commit,
178        } => {
179            let mut s = format!("remote=github,owner={owner},repository={repository}");
180            if let Some(c) = commit {
181                s.push_str(&format!(",commit={c}"));
182            }
183            s
184        }
185        crate::RemotePathCommitOptional::Filesystem {
186            owner,
187            repository,
188            commit,
189        } => {
190            let mut s = format!("remote=filesystem,owner={owner},repository={repository}");
191            if let Some(c) = commit {
192                s.push_str(&format!(",commit={c}"));
193            }
194            s
195        }
196        crate::RemotePathCommitOptional::Mock { name } => format!("remote=mock,name={name}"),
197    }
198}