update coordinator and add end to end tests

This commit is contained in:
Timur Gordon
2025-11-19 10:34:28 +01:00
parent 7675dc2150
commit 8c33c73b3c
8 changed files with 830 additions and 10 deletions

View File

@@ -200,3 +200,213 @@ fn is_offsetdatetime_type(ty: &Type) -> bool {
}
false
}
/// Derive macro for generating CRUD client methods for Osiris models
///
/// This macro generates async CRUD methods (create, get, update, delete, list) for a model,
/// plus any custom methods defined on the model.
///
/// # Example
///
/// ```rust
/// #[derive(OsirisModel)]
/// #[osiris(
/// collection = "calendar_events",
/// id_field = "event_id",
/// methods = ["reschedule", "cancel"]
/// )]
/// pub struct CalendarEvent {
/// pub event_id: String,
/// pub title: String,
/// pub start_time: i64,
/// // ...
/// }
/// ```
///
/// This generates methods on OsirisClient:
/// - `create_calendar_event(&self, event: CalendarEvent) -> Result<CalendarEvent>`
/// - `get_calendar_event(&self, event_id: &str) -> Result<CalendarEvent>`
/// - `update_calendar_event(&self, event_id: &str, event: CalendarEvent) -> Result<CalendarEvent>`
/// - `delete_calendar_event(&self, event_id: &str) -> Result<()>`
/// - `list_calendar_events(&self) -> Result<Vec<CalendarEvent>>`
/// - `reschedule_calendar_event(&self, event_id: &str, new_time: i64) -> Result<CalendarEvent>`
/// - `cancel_calendar_event(&self, event_id: &str) -> Result<CalendarEvent>`
#[proc_macro_derive(OsirisModel, attributes(osiris))]
pub fn derive_osiris_model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let model_name = &input.ident;
let model_name_snake = to_snake_case(&model_name.to_string());
// Parse attributes
let mut collection = model_name_snake.clone();
let mut id_field = "id".to_string();
let mut custom_methods: Vec<String> = Vec::new();
for attr in &input.attrs {
if attr.path().is_ident("osiris") {
if let Ok(meta_list) = attr.parse_args::<syn::MetaList>() {
// Parse nested attributes
for nested in meta_list.tokens.clone() {
let nested_str = nested.to_string();
if nested_str.starts_with("collection") {
if let Some(val) = extract_string_value(&nested_str) {
collection = val;
}
} else if nested_str.starts_with("id_field") {
if let Some(val) = extract_string_value(&nested_str) {
id_field = val;
}
} else if nested_str.starts_with("methods") {
custom_methods = extract_array_values(&nested_str);
}
}
}
}
}
// Generate method names
let create_method = syn::Ident::new(&format!("create_{}", model_name_snake), model_name.span());
let get_method = syn::Ident::new(&format!("get_{}", model_name_snake), model_name.span());
let update_method = syn::Ident::new(&format!("update_{}", model_name_snake), model_name.span());
let delete_method = syn::Ident::new(&format!("delete_{}", model_name_snake), model_name.span());
let list_method = syn::Ident::new(&format!("list_{}s", model_name_snake), model_name.span());
// Generate custom method implementations
let custom_method_impls: Vec<_> = custom_methods.iter().map(|method_name| {
let method_ident = syn::Ident::new(&format!("{}_{}", method_name, model_name_snake), model_name.span());
let rhai_call = format!("{}_{}", model_name_snake, method_name);
quote! {
pub async fn #method_ident(&self, id: &str, params: serde_json::Value) -> Result<#model_name, OsirisClientError> {
let script = format!(
r#"
let obj = {}::get("{}");
obj.{}(params);
obj.save();
obj
"#,
#collection, id, #method_name
);
let response = self.execute_script(&script).await?;
// Parse response and return model
// This is a simplified version - actual implementation would parse the job result
Err(OsirisClientError::CommandFailed("Not yet implemented".to_string()))
}
}
}).collect();
let expanded = quote! {
impl OsirisClient {
/// Create a new instance of #model_name
pub async fn #create_method(&self, model: &#model_name) -> Result<#model_name, OsirisClientError> {
let json = serde_json::to_string(model)
.map_err(|e| OsirisClientError::SerializationFailed(e.to_string()))?;
let script = format!(
r#"
let data = {};
let obj = {}::new(data);
obj.save();
obj
"#,
json, #collection
);
let response = self.execute_script(&script).await?;
// Parse response - simplified for now
Err(OsirisClientError::CommandFailed("Not yet implemented".to_string()))
}
/// Get an instance of #model_name by ID
pub async fn #get_method(&self, id: &str) -> Result<#model_name, OsirisClientError> {
let query = format!(r#"{{ "{}": "{}" }}"#, #id_field, id);
self.query::<#model_name>(#collection, &query).await
}
/// Update an existing #model_name
pub async fn #update_method(&self, id: &str, model: &#model_name) -> Result<#model_name, OsirisClientError> {
let json = serde_json::to_string(model)
.map_err(|e| OsirisClientError::SerializationFailed(e.to_string()))?;
let script = format!(
r#"
let obj = {}::get("{}");
let data = {};
obj.update(data);
obj.save();
obj
"#,
#collection, id, json
);
let response = self.execute_script(&script).await?;
Err(OsirisClientError::CommandFailed("Not yet implemented".to_string()))
}
/// Delete an instance of #model_name
pub async fn #delete_method(&self, id: &str) -> Result<(), OsirisClientError> {
let script = format!(
r#"
let obj = {}::get("{}");
obj.delete();
"#,
#collection, id
);
self.execute_script(&script).await?;
Ok(())
}
/// List all instances of #model_name
pub async fn #list_method(&self) -> Result<Vec<#model_name>, OsirisClientError> {
self.query_all::<#model_name>(#collection).await
}
#(#custom_method_impls)*
}
};
TokenStream::from(expanded)
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap());
} else {
result.push(ch);
}
}
result
}
fn extract_string_value(s: &str) -> Option<String> {
// Extract value from "key = \"value\"" format
if let Some(eq_pos) = s.find('=') {
let value_part = &s[eq_pos + 1..].trim();
let cleaned = value_part.trim_matches(|c| c == '"' || c == ' ');
return Some(cleaned.to_string());
}
None
}
fn extract_array_values(s: &str) -> Vec<String> {
// Extract values from "methods = [\"method1\", \"method2\"]" format
if let Some(start) = s.find('[') {
if let Some(end) = s.find(']') {
let array_content = &s[start + 1..end];
return array_content
.split(',')
.map(|item| item.trim().trim_matches('"').to_string())
.filter(|item| !item.is_empty())
.collect();
}
}
Vec::new()
}