Skip to main content

cloudillo_action/dsl/
types.rs

1//! Type definitions for the Action DSL
2
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5use std::collections::HashMap;
6
7use crate::hooks::HookImplementation;
8
9/// Complete action type definition in DSL format
10#[skip_serializing_none]
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ActionDefinition {
13	/// Action type identifier (e.g., "CONN", "POST")
14	pub r#type: String,
15	/// Semantic version
16	pub version: String,
17	/// Human-readable description
18	pub description: String,
19
20	/// Metadata about the action type
21	pub metadata: Option<ActionMetadata>,
22
23	/// Subtype definitions
24	pub subtypes: Option<HashMap<String, String>>,
25
26	/// Field constraints (required/optional/forbidden)
27	pub fields: FieldConstraints,
28
29	/// Content schema definition (only field with configurable schema)
30	pub schema: Option<ContentSchemaWrapper>,
31
32	/// Behavior flags
33	pub behavior: BehaviorFlags,
34
35	/// Key pattern for unique action identification
36	pub key_pattern: Option<String>,
37
38	/// Lifecycle hooks
39	pub hooks: ActionHooks,
40
41	/// Permission rules
42	pub permissions: Option<PermissionRules>,
43}
44
45/// Metadata about an action type
46#[skip_serializing_none]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ActionMetadata {
49	pub category: Option<String>,
50	pub tags: Option<Vec<String>>,
51	pub deprecated: Option<bool>,
52	pub experimental: Option<bool>,
53}
54
55/// Field constraints - only optionality is configurable (types are fixed)
56#[skip_serializing_none]
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
58pub struct FieldConstraints {
59	/// Content field (type: json)
60	pub content: Option<FieldConstraint>,
61	/// Audience field (type: idTag)
62	pub audience: Option<FieldConstraint>,
63	/// Parent field (type: actionId)
64	pub parent: Option<FieldConstraint>,
65	/// Subject field (type: actionId/string)
66	pub subject: Option<FieldConstraint>,
67	/// Attachments field (type: fileId[])
68	pub attachments: Option<FieldConstraint>,
69}
70
71/// Field constraint - controls whether a field is required, forbidden, or optional
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum FieldConstraint {
75	/// Field must be present and valid
76	Required,
77	/// Field must be null/undefined
78	Forbidden,
79}
80
81/// Wrapper for content schema
82#[skip_serializing_none]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ContentSchemaWrapper {
85	pub content: Option<ContentSchema>,
86}
87
88/// Schema definition for the content field
89#[skip_serializing_none]
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ContentSchema {
92	/// Content type
93	#[serde(rename = "type")]
94	pub content_type: ContentType,
95
96	/// String constraints
97	pub min_length: Option<usize>,
98	pub max_length: Option<usize>,
99	pub pattern: Option<String>,
100
101	/// Enum constraint
102	pub r#enum: Option<Vec<serde_json::Value>>,
103
104	/// Object properties (for object type)
105	pub properties: Option<HashMap<String, SchemaField>>,
106
107	/// Required properties (for object type)
108	pub required: Option<Vec<String>>,
109
110	/// Description
111	pub description: Option<String>,
112}
113
114/// Content type
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "lowercase")]
117pub enum ContentType {
118	String,
119	Number,
120	Boolean,
121	Object,
122	Json,
123}
124
125/// Schema field definition for object properties
126#[skip_serializing_none]
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SchemaField {
129	#[serde(rename = "type")]
130	pub field_type: FieldType,
131
132	pub min_length: Option<usize>,
133	pub max_length: Option<usize>,
134	pub r#enum: Option<Vec<serde_json::Value>>,
135	pub items: Option<Box<SchemaField>>,
136}
137
138/// Field type for schema properties
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum FieldType {
142	String,
143	Number,
144	Boolean,
145	Array,
146	Json,
147}
148
149/// Behavior flags controlling action processing
150///
151/// # Implementation Status
152///
153/// ## Fully Implemented
154/// - `broadcast` - Checked in `schedule_delivery()` for self-posting
155/// - `allow_unknown` - Validated on both inbound (permission check) and outbound (create_action)
156/// - `ephemeral` - Skips persistence, forwards to WebSocket only
157/// - `approvable` - Enables APRV flow, auto-approve for trusted sources
158/// - `requires_subscription` - Validated on both inbound and outbound
159/// - `deliver_subject` - Delivers subject action along with main action
160/// - `subscribable` - Enables SUBS-based permissions and visibility
161/// - `deliver_to_subject_owner` - Dual delivery to subject owner
162/// - `default_flags` - Applied during action creation
163///
164/// ## Reserved (Not Implemented)
165/// - `requires_acceptance` - RESERVED: Would set initial status to CONFIRMATION
166/// - `local_only` - RESERVED: Would skip federation in schedule_delivery
167/// - `ttl` - RESERVED: Time-to-live for action expiration
168/// - `sync` - RESERVED: Synchronous processing mode
169/// - `federated` - RESERVED: Cross-instance federation control
170#[skip_serializing_none]
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct BehaviorFlags {
173	// === Fully Implemented ===
174	/// Send to all followers when posting to own wall (no audience).
175	/// Checked in `schedule_delivery()` for self-posting.
176	pub broadcast: Option<bool>,
177
178	/// Accept actions from non-connected/non-following users.
179	/// Validated on both inbound (permission check) and outbound (create_action).
180	pub allow_unknown: Option<bool>,
181
182	/// Don't persist to database, only forward to WebSocket.
183	/// Used for real-time ephemeral actions like typing indicators.
184	pub ephemeral: Option<bool>,
185
186	/// Can this action receive APRV (approval) from audience?
187	/// When true, accepting this action will generate an APRV federated signal.
188	/// Also enables auto-approve for trusted sources.
189	pub approvable: Option<bool>,
190
191	/// Child actions require SUBS (subscription) validation.
192	/// Validated on both inbound and outbound flows.
193	pub requires_subscription: Option<bool>,
194
195	/// Deliver subject action along with this action to recipients.
196	/// Used by APRV to include the approved POST when fanning out.
197	pub deliver_subject: Option<bool>,
198
199	/// This action type can have SUBS (subscriptions) pointing to it.
200	/// When true, subscribers are included in visibility checks for Direct visibility.
201	/// Also enables fan-out to subscribers in parent chain.
202	pub subscribable: Option<bool>,
203
204	/// Also deliver to subject's owner (in addition to audience).
205	/// Used by INVT to deliver to both invitee and CONV home for validation.
206	pub deliver_to_subject_owner: Option<bool>,
207
208	/// Default flags for this action type (R/r=reactions, C/c=comments, O/o=open).
209	/// Applied during action creation.
210	pub default_flags: Option<String>,
211
212	/// Flag character that gates this action on the PARENT action's flags.
213	/// If set and the parent has the lowercase version, this action is rejected.
214	/// E.g., 'C' for CMNT: if parent has 'c' (comments disabled), reject.
215	pub gated_by_parent_flag: Option<char>,
216
217	/// Flag character that gates this action on the SUBJECT action's flags.
218	/// Same logic but checks the subject action.
219	/// E.g., 'R' for REACT: if subject has 'r' (reactions disabled), reject.
220	pub gated_by_subject_flag: Option<char>,
221
222	// === Reserved (Not Implemented) ===
223	/// RESERVED: Requires user confirmation before activation.
224	/// When implemented, would set initial status to CONFIRMATION.
225	pub requires_acceptance: Option<bool>,
226
227	/// RESERVED: Never federate this action type.
228	/// When implemented, would skip federation in schedule_delivery.
229	pub local_only: Option<bool>,
230
231	/// RESERVED: Time to live in seconds.
232	/// When implemented, would enable automatic action expiration.
233	pub ttl: Option<u64>,
234
235	/// RESERVED: Process synchronously.
236	/// Currently only affects IDP:REG hook execution.
237	pub sync: Option<bool>,
238
239	/// RESERVED: Allow cross-instance federation.
240	/// Default behavior is to federate; this flag is reserved for future use.
241	pub federated: Option<bool>,
242}
243
244/// Lifecycle hooks for action processing
245#[derive(Debug, Clone, Default)]
246pub struct ActionHooks {
247	/// Execute when creating an action locally
248	pub on_create: HookImplementation,
249	/// Execute when receiving an action from remote
250	pub on_receive: HookImplementation,
251	/// Execute when user accepts a confirmation action
252	pub on_accept: HookImplementation,
253	/// Execute when user rejects a confirmation action
254	pub on_reject: HookImplementation,
255}
256
257// Custom serialization for ActionHooks
258impl Serialize for ActionHooks {
259	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
260	where
261		S: serde::Serializer,
262	{
263		use serde::ser::SerializeStruct;
264		let mut state = serializer.serialize_struct("ActionHooks", 4)?;
265		state.serialize_field("on_create", &self.on_create)?;
266		state.serialize_field("on_receive", &self.on_receive)?;
267		state.serialize_field("on_accept", &self.on_accept)?;
268		state.serialize_field("on_reject", &self.on_reject)?;
269		state.end()
270	}
271}
272
273// Custom deserialization for ActionHooks
274impl<'de> Deserialize<'de> for ActionHooks {
275	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276	where
277		D: serde::Deserializer<'de>,
278	{
279		use serde::de::{self, MapAccess, Visitor};
280		#[allow(unused_imports)]
281		use std::fmt;
282
283		enum Field {
284			Create,
285			Receive,
286			Accept,
287			Reject,
288		}
289
290		impl<'de> Deserialize<'de> for Field {
291			fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292			where
293				D: serde::Deserializer<'de>,
294			{
295				struct FieldVisitor;
296
297				impl<'de> Visitor<'de> for FieldVisitor {
298					type Value = Field;
299
300					fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
301						formatter
302							.write_str("`on_create`, `on_receive`, `on_accept`, or `on_reject`")
303					}
304
305					fn visit_str<E>(self, value: &str) -> Result<Field, E>
306					where
307						E: de::Error,
308					{
309						match value {
310							"on_create" => Ok(Field::Create),
311							"on_receive" => Ok(Field::Receive),
312							"on_accept" => Ok(Field::Accept),
313							"on_reject" => Ok(Field::Reject),
314							_ => Err(de::Error::unknown_field(value, FIELDS)),
315						}
316					}
317				}
318
319				deserializer.deserialize_identifier(FieldVisitor)
320			}
321		}
322
323		struct ActionHooksVisitor;
324
325		impl<'de> Visitor<'de> for ActionHooksVisitor {
326			type Value = ActionHooks;
327
328			fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
329				formatter.write_str("struct ActionHooks")
330			}
331
332			fn visit_map<V>(self, mut map: V) -> Result<ActionHooks, V::Error>
333			where
334				V: MapAccess<'de>,
335			{
336				let mut on_create = HookImplementation::None;
337				let mut on_receive = HookImplementation::None;
338				let mut on_accept = HookImplementation::None;
339				let mut on_reject = HookImplementation::None;
340
341				while let Some(key) = map.next_key()? {
342					match key {
343						Field::Create => {
344							if !matches!(on_create, HookImplementation::None) {
345								return Err(de::Error::duplicate_field("on_create"));
346							}
347							on_create = map.next_value()?;
348						}
349						Field::Receive => {
350							if !matches!(on_receive, HookImplementation::None) {
351								return Err(de::Error::duplicate_field("on_receive"));
352							}
353							on_receive = map.next_value()?;
354						}
355						Field::Accept => {
356							if !matches!(on_accept, HookImplementation::None) {
357								return Err(de::Error::duplicate_field("on_accept"));
358							}
359							on_accept = map.next_value()?;
360						}
361						Field::Reject => {
362							if !matches!(on_reject, HookImplementation::None) {
363								return Err(de::Error::duplicate_field("on_reject"));
364							}
365							on_reject = map.next_value()?;
366						}
367					}
368				}
369
370				Ok(ActionHooks { on_create, on_receive, on_accept, on_reject })
371			}
372		}
373
374		const FIELDS: &[&str] = &["on_create", "on_receive", "on_accept", "on_reject"];
375		deserializer.deserialize_struct("ActionHooks", FIELDS, ActionHooksVisitor)
376	}
377}
378
379/// Permission rules for action types
380#[skip_serializing_none]
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct PermissionRules {
383	pub can_create: Option<String>,
384	pub can_receive: Option<String>,
385	pub requires_following: Option<bool>,
386	pub requires_connected: Option<bool>,
387}
388
389/// DSL operation - tagged enum for all operation types
390#[derive(Debug, Clone, Serialize, Deserialize)]
391#[serde(tag = "op", rename_all = "snake_case")]
392pub enum Operation {
393	// Profile operations
394	UpdateProfile {
395		target: Expression,
396		set: HashMap<String, Expression>,
397	},
398	GetProfile {
399		target: Expression,
400		#[serde(skip_serializing_if = "Option::is_none")]
401		r#as: Option<String>,
402	},
403
404	// Action operations
405	CreateAction {
406		r#type: String,
407		#[serde(skip_serializing_if = "Option::is_none")]
408		subtype: Option<Expression>,
409		#[serde(skip_serializing_if = "Option::is_none")]
410		audience: Option<Expression>,
411		#[serde(skip_serializing_if = "Option::is_none")]
412		parent: Option<Expression>,
413		#[serde(skip_serializing_if = "Option::is_none")]
414		subject: Option<Expression>,
415		#[serde(skip_serializing_if = "Option::is_none")]
416		content: Option<Expression>,
417		#[serde(skip_serializing_if = "Option::is_none")]
418		attachments: Option<Expression>,
419	},
420	GetAction {
421		#[serde(skip_serializing_if = "Option::is_none")]
422		key: Option<Expression>,
423		#[serde(skip_serializing_if = "Option::is_none")]
424		action_id: Option<Expression>,
425		#[serde(skip_serializing_if = "Option::is_none")]
426		r#as: Option<String>,
427	},
428	UpdateAction {
429		target: Expression,
430		set: HashMap<String, UpdateValue>,
431	},
432	DeleteAction {
433		target: Expression,
434	},
435
436	// Control flow operations
437	If {
438		condition: Expression,
439		then: Vec<Operation>,
440		#[serde(skip_serializing_if = "Option::is_none")]
441		r#else: Option<Vec<Operation>>,
442	},
443	Switch {
444		value: Expression,
445		cases: HashMap<String, Vec<Operation>>,
446		#[serde(skip_serializing_if = "Option::is_none")]
447		default: Option<Vec<Operation>>,
448	},
449	Foreach {
450		array: Expression,
451		#[serde(skip_serializing_if = "Option::is_none")]
452		r#as: Option<String>,
453		r#do: Vec<Operation>,
454	},
455	Return {
456		#[serde(skip_serializing_if = "Option::is_none")]
457		value: Option<Expression>,
458	},
459
460	// Data operations
461	Set {
462		var: String,
463		value: Expression,
464	},
465	Get {
466		var: String,
467		from: Expression,
468	},
469	Merge {
470		objects: Vec<Expression>,
471		r#as: String,
472	},
473
474	// Federation operations
475	BroadcastToFollowers {
476		action_id: Expression,
477		token: Expression,
478	},
479	SendToAudience {
480		action_id: Expression,
481		token: Expression,
482		audience: Expression,
483	},
484
485	// Notification operations
486	CreateNotification {
487		user: Expression,
488		r#type: Expression,
489		action_id: Expression,
490		#[serde(skip_serializing_if = "Option::is_none")]
491		priority: Option<Expression>,
492	},
493
494	// Utility operations
495	Log {
496		#[serde(skip_serializing_if = "Option::is_none")]
497		level: Option<String>,
498		message: Expression,
499	},
500	Abort {
501		error: Expression,
502		#[serde(skip_serializing_if = "Option::is_none")]
503		code: Option<String>,
504	},
505}
506
507/// Update value for action updates (supports increment/decrement)
508#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(untagged)]
510pub enum UpdateValue {
511	Direct(Expression),
512	Increment { increment: Expression },
513	Decrement { decrement: Expression },
514	Set { set: Expression },
515}
516
517/// Expression - can be a literal, variable reference, or complex expression
518#[derive(Debug, Clone, Serialize, Deserialize)]
519#[serde(untagged)]
520pub enum Expression {
521	// Literals
522	Null,
523	Bool(bool),
524	Number(f64),
525	String(String),
526
527	// Complex expressions
528	Comparison(Box<ComparisonExpr>),
529	Logical(Box<LogicalExpr>),
530	Arithmetic(Box<ArithmeticExpr>),
531	StringOp(Box<StringOpExpr>),
532	Ternary(Box<TernaryExpr>),
533	Coalesce(Box<CoalesceExpr>),
534}
535
536/// Comparison expressions
537#[derive(Debug, Clone, Serialize, Deserialize)]
538#[serde(rename_all = "lowercase")]
539pub enum ComparisonExpr {
540	Eq([Expression; 2]),
541	Ne([Expression; 2]),
542	Gt([Expression; 2]),
543	Gte([Expression; 2]),
544	Lt([Expression; 2]),
545	Lte([Expression; 2]),
546}
547
548/// Logical expressions
549#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(rename_all = "lowercase")]
551pub enum LogicalExpr {
552	And(Vec<Expression>),
553	Or(Vec<Expression>),
554	Not(Expression),
555}
556
557/// Arithmetic expressions
558#[derive(Debug, Clone, Serialize, Deserialize)]
559#[serde(rename_all = "lowercase")]
560pub enum ArithmeticExpr {
561	Add(Vec<Expression>),
562	Subtract([Expression; 2]),
563	Multiply(Vec<Expression>),
564	Divide([Expression; 2]),
565}
566
567/// String operations
568#[derive(Debug, Clone, Serialize, Deserialize)]
569#[serde(rename_all = "lowercase")]
570pub enum StringOpExpr {
571	Concat(Vec<Expression>),
572	Contains([Expression; 2]),
573	StartsWith([Expression; 2]),
574	EndsWith([Expression; 2]),
575}
576
577/// Ternary expression (if-then-else)
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct TernaryExpr {
580	pub r#if: Expression,
581	pub then: Expression,
582	pub r#else: Expression,
583}
584
585/// Coalesce expression (return first non-null value)
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct CoalesceExpr {
588	pub coalesce: Vec<Expression>,
589}
590
591// Note: HookContext is now in crate::hooks and re-exported above
592// vim: ts=4