Skip to main content

kura_cli/commands/
event.rs

1use clap::Subcommand;
2use serde_json::json;
3
4use crate::util::{api_request, exit_error, read_json_from_file};
5
6#[derive(Subcommand)]
7pub enum EventCommands {
8    /// Create a new event
9    Create {
10        /// Event type (e.g. "set.logged", "meal.logged", "metric.logged")
11        #[arg(long)]
12        event_type: String,
13        /// Event timestamp (RFC3339). Defaults to now.
14        #[arg(long)]
15        timestamp: Option<String>,
16        /// Event data as JSON string
17        #[arg(long, required_unless_present = "data_file")]
18        data: Option<String>,
19        /// Read event data from file (use '-' for stdin)
20        #[arg(long, short = 'f', conflicts_with = "data")]
21        data_file: Option<String>,
22        /// Idempotency key (auto-generated if omitted)
23        #[arg(long)]
24        idempotency_key: Option<String>,
25        /// Source identifier (defaults to "cli")
26        #[arg(long, default_value = "cli")]
27        source: String,
28        /// Agent identifier
29        #[arg(long)]
30        agent: Option<String>,
31    },
32    /// List events with optional filters
33    List {
34        /// Filter by event type
35        #[arg(long)]
36        event_type: Option<String>,
37        /// Only events after this timestamp (RFC3339)
38        #[arg(long)]
39        since: Option<String>,
40        /// Only events before this timestamp (RFC3339)
41        #[arg(long)]
42        until: Option<String>,
43        /// Maximum number of events to return
44        #[arg(long)]
45        limit: Option<u32>,
46        /// Pagination cursor from previous response
47        #[arg(long)]
48        cursor: Option<String>,
49    },
50    /// Create multiple events atomically
51    Batch {
52        /// JSON file with events array (use '-' for stdin)
53        #[arg(long)]
54        file: String,
55    },
56}
57
58pub async fn run(api_url: &str, token: Option<&str>, command: EventCommands) -> i32 {
59    match command {
60        EventCommands::Create {
61            event_type,
62            timestamp,
63            data,
64            data_file,
65            idempotency_key,
66            source,
67            agent,
68        } => {
69            create(
70                api_url,
71                token,
72                &event_type,
73                timestamp.as_deref(),
74                data.as_deref(),
75                data_file.as_deref(),
76                idempotency_key.as_deref(),
77                &source,
78                agent.as_deref(),
79            )
80            .await
81        }
82        EventCommands::List {
83            event_type,
84            since,
85            until,
86            limit,
87            cursor,
88        } => {
89            list(
90                api_url,
91                token,
92                event_type.as_deref(),
93                since.as_deref(),
94                until.as_deref(),
95                limit,
96                cursor.as_deref(),
97            )
98            .await
99        }
100        EventCommands::Batch { file } => batch(api_url, token, &file).await,
101    }
102}
103
104async fn create(
105    api_url: &str,
106    token: Option<&str>,
107    event_type: &str,
108    timestamp: Option<&str>,
109    data: Option<&str>,
110    data_file: Option<&str>,
111    idempotency_key: Option<&str>,
112    source: &str,
113    agent: Option<&str>,
114) -> i32 {
115    let data_value: serde_json::Value = if let Some(d) = data {
116        match serde_json::from_str(d) {
117            Ok(v) => v,
118            Err(e) => exit_error(
119                &format!("Invalid JSON in --data: {e}"),
120                Some("Provide valid JSON, e.g. --data '{\"weight_kg\":100}'"),
121            ),
122        }
123    } else if let Some(f) = data_file {
124        match read_json_from_file(f) {
125            Ok(v) => v,
126            Err(e) => exit_error(&e, Some("Provide a valid JSON file or use '-' for stdin")),
127        }
128    } else {
129        exit_error(
130            "Either --data or --data-file is required",
131            Some("Use --data '{...}' or --data-file path.json"),
132        )
133    };
134
135    let ts = match timestamp {
136        Some(t) => t.to_string(),
137        None => chrono::Utc::now().to_rfc3339(),
138    };
139
140    let idem_key = idempotency_key
141        .map(|k| k.to_string())
142        .unwrap_or_else(|| uuid::Uuid::now_v7().to_string());
143
144    let mut metadata = json!({
145        "source": source,
146        "idempotency_key": idem_key
147    });
148    if let Some(a) = agent {
149        metadata["agent"] = json!(a);
150    }
151
152    let body = json!({
153        "timestamp": ts,
154        "event_type": event_type,
155        "data": data_value,
156        "metadata": metadata
157    });
158
159    api_request(
160        api_url,
161        reqwest::Method::POST,
162        "/v1/events",
163        token,
164        Some(body),
165        &[],
166        &[],
167        false,
168        false,
169    )
170    .await
171}
172
173async fn list(
174    api_url: &str,
175    token: Option<&str>,
176    event_type: Option<&str>,
177    since: Option<&str>,
178    until: Option<&str>,
179    limit: Option<u32>,
180    cursor: Option<&str>,
181) -> i32 {
182    let mut query = Vec::new();
183    if let Some(et) = event_type {
184        query.push(("event_type".to_string(), et.to_string()));
185    }
186    if let Some(s) = since {
187        query.push(("since".to_string(), s.to_string()));
188    }
189    if let Some(u) = until {
190        query.push(("until".to_string(), u.to_string()));
191    }
192    if let Some(l) = limit {
193        query.push(("limit".to_string(), l.to_string()));
194    }
195    if let Some(c) = cursor {
196        query.push(("cursor".to_string(), c.to_string()));
197    }
198
199    api_request(
200        api_url,
201        reqwest::Method::GET,
202        "/v1/events",
203        token,
204        None,
205        &query,
206        &[],
207        false,
208        false,
209    )
210    .await
211}
212
213async fn batch(api_url: &str, token: Option<&str>, file: &str) -> i32 {
214    let body = match read_json_from_file(file) {
215        Ok(v) => v,
216        Err(e) => exit_error(&e, Some("Provide a JSON file with {\"events\": [...]}")),
217    };
218
219    api_request(
220        api_url,
221        reqwest::Method::POST,
222        "/v1/events/batch",
223        token,
224        Some(body),
225        &[],
226        &[],
227        false,
228        false,
229    )
230    .await
231}