use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Artifact {
Contract,
OpenApi,
AsyncApi,
SocketContract,
}
impl Artifact {
pub(crate) fn key(self) -> &'static str {
match self {
Self::Contract => "contractArtifactPath",
Self::OpenApi => "openapiPath",
Self::AsyncApi => "asyncapiPath",
Self::SocketContract => "socketContractArtifactPath",
}
}
fn alias(self) -> &'static str {
match self {
Self::Contract => "contract_artifact_path",
Self::OpenApi => "openapi_path",
Self::AsyncApi => "asyncapi_path",
Self::SocketContract => "socket_contract_artifact_path",
}
}
pub(crate) fn file(self) -> &'static str {
match self {
Self::Contract => "contract.json",
Self::OpenApi => "openapi.json",
Self::AsyncApi => "asyncapi.json",
Self::SocketContract => "socket-contract.json",
}
}
fn legacy_directory(self) -> &'static str {
match self {
Self::Contract => "contracts",
Self::OpenApi => "openapi",
Self::AsyncApi => "asyncapi",
Self::SocketContract => "socket-contracts",
}
}
}
pub(crate) fn legacy(name: &str, artifact: Artifact) -> String {
format!(
".forklaunch/{}/{name}/{}",
artifact.legacy_directory(),
artifact.file()
)
}
fn text<'a>(metadata: &'a toml::Table, key: &str) -> Option<&'a str> {
metadata.get(key).and_then(toml::Value::as_str)
}
fn explicit(metadata: &toml::Table, artifact: Artifact) -> Option<&str> {
text(metadata, artifact.key()).or_else(|| text(metadata, artifact.alias()))
}
pub(crate) fn sdk_directory(metadata: &toml::Table) -> Option<String> {
if text(metadata, "sdkLayout") != Some("project-local-v1") {
return None;
}
let project = text(metadata, "cratePath")
.or_else(|| text(metadata, "crate_path"))
.or_else(|| text(metadata, "staticPath"))
.or_else(|| text(metadata, "static_path"))?
.trim_end_matches('/');
Some(if text(metadata, "language") == Some("rust") {
format!("{project}/sdk/typescript")
} else {
format!("{project}/sdk")
})
}
pub(crate) fn default_path(
name: &str,
metadata: Option<&toml::Table>,
artifact: Artifact,
) -> String {
metadata.and_then(sdk_directory).map_or_else(
|| legacy(name, artifact),
|directory| format!("{directory}/{}", artifact.file()),
)
}
pub(crate) fn path(name: &str, metadata: Option<&toml::Table>, artifact: Artifact) -> String {
metadata
.and_then(|metadata| explicit(metadata, artifact))
.map_or_else(|| default_path(name, metadata, artifact), str::to_owned)
}
pub(crate) fn project_path(project: &toml::Value, artifact: Artifact) -> Option<String> {
let name = project.get("name").and_then(toml::Value::as_str)?;
Some(path(
name,
project.get("metadata").and_then(toml::Value::as_table),
artifact,
))
}
pub(crate) fn manifest_path(manifest: &toml::Value, name: &str, artifact: Artifact) -> String {
let metadata = manifest
.get("projects")
.and_then(toml::Value::as_array)
.and_then(|projects| {
projects
.iter()
.find(|project| project.get("name").and_then(toml::Value::as_str) == Some(name))
})
.and_then(|project| project.get("metadata"))
.and_then(toml::Value::as_table);
path(name, metadata, artifact)
}
pub(crate) fn application_path(root: &Path, name: &str, artifact: Artifact) -> String {
std::fs::read_to_string(root.join(".forklaunch/manifest.toml"))
.ok()
.and_then(|content| toml::from_str::<toml::Value>(&content).ok())
.map_or_else(
|| legacy(name, artifact),
|manifest| manifest_path(&manifest, name, artifact),
)
}
pub(crate) fn socket_registry_marker(name: &str, metadata: Option<&toml::Table>) -> PathBuf {
match metadata.and_then(sdk_directory) {
Some(directory) => PathBuf::from(directory).join("socket-registry-export.json"),
None => PathBuf::from(format!(
".forklaunch/socket-contracts/{name}/registry-export.json"
)),
}
}
pub(crate) fn socket_receipt(name: &str, metadata: Option<&toml::Table>, target: &str) -> PathBuf {
match metadata.and_then(sdk_directory) {
Some(directory) => PathBuf::from(directory).join(format!("socket-sdk-{target}.json")),
None => PathBuf::from(format!(
".forklaunch/socket-sdk-outputs/{name}/{target}.json"
)),
}
}
pub(crate) fn application_metadata(root: &Path, name: &str) -> Option<toml::Table> {
let manifest: toml::Value =
toml::from_str(&std::fs::read_to_string(root.join(".forklaunch/manifest.toml")).ok()?)
.ok()?;
manifest
.get("projects")?
.as_array()?
.iter()
.find(|project| project.get("name").and_then(toml::Value::as_str) == Some(name))?
.get("metadata")?
.as_table()
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
fn table(source: &str) -> toml::Table {
toml::from_str(source).unwrap()
}
#[test]
fn service_owned_sdks_hold_every_artifact() {
let rust = table("sdkLayout='project-local-v1'\nlanguage='rust'\ncratePath='src/notes'\n");
let typescript = table(
"sdkLayout='project-local-v1'\nlanguage='typescript'\nstaticPath='modules/legacy'\n",
);
for (artifact, file) in [
(Artifact::Contract, "contract.json"),
(Artifact::OpenApi, "openapi.json"),
(Artifact::AsyncApi, "asyncapi.json"),
(Artifact::SocketContract, "socket-contract.json"),
] {
assert_eq!(
path("notes", Some(&rust), artifact),
format!("src/notes/sdk/typescript/{file}")
);
assert_eq!(
path("legacy", Some(&typescript), artifact),
format!("modules/legacy/sdk/{file}")
);
}
}
#[test]
fn explicit_and_registered_paths_win_and_unowned_projects_keep_legacy_paths() {
let custom = table(
"asyncapiPath='docs/events.json'\ncontractArtifactPath='src/client-sdk/typescript/notes/contract.json'\n",
);
assert_eq!(
path("notes", Some(&custom), Artifact::AsyncApi),
"docs/events.json"
);
assert_eq!(
path("notes", Some(&custom), Artifact::SocketContract),
".forklaunch/socket-contracts/notes/socket-contract.json"
);
assert_eq!(
path("notes", None, Artifact::OpenApi),
".forklaunch/openapi/notes/openapi.json"
);
assert_eq!(
socket_registry_marker("notes", None),
Path::new(".forklaunch/socket-contracts/notes/registry-export.json")
);
let rust = table("sdkLayout='project-local-v1'\nlanguage='rust'\ncratePath='src/notes'\n");
assert_eq!(
socket_receipt("notes", Some(&rust), "rust"),
Path::new("src/notes/sdk/typescript/socket-sdk-rust.json")
);
}
}