Skip to main content

tango/models/
webhook.rs

1//! Typed models for the Tango webhook management API.
2//!
3//! Wire types are mirrored from `tango-node/src/models/Webhooks.ts`. Every
4//! struct carries `#[serde(flatten)] extra: HashMap<String, Value>` so a
5//! server-side schema addition surfaces as `extra["new_field"]` rather than
6//! being dropped during deserialization.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11
12// ---------------------------------------------------------------------------
13// Webhook endpoints
14// ---------------------------------------------------------------------------
15
16/// A configured webhook endpoint.
17#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
18pub struct WebhookEndpoint {
19    /// Endpoint UUID.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub id: Option<String>,
22    /// Human-readable name (unique per user).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub name: Option<String>,
25    /// Destination URL the server will POST deliveries to.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub callback_url: Option<String>,
28    /// Endpoint signing secret. Surfaced only on creation; subsequent reads
29    /// typically omit it.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub secret: Option<String>,
32    /// Whether the endpoint is enabled.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub is_active: Option<bool>,
35    /// ISO timestamp the endpoint was created.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub created_at: Option<String>,
38    /// ISO timestamp the endpoint was last updated.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub updated_at: Option<String>,
41    /// Forward-compatible bucket.
42    #[serde(flatten)]
43    pub extra: HashMap<String, Value>,
44}
45
46/// Request body for
47/// [`Client::create_webhook_endpoint`](crate::Client::create_webhook_endpoint).
48///
49/// `name` and `callback_url` are required. The Tango API enforces
50/// `unique(user, name)` on endpoints; the SDK validates this client-side
51/// for a cleaner error than the server's 400.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct WebhookEndpointCreateInput {
54    /// Human-readable name (must be unique per user).
55    pub name: String,
56    /// Destination URL.
57    pub callback_url: String,
58    /// Whether to enable on creation. Server defaults to `true` when omitted.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub is_active: Option<bool>,
61    /// Event types to subscribe to. Empty means "all event types".
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub event_types: Vec<String>,
64}
65
66/// PATCH body for
67/// [`Client::update_webhook_endpoint`](crate::Client::update_webhook_endpoint).
68/// Only `Some` fields are sent.
69#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
70pub struct WebhookEndpointUpdateInput {
71    /// New name.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub name: Option<String>,
74    /// New callback URL.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub callback_url: Option<String>,
77    /// Enable/disable.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub is_active: Option<bool>,
80    /// Replace the subscribed event-type list.
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub event_types: Vec<String>,
83}
84
85// ---------------------------------------------------------------------------
86// Webhook event types + sample payloads + test delivery
87// ---------------------------------------------------------------------------
88
89/// One event type the server can emit.
90#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
91pub struct WebhookEventType {
92    /// Wire name of the event type (e.g. `"alerts.contract.match"`).
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub event_type: Option<String>,
95    /// Human-readable description.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub description: Option<String>,
98    /// Schema version of the event body.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub schema_version: Option<i64>,
101    /// Forward-compatible bucket.
102    #[serde(flatten)]
103    pub extra: HashMap<String, Value>,
104}
105
106/// Response from
107/// [`Client::list_webhook_event_types`](crate::Client::list_webhook_event_types).
108#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
109pub struct WebhookEventTypesResponse {
110    /// The event types the server can emit.
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    pub event_types: Vec<WebhookEventType>,
113    /// Forward-compatible bucket.
114    #[serde(flatten)]
115    pub extra: HashMap<String, Value>,
116}
117
118/// One timestamped batch of synthetic events in a sample-payload response.
119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
120pub struct WebhookSampleDelivery {
121    /// ISO timestamp for the sample.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub timestamp: Option<String>,
124    /// The events in this batch (each event is a free-form JSON object).
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub events: Vec<Value>,
127    /// Forward-compatible bucket.
128    #[serde(flatten)]
129    pub extra: HashMap<String, Value>,
130}
131
132/// Wrapper around the per-type sample-delivery struct used in the all-types
133/// variant of [`WebhookSamplePayloadResponse`].
134#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
135pub struct WebhookSamplePayloadSample {
136    /// The sample delivery for one event type.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub sample_delivery: Option<WebhookSampleDelivery>,
139    /// Forward-compatible bucket.
140    #[serde(flatten)]
141    pub extra: HashMap<String, Value>,
142}
143
144/// Response from
145/// [`Client::get_webhook_sample_payload`](crate::Client::get_webhook_sample_payload).
146///
147/// Covers both variants the endpoint returns:
148/// - The single-event-type variant (when `event_type` is passed): `event_type`,
149///   `sample_delivery`, `signature_header`, `note` are populated.
150/// - The all-types variant (when `event_type` is omitted): `samples` and
151///   `usage` are populated.
152///
153/// Callers can branch on which fields are populated.
154#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
155pub struct WebhookSamplePayloadResponse {
156    /// Event type (single-variant only).
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub event_type: Option<String>,
159    /// Sample delivery body (single-variant only).
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub sample_delivery: Option<WebhookSampleDelivery>,
162    /// Example signature header line (single-variant only).
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub signature_header: Option<String>,
165    /// Free-text guidance from the server.
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub note: Option<String>,
168    /// Map of event type → sample (all-types variant).
169    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
170    pub samples: HashMap<String, WebhookSamplePayloadSample>,
171    /// Free-text guidance (all-types variant).
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub usage: Option<String>,
174    /// Forward-compatible bucket.
175    #[serde(flatten)]
176    pub extra: HashMap<String, Value>,
177}
178
179/// Response from
180/// [`Client::test_webhook_endpoint`](crate::Client::test_webhook_endpoint).
181#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
182pub struct WebhookTestDeliveryResult {
183    /// Whether the test POST succeeded.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub success: Option<bool>,
186    /// HTTP status code returned by the destination.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub status_code: Option<i64>,
189    /// Response time of the destination, in milliseconds.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub response_time_ms: Option<i64>,
192    /// Echoed destination URL.
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub endpoint_url: Option<String>,
195    /// Human-readable message.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub message: Option<String>,
198    /// Error message, when `success` is false.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub error: Option<String>,
201    /// Response body returned by the destination.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub response_body: Option<String>,
204    /// The synthetic body the server delivered.
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub test_payload: Option<Value>,
207    /// Forward-compatible bucket.
208    #[serde(flatten)]
209    pub extra: HashMap<String, Value>,
210}
211
212// ---------------------------------------------------------------------------
213// Webhook alerts (filter-based subscription convenience API)
214// ---------------------------------------------------------------------------
215
216/// A filter-based webhook subscription.
217///
218/// The alerts API uses `name` + `filters` (whereas the canonical subscriptions
219/// API uses `subscription_name` + `filter_definition`).
220#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
221pub struct WebhookAlert {
222    /// Alert UUID.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub alert_id: Option<String>,
225    /// Human-readable name.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub name: Option<String>,
228    /// Resource type the alert filters (`"contract"`, `"opportunity"`, …; singular).
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub query_type: Option<String>,
231    /// Filter map applied against new resources.
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub filters: Option<Value>,
234    /// `"realtime"`, `"hourly"`, etc.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub frequency: Option<String>,
237    /// Cron expression when `frequency = "custom"`.
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub cron_expression: Option<String>,
240    /// Lifecycle status (`"active"`, `"paused"`, …).
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub status: Option<String>,
243    /// ISO timestamp the alert was created.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub created_at: Option<String>,
246    /// ISO timestamp the alert was last checked.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub last_checked_at: Option<String>,
249    /// Number of resources the alert has matched.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub match_count: Option<i64>,
252    /// Forward-compatible bucket.
253    #[serde(flatten)]
254    pub extra: HashMap<String, Value>,
255}
256
257/// Request body for
258/// [`Client::create_webhook_alert`](crate::Client::create_webhook_alert).
259///
260/// `name`, `query_type`, and a non-empty `filters` map are required.
261/// `query_type` is SINGULAR (`"contract"`, not `"contracts"`). For accounts
262/// with multiple webhook endpoints, set `endpoint` to the destination UUID;
263/// single-endpoint accounts may omit it.
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
265pub struct WebhookAlertCreateInput {
266    /// Human-readable name.
267    pub name: String,
268    /// Singular resource type (e.g. `"contract"`).
269    pub query_type: String,
270    /// Filter map applied against new resources (non-empty).
271    pub filters: Value,
272    /// `"realtime"`, `"hourly"`, etc.
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub frequency: Option<String>,
275    /// Cron expression when `frequency = "custom"`.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub cron_expression: Option<String>,
278    /// Destination endpoint UUID (required when the account has more than one).
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub endpoint: Option<String>,
281}
282
283/// PATCH body for
284/// [`Client::update_webhook_alert`](crate::Client::update_webhook_alert).
285///
286/// Only `name`, `frequency`, `cron_expression`, and `is_active` are writable
287/// server-side. `query_type` and `filters` are read-only after creation.
288#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
289pub struct WebhookAlertUpdateInput {
290    /// New name.
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub name: Option<String>,
293    /// New frequency.
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub frequency: Option<String>,
296    /// New cron expression.
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub cron_expression: Option<String>,
299    /// Enable / pause.
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub is_active: Option<bool>,
302}