orcid 0.2.1

A wrapper around the ORCID API
Documentation
use crate::date::Date;
use crate::organization::Organization;
use serde_json;

#[derive(Debug, Clone)]
pub struct Membership {
    organization: Option<Organization>,
    department_name: Option<String>,
    role_title: Option<String>,
    start_date: Option<Date>,
    end_date: Option<Date>,
    external_ids: Vec<(String, String)>,
    url: Option<String>,
}

impl Membership {
    pub fn new_from_json(j: &serde_json::Value) -> Self {
        let membership_summary = &j["membership-summary"][0];

        let external_ids =
            if let Some(ext_ids) = membership_summary["external-ids"]["external-id"].as_array() {
                ext_ids
                    .iter()
                    .filter_map(|id| {
                        match (
                            id["external-id-type"].as_str(),
                            id["external-id-value"].as_str(),
                        ) {
                            (Some(id_type), Some(id_value)) => {
                                Some((id_type.to_string(), id_value.to_string()))
                            }
                            _ => None,
                        }
                    })
                    .collect()
            } else {
                vec![]
            };

        Self {
            organization: if membership_summary["organization"].is_object() {
                Some(Organization::new_from_json(
                    &membership_summary["organization"],
                ))
            } else {
                None
            },
            department_name: membership_summary["department-name"]
                .as_str()
                .map(|s| s.to_string()),
            role_title: membership_summary["role-title"]
                .as_str()
                .map(|s| s.to_string()),
            start_date: if membership_summary["start-date"].is_object() {
                Some(Date::new_from_json(&membership_summary["start-date"]))
            } else {
                None
            },
            end_date: if membership_summary["end-date"].is_object() {
                Some(Date::new_from_json(&membership_summary["end-date"]))
            } else {
                None
            },
            external_ids,
            url: membership_summary["url"]["value"]
                .as_str()
                .map(|s| s.to_string()),
        }
    }

    // Getter methods
    pub fn organization(&self) -> Option<&Organization> {
        self.organization.as_ref()
    }

    pub fn department_name(&self) -> Option<&String> {
        self.department_name.as_ref()
    }

    pub fn role_title(&self) -> Option<&String> {
        self.role_title.as_ref()
    }

    pub fn start_date(&self) -> Option<&Date> {
        self.start_date.as_ref()
    }

    pub fn end_date(&self) -> Option<&Date> {
        self.end_date.as_ref()
    }

    pub fn external_ids(&self) -> &Vec<(String, String)> {
        &self.external_ids
    }

    pub fn url(&self) -> Option<&String> {
        self.url.as_ref()
    }
}

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

    #[test]
    fn test_new_from_json_complete() {
        let j = json!({
            "membership-summary": [{
                "organization": {
                    "name": "Professional Society",
                    "address": {
                        "city": "London",
                        "region": "England",
                        "country": "GB"
                    }
                },
                "department-name": "Computer Science Division",
                "role-title": "Fellow",
                "start-date": {
                    "year": { "value": 2020 },
                    "month": { "value": 6 },
                    "day": { "value": 1 }
                },
                "end-date": {
                    "year": { "value": 2025 },
                    "month": { "value": 5 },
                    "day": { "value": 31 }
                },
                "external-ids": {
                    "external-id": [{
                        "external-id-type": "membership-id",
                        "external-id-value": "MEM-2020-12345"
                    }]
                },
                "url": {
                    "value": "https://example.org/members/12345"
                }
            }]
        });

        let membership = Membership::new_from_json(&j);

        assert!(membership.organization().is_some());
        assert_eq!(
            membership.organization().unwrap().name(),
            Some(&"Professional Society".to_string())
        );
        assert_eq!(
            membership.department_name(),
            Some(&"Computer Science Division".to_string())
        );
        assert_eq!(membership.role_title(), Some(&"Fellow".to_string()));
        assert!(membership.start_date().is_some());
        assert!(membership.end_date().is_some());
        assert_eq!(membership.external_ids().len(), 1);
        assert_eq!(
            membership.external_ids()[0],
            ("membership-id".to_string(), "MEM-2020-12345".to_string())
        );
        assert_eq!(
            membership.url(),
            Some(&"https://example.org/members/12345".to_string())
        );
    }

    #[test]
    fn test_new_from_json_minimal() {
        let j = json!({
            "membership-summary": [{
                "organization": {
                    "name": "Basic Society"
                }
            }]
        });

        let membership = Membership::new_from_json(&j);

        assert!(membership.organization().is_some());
        assert_eq!(membership.department_name(), None);
        assert_eq!(membership.role_title(), None);
        assert!(membership.start_date().is_none());
        assert!(membership.end_date().is_none());
        assert_eq!(membership.external_ids().len(), 0);
        assert_eq!(membership.url(), None);
    }

    #[test]
    fn test_new_from_json_empty() {
        let j = json!({
            "membership-summary": [{}]
        });

        let membership = Membership::new_from_json(&j);

        assert!(membership.organization().is_none());
        assert_eq!(membership.department_name(), None);
        assert_eq!(membership.role_title(), None);
        assert!(membership.start_date().is_none());
        assert!(membership.end_date().is_none());
        assert_eq!(membership.external_ids().len(), 0);
        assert_eq!(membership.url(), None);
    }

    #[test]
    fn test_new_from_json_multiple_external_ids() {
        let j = json!({
            "membership-summary": [{
                "organization": {
                    "name": "Multi-ID Society"
                },
                "external-ids": {
                    "external-id": [
                        {
                            "external-id-type": "membership-id",
                            "external-id-value": "MEM-123"
                        },
                        {
                            "external-id-type": "legacy-id",
                            "external-id-value": "OLD-456"
                        },
                        {
                            "external-id-type": "doi",
                            "external-id-value": "10.1234/membership"
                        }
                    ]
                }
            }]
        });

        let membership = Membership::new_from_json(&j);

        assert_eq!(membership.external_ids().len(), 3);
        assert_eq!(
            membership.external_ids()[0],
            ("membership-id".to_string(), "MEM-123".to_string())
        );
        assert_eq!(
            membership.external_ids()[1],
            ("legacy-id".to_string(), "OLD-456".to_string())
        );
        assert_eq!(
            membership.external_ids()[2],
            ("doi".to_string(), "10.1234/membership".to_string())
        );
    }

    #[test]
    fn test_debug_trait() {
        let membership = Membership {
            organization: None,
            department_name: Some("Test Dept".to_string()),
            role_title: Some("Member".to_string()),
            start_date: None,
            end_date: None,
            external_ids: vec![],
            url: None,
        };

        let debug_str = format!("{:?}", membership);
        assert!(debug_str.contains("Membership"));
        assert!(debug_str.contains("Test Dept"));
        assert!(debug_str.contains("Member"));
    }

    #[test]
    fn test_clone_trait() {
        let membership = Membership {
            organization: None,
            department_name: Some("Engineering".to_string()),
            role_title: Some("Senior Member".to_string()),
            start_date: None,
            end_date: None,
            external_ids: vec![("id".to_string(), "123".to_string())],
            url: Some("https://example.com".to_string()),
        };

        let cloned = membership.clone();
        assert_eq!(cloned.department_name(), membership.department_name());
        assert_eq!(cloned.role_title(), membership.role_title());
        assert_eq!(cloned.external_ids(), membership.external_ids());
        assert_eq!(cloned.url(), membership.url());
    }
}