ferripfs-config 0.1.0

IPFS node configuration types, compatible with Kubo config format
Documentation
// Ported from: kubo/config/types.go
// Kubo version: v0.39.0
// Original: https://github.com/ipfs/kubo/blob/v0.39.0/config/types.go
//
// Original work: Copyright (c) Protocol Labs, Inc.
// Port: Copyright (c) 2026 ferripfs contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Custom types for configuration, matching Kubo's types.go

use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Flexible string type that accepts both a single string and array of strings
#[derive(Debug, Clone, Default)]
pub struct Strings(pub Vec<String>);

impl Serialize for Strings {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        if self.0.len() == 1 {
            self.0[0].serialize(serializer)
        } else {
            self.0.serialize(serializer)
        }
    }
}

impl<'de> Deserialize<'de> for Strings {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum StringOrVec {
            String(String),
            Vec(Vec<String>),
        }

        match StringOrVec::deserialize(deserializer)? {
            StringOrVec::String(s) => Ok(Strings(vec![s])),
            StringOrVec::Vec(v) => Ok(Strings(v)),
        }
    }
}

impl Strings {
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn first(&self) -> Option<&String> {
        self.0.first()
    }

    pub fn iter(&self) -> impl Iterator<Item = &String> {
        self.0.iter()
    }
}

/// Ternary flag: can be true, false, or default (null/missing)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Flag {
    False,
    #[default]
    Default,
    True,
}

impl Flag {
    /// Get the boolean value with a default
    pub fn with_default(self, default_value: bool) -> bool {
        match self {
            Flag::False => false,
            Flag::Default => default_value,
            Flag::True => true,
        }
    }
}

impl Serialize for Flag {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Flag::False => serializer.serialize_bool(false),
            Flag::Default => serializer.serialize_none(),
            Flag::True => serializer.serialize_bool(true),
        }
    }
}

impl<'de> Deserialize<'de> for Flag {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let opt: Option<bool> = Option::deserialize(deserializer)?;
        Ok(match opt {
            Some(true) => Flag::True,
            Some(false) => Flag::False,
            None => Flag::Default,
        })
    }
}

/// Priority with default (null) and disabled (false) states
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Priority {
    #[default]
    Default,
    Disabled,
    Value(i64),
}

impl Priority {
    pub fn with_default(self, default_value: i64) -> Option<i64> {
        match self {
            Priority::Default => Some(default_value),
            Priority::Disabled => None,
            Priority::Value(v) => Some(v),
        }
    }
}

impl Serialize for Priority {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Priority::Default => serializer.serialize_none(),
            Priority::Disabled => serializer.serialize_bool(false),
            Priority::Value(v) => serializer.serialize_i64(*v),
        }
    }
}

impl<'de> Deserialize<'de> for Priority {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum PriorityValue {
            Bool(bool),
            Int(i64),
        }

        let opt: Option<PriorityValue> = Option::deserialize(deserializer)?;
        Ok(match opt {
            None => Priority::Default,
            Some(PriorityValue::Bool(false)) => Priority::Disabled,
            Some(PriorityValue::Bool(true)) => Priority::Value(1),
            Some(PriorityValue::Int(v)) => {
                if v <= 0 {
                    Priority::Disabled
                } else {
                    Priority::Value(v)
                }
            }
        })
    }
}

/// Optional duration, stored as a string like "1h", "30m", "10s"
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OptionalDuration(
    #[serde(default, skip_serializing_if = "Option::is_none")] pub Option<String>,
);

impl OptionalDuration {
    pub fn is_none(&self) -> bool {
        self.0.is_none()
    }

    pub fn as_str(&self) -> Option<&str> {
        self.0.as_deref()
    }
}

/// Optional integer
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OptionalInteger(
    #[serde(default, skip_serializing_if = "Option::is_none")] pub Option<i64>,
);

impl OptionalInteger {
    pub fn is_none(&self) -> bool {
        self.0.is_none()
    }

    pub fn value(&self) -> Option<i64> {
        self.0
    }

    pub fn with_default(&self, default: i64) -> i64 {
        self.0.unwrap_or(default)
    }
}

/// Optional string
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OptionalString(
    #[serde(default, skip_serializing_if = "Option::is_none")] pub Option<String>,
);

impl OptionalString {
    pub fn is_none(&self) -> bool {
        self.0.is_none()
    }

    pub fn as_str(&self) -> Option<&str> {
        self.0.as_deref()
    }

    pub fn with_default<'a>(&'a self, default: &'a str) -> &'a str {
        self.0.as_deref().unwrap_or(default)
    }
}

/// Optional bytes size, stored as a string like "10GB", "1MB"
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OptionalBytes(
    #[serde(default, skip_serializing_if = "Option::is_none")] pub Option<String>,
);

impl OptionalBytes {
    pub fn is_none(&self) -> bool {
        self.0.is_none()
    }

    pub fn as_str(&self) -> Option<&str> {
        self.0.as_deref()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_strings_single() {
        let s: Strings = serde_json::from_str(r#""/ip4/0.0.0.0/tcp/4001""#).unwrap();
        assert_eq!(s.0.len(), 1);
        assert_eq!(s.0[0], "/ip4/0.0.0.0/tcp/4001");
    }

    #[test]
    fn test_strings_array() {
        let s: Strings =
            serde_json::from_str(r#"["/ip4/0.0.0.0/tcp/4001", "/ip6/::/tcp/4001"]"#).unwrap();
        assert_eq!(s.0.len(), 2);
    }

    #[test]
    fn test_flag() {
        let f: Flag = serde_json::from_str("true").unwrap();
        assert_eq!(f, Flag::True);

        let f: Flag = serde_json::from_str("false").unwrap();
        assert_eq!(f, Flag::False);

        let f: Flag = serde_json::from_str("null").unwrap();
        assert_eq!(f, Flag::Default);
    }

    #[test]
    fn test_priority() {
        let p: Priority = serde_json::from_str("100").unwrap();
        assert_eq!(p, Priority::Value(100));

        let p: Priority = serde_json::from_str("false").unwrap();
        assert_eq!(p, Priority::Disabled);

        let p: Priority = serde_json::from_str("null").unwrap();
        assert_eq!(p, Priority::Default);
    }
}