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}