alien_core/resources/
aws_open_search.rs1use crate::error::{ErrorData, Result};
2use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef};
3use crate::ResourceType;
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::fmt::Debug;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
49#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51#[builder(start_fn = new)]
52pub struct AwsOpenSearch {
53 #[builder(start_fn)]
56 pub id: String,
57 #[builder(default)]
60 #[serde(default)]
61 pub collection_type: AwsOpenSearchCollectionType,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
69#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70#[serde(rename_all = "camelCase")]
71pub enum AwsOpenSearchCollectionType {
72 #[default]
74 Search,
75 VectorSearch,
77}
78
79impl AwsOpenSearch {
80 pub const RESOURCE_TYPE: ResourceType =
85 ResourceType::from_static("experimental/aws-opensearch");
86
87 pub fn id(&self) -> &str {
89 &self.id
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
100#[serde(rename_all = "camelCase")]
101pub struct AwsOpenSearchOutputs {
102 pub endpoint: String,
105 pub collection_arn: String,
107}
108
109impl ResourceOutputsDefinition for AwsOpenSearchOutputs {
110 fn get_resource_type(&self) -> ResourceType {
111 AwsOpenSearch::RESOURCE_TYPE.clone()
112 }
113
114 fn as_any(&self) -> &dyn Any {
115 self
116 }
117
118 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
119 Box::new(self.clone())
120 }
121
122 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
123 other.as_any().downcast_ref::<AwsOpenSearchOutputs>() == Some(self)
124 }
125
126 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
127 serde_json::to_value(self)
128 }
129}
130
131impl ResourceDefinition for AwsOpenSearch {
132 fn get_resource_type(&self) -> ResourceType {
133 Self::RESOURCE_TYPE
134 }
135
136 fn id(&self) -> &str {
137 &self.id
138 }
139
140 fn get_dependencies(&self) -> Vec<ResourceRef> {
141 Vec::new()
142 }
143
144 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
145 let new_search = new_config
146 .as_any()
147 .downcast_ref::<AwsOpenSearch>()
148 .ok_or_else(|| {
149 AlienError::new(ErrorData::UnexpectedResourceType {
150 resource_id: self.id.clone(),
151 expected: Self::RESOURCE_TYPE,
152 actual: new_config.get_resource_type(),
153 })
154 })?;
155
156 if self.id != new_search.id {
157 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
158 resource_id: self.id.clone(),
159 reason: "the 'id' field is immutable".to_string(),
160 }));
161 }
162
163 if self.collection_type != new_search.collection_type {
166 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
167 resource_id: self.id.clone(),
168 reason: "the 'collectionType' field is immutable once the resource exists"
169 .to_string(),
170 }));
171 }
172
173 Ok(())
174 }
175
176 fn as_any(&self) -> &dyn Any {
177 self
178 }
179
180 fn as_any_mut(&mut self) -> &mut dyn Any {
181 self
182 }
183
184 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
185 Box::new(self.clone())
186 }
187
188 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
189 other.as_any().downcast_ref::<AwsOpenSearch>() == Some(self)
190 }
191
192 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
193 serde_json::to_value(self)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn builder_defaults_to_search() {
203 let search = AwsOpenSearch::new("search".to_string()).build();
204 assert_eq!(search.id, "search");
205 assert_eq!(search.collection_type, AwsOpenSearchCollectionType::Search);
206 }
207
208 #[test]
209 fn resource_type_uses_experimental_namespace() {
210 assert_eq!(
211 AwsOpenSearch::RESOURCE_TYPE.as_ref(),
212 "experimental/aws-opensearch"
213 );
214 }
215
216 #[test]
217 fn validate_update_rejects_id_change() {
218 let original = AwsOpenSearch::new("search".to_string()).build();
219 let renamed = AwsOpenSearch::new("other".to_string()).build();
220 let err = original
221 .validate_update(&renamed)
222 .expect_err("changing the id must be rejected");
223 assert!(err.to_string().contains("'id' field is immutable"));
224 }
225
226 #[test]
227 fn validate_update_rejects_collection_type_change() {
228 let search = AwsOpenSearch::new("search".to_string()).build();
229 let vector = AwsOpenSearch::new("search".to_string())
230 .collection_type(AwsOpenSearchCollectionType::VectorSearch)
231 .build();
232
233 let err = search
234 .validate_update(&vector)
235 .expect_err("changing the collection type must be rejected");
236 assert!(err
237 .to_string()
238 .contains("'collectionType' field is immutable"));
239 assert!(vector.validate_update(&vector).is_ok());
241 }
242
243 #[test]
244 fn serializes_with_camel_case_and_roundtrips() {
245 let search = AwsOpenSearch::new("vectors".to_string())
246 .collection_type(AwsOpenSearchCollectionType::VectorSearch)
247 .build();
248 let json = serde_json::to_value(&search).unwrap();
249 assert_eq!(json["collectionType"], "vectorSearch");
250
251 let roundtrip: AwsOpenSearch = serde_json::from_value(json).unwrap();
252 assert_eq!(search, roundtrip);
253 }
254
255 #[test]
256 fn outputs_roundtrip() {
257 let outputs = AwsOpenSearchOutputs {
258 endpoint: "https://abc123.aoss.us-east-1.on.aws".to_string(),
259 collection_arn: "arn:aws:aoss:us-east-1:123456789012:collection/abc123".to_string(),
260 };
261 let json = serde_json::to_string(&outputs).unwrap();
262 let deserialized: AwsOpenSearchOutputs = serde_json::from_str(&json).unwrap();
263 assert_eq!(outputs, deserialized);
264 }
265}