use std::fmt;
use serde_json::{json, Map, Value};
use crate::data;
use crate::enums::input_type;
use crate::redact;
pub mod provider {
pub const AMAZONS3: &str = "amazons3";
pub const AZURE: &str = "azure";
pub const FTP: &str = "ftp";
pub const GDRIVE: &str = "gdrive";
pub const GOOGLECLOUD: &str = "googlecloud";
pub const YOUTUBE: &str = "youtube";
pub const ALL: [&str; 6] = [AMAZONS3, AZURE, FTP, GDRIVE, GOOGLECLOUD, YOUTUBE];
}
#[derive(Clone)]
pub struct CloudInput {
pub source: String,
pub parameters: Map<String, Value>,
pub credentials: Map<String, Value>,
}
impl CloudInput {
pub fn of(source: impl Into<String>) -> Self {
CloudInput {
source: source.into(),
parameters: Map::new(),
credentials: Map::new(),
}
}
pub fn amazon_s3(
bucket: impl Into<String>,
file: impl Into<String>,
accesskeyid: impl Into<String>,
secretaccesskey: impl Into<String>,
) -> Self {
CloudInput {
source: provider::AMAZONS3.to_string(),
parameters: obj([("bucket", bucket.into()), ("file", file.into())]),
credentials: obj([
("accesskeyid", accesskeyid.into()),
("secretaccesskey", secretaccesskey.into()),
]),
}
}
pub fn azure(
container: impl Into<String>,
file: impl Into<String>,
accountname: impl Into<String>,
accountkey: impl Into<String>,
) -> Self {
CloudInput {
source: provider::AZURE.to_string(),
parameters: obj([("container", container.into()), ("file", file.into())]),
credentials: obj([
("accountname", accountname.into()),
("accountkey", accountkey.into()),
]),
}
}
pub fn ftp(
host: impl Into<String>,
file: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
CloudInput {
source: provider::FTP.to_string(),
parameters: obj([("host", host.into()), ("file", file.into())]),
credentials: obj([("username", username.into()), ("password", password.into())]),
}
}
pub fn google_cloud(
projectid: impl Into<String>,
bucket: impl Into<String>,
file: impl Into<String>,
keyfile: impl Into<String>,
) -> Self {
CloudInput {
source: provider::GOOGLECLOUD.to_string(),
parameters: obj([
("projectid", projectid.into()),
("bucket", bucket.into()),
("file", file.into()),
]),
credentials: obj([("keyfile", keyfile.into())]),
}
}
pub fn parameter(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.parameters.insert(key.into(), value.into());
self
}
pub fn credential(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.credentials.insert(key.into(), value.into());
self
}
pub fn to_value(&self) -> Value {
json!({
"type": input_type::CLOUD,
"source": self.source,
"parameters": self.parameters,
"credentials": self.credentials,
})
}
}
impl fmt::Debug for CloudInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CloudInput")
.field("type", &input_type::CLOUD)
.field("source", &self.source)
.field("parameters", &redact::parameters(&self.parameters))
.field("credentials", &redact::MARKER)
.finish()
}
}
impl From<CloudInput> for Value {
fn from(input: CloudInput) -> Value {
input.to_value()
}
}
impl From<&CloudInput> for Value {
fn from(input: &CloudInput) -> Value {
input.to_value()
}
}
#[derive(Clone)]
pub struct OutputTarget {
pub kind: String,
pub parameters: Map<String, Value>,
pub credentials: Map<String, Value>,
pub status: Option<String>,
}
impl OutputTarget {
pub fn of(kind: impl Into<String>) -> Self {
OutputTarget {
kind: kind.into(),
parameters: Map::new(),
credentials: Map::new(),
status: None,
}
}
pub fn new(
kind: impl Into<String>,
parameters: Map<String, Value>,
credentials: Map<String, Value>,
) -> Self {
OutputTarget {
kind: kind.into(),
parameters,
credentials,
status: None,
}
}
pub fn parameter(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.parameters.insert(key.into(), value.into());
self
}
pub fn credential(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.credentials.insert(key.into(), value.into());
self
}
pub fn to_value(&self) -> Value {
json!({
"type": self.kind,
"parameters": self.parameters,
"credentials": self.credentials,
})
}
pub(crate) fn from_value(v: &Value) -> Self {
OutputTarget {
kind: data::string(v.get("type"), ""),
parameters: data::object(v.get("parameters")),
credentials: Map::new(),
status: data::opt_string(v.get("status")),
}
}
}
impl fmt::Debug for OutputTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutputTarget")
.field("type", &self.kind)
.field("parameters", &redact::parameters(&self.parameters))
.field("credentials", &redact::MARKER)
.field("status", &self.status)
.finish()
}
}
fn obj<const N: usize>(pairs: [(&str, String); N]) -> Map<String, Value> {
let mut m = Map::new();
for (k, v) in pairs {
m.insert(k.to_string(), Value::String(v));
}
m
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_vocabulary_in_order() {
assert_eq!(
provider::ALL,
[
"amazons3",
"azure",
"ftp",
"gdrive",
"googlecloud",
"youtube"
]
);
}
#[test]
fn amazon_s3_descriptor_has_flat_lowercase_keys() {
let v = CloudInput::amazon_s3("my-bucket", "in/photo.png", "AKIA_TEST", "SECRET_TEST")
.to_value();
assert_eq!(v["type"], "cloud");
assert_eq!(v["source"], "amazons3");
assert_eq!(
v["parameters"],
json!({"bucket": "my-bucket", "file": "in/photo.png"})
);
assert_eq!(
v["credentials"],
json!({"accesskeyid": "AKIA_TEST", "secretaccesskey": "SECRET_TEST"})
);
}
#[test]
fn per_provider_constructors_carry_keys_verbatim() {
assert_eq!(
CloudInput::azure("c", "f", "n", "k").to_value(),
json!({
"type": "cloud", "source": "azure",
"parameters": {"container": "c", "file": "f"},
"credentials": {"accountname": "n", "accountkey": "k"}
})
);
assert_eq!(
CloudInput::google_cloud("p", "b", "f", "kf").to_value(),
json!({
"type": "cloud", "source": "googlecloud",
"parameters": {"projectid": "p", "bucket": "b", "file": "f"},
"credentials": {"keyfile": "kf"}
})
);
}
#[test]
fn fluent_setters_carry_forward_compat_keys() {
let v = CloudInput::amazon_s3("b", "f", "id", "sec")
.parameter("region", "eu")
.credential("sessiontoken", "t")
.to_value();
assert_eq!(
v["parameters"],
json!({"bucket": "b", "file": "f", "region": "eu"})
);
assert_eq!(
v["credentials"],
json!({"accesskeyid": "id", "secretaccesskey": "sec", "sessiontoken": "t"})
);
}
#[test]
fn output_target_omits_status_on_serialize_but_hydrates_it_on_read() {
let created = OutputTarget::of("ftp")
.parameter("host", "h")
.credential("username", "u");
let v = created.to_value();
assert!(v.get("status").is_none());
assert_eq!(v["type"], "ftp");
let read = OutputTarget::from_value(&json!({
"type": "ftp", "parameters": {"host": "h"}, "credentials": {"x": "y"}, "status": "completed"
}));
assert_eq!(read.kind, "ftp");
assert_eq!(read.status.as_deref(), Some("completed"));
assert!(read.credentials.is_empty());
}
#[test]
fn debug_masks_credentials_and_sensitive_parameters() {
let input = CloudInput::amazon_s3("b", "f", "AKIA", "SUPERSECRET123");
let dbg = format!("{input:?}");
assert!(!dbg.contains("SUPERSECRET123"));
assert!(dbg.contains("[REDACTED]"));
let leaf = CloudInput::of("amazons3")
.parameter("token", "PARAMSECRET")
.parameter("bucket", "b");
let dbg = format!("{leaf:?}");
assert!(!dbg.contains("PARAMSECRET"));
assert!(dbg.contains("[REDACTED]"));
assert!(dbg.contains("bucket"));
}
}