hodei_hrn/
api.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HrnError {
7    #[error("Formato de HRN inválido: Se esperaban 6 partes separadas por ':'")]
8    InvalidFormat,
9    #[error("Prefijo de HRN inválido: debe empezar con 'hrn:'")]
10    InvalidPrefix,
11    #[error("Parte del recurso inválida: debe tener el formato 'tipo/id'")]
12    InvalidResourcePart,
13    #[error("Parte requerida del HRN no especificada: {0}")]
14    MissingPart(String),
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct Hrn {
19    pub partition: String,
20    pub service: String,
21    pub region: String,
22    pub tenant_id: String,
23    pub resource_type: String,
24    pub resource_id: String,
25}
26
27#[derive(Default)]
28pub struct HrnBuilder {
29    partition: Option<String>,
30    service: Option<String>,
31    region: Option<String>,
32    tenant_id: Option<String>,
33    resource_type: Option<String>,
34    resource_id: Option<String>,
35}
36
37impl HrnBuilder {
38    pub fn new() -> Self {
39        Self {
40            partition: Some("hodei".to_string()),
41            region: Some("global".to_string()),
42            ..Default::default()
43        }
44    }
45
46    pub fn service(mut self, service: &str) -> Self {
47        self.service = Some(service.to_string());
48        self
49    }
50
51    pub fn tenant_id(mut self, tenant_id: &str) -> Self {
52        self.tenant_id = Some(tenant_id.to_string());
53        self
54    }
55
56    pub fn resource(mut self, resource_path: &str) -> Result<Self, HrnError> {
57        if let Some((res_type, res_id)) = resource_path.split_once('/') {
58            self.resource_type = Some(res_type.to_string());
59            self.resource_id = Some(res_id.to_string());
60            Ok(self)
61        } else {
62            Err(HrnError::InvalidResourcePart)
63        }
64    }
65
66    pub fn build(self) -> Result<Hrn, HrnError> {
67        Ok(Hrn {
68            partition: self.partition.ok_or_else(|| HrnError::MissingPart("partition".into()))?,
69            service: self.service.ok_or_else(|| HrnError::MissingPart("service".into()))?,
70            region: self.region.ok_or_else(|| HrnError::MissingPart("region".into()))?,
71            tenant_id: self.tenant_id.ok_or_else(|| HrnError::MissingPart("tenant_id".into()))?,
72            resource_type: self.resource_type.ok_or_else(|| HrnError::MissingPart("resource_type".into()))?,
73            resource_id: self.resource_id.ok_or_else(|| HrnError::MissingPart("resource_id".into()))?,
74        })
75    }
76}
77
78impl Hrn {
79    pub fn builder() -> HrnBuilder {
80        HrnBuilder::new()
81    }
82}
83
84impl fmt::Display for Hrn {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(
87            f,
88            "hrn:{}:{}:{}:{}:{}/{}",
89            self.partition, self.service, self.region, self.tenant_id, self.resource_type, self.resource_id
90        )
91    }
92}
93
94impl FromStr for Hrn {
95    type Err = HrnError;
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        if !s.starts_with("hrn:") { return Err(HrnError::InvalidPrefix); }
98        let parts: Vec<&str> = s[4..].split(':').collect();
99        if parts.len() != 5 { return Err(HrnError::InvalidFormat); }
100        let resource_parts: Vec<&str> = parts[4].split('/').collect();
101        if resource_parts.len() != 2 { return Err(HrnError::InvalidResourcePart); }
102        Ok(Hrn {
103            partition: parts[0].to_string(),
104            service: parts[1].to_string(),
105            region: parts[2].to_string(),
106            tenant_id: parts[3].to_string(),
107            resource_type: resource_parts[0].to_string(),
108            resource_id: resource_parts[1].to_string(),
109        })
110    }
111}
112
113#[cfg(feature = "sqlx")]
114impl sqlx::Type<sqlx::Postgres> for Hrn {
115    fn type_info() -> sqlx::postgres::PgTypeInfo {
116        sqlx::postgres::PgTypeInfo::with_name("TEXT")
117    }
118}
119
120#[cfg(feature = "sqlx")]
121impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Hrn {
122    fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
123        let hrn_string = self.to_string();
124        <String as sqlx::Encode<sqlx::Postgres>>::encode_by_ref(&hrn_string, buf)
125    }
126}
127
128#[cfg(feature = "sqlx")]
129impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Hrn {
130    fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
131        let str_value = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
132        Ok(str_value.parse()?)
133    }
134}