Action Type DSL

The Action Type DSL (Domain-Specific Language) is Cloudillo’s system for defining action types declaratively. Instead of hardcoding behavior for each action type, the DSL allows flexible configuration of validation rules, processing behavior, and lifecycle hooks.

Overview

The DSL system serves several purposes:

  • Validation: Define what fields are required/optional for each action type
  • Behavior Configuration: Control how actions are processed, delivered, and stored
  • Extensibility: Add new action types without modifying core code
  • Consistency: Ensure all actions of a type follow the same rules

ActionDefinition Structure

Each action type is defined by an ActionDefinition:

struct ActionDefinition {
    action_type: String,      // e.g., "POST", "CMNT", "FLLW"
    version: u32,             // Schema version
    metadata: Metadata,       // Display name, description, etc.
    subtypes: Vec<String>,    // e.g., POST:IMG, POST:VID
    fields: Vec<FieldDef>,    // Field definitions
    schema: Schema,           // JSON Schema for validation
    behavior: Behavior,       // Processing behavior flags
    hooks: Hooks,             // Lifecycle hooks
    permissions: Permissions, // Permission requirements
    key_pattern: String,      // Database key pattern for overriding
}

Metadata

struct Metadata {
    display_name: String,     // Human-readable name
    description: String,      // What this action represents
    category: Category,       // content, user, communication, metadata
}

Fields

Each field in an action is defined with validation rules:

struct FieldDef {
    name: String,             // Field name in JWT (e.g., "c", "p", "vis")
    field_type: FieldType,    // string, object, array, number, boolean
    required: bool,           // Is this field required?
    validate: Option<Schema>, // Additional validation rules
}

Behavior Flags

Behavior flags control how actions are processed:

Flag Description Example Types
broadcast Send to multiple recipients (followers) POST, REPOST
allow_unknown Accept actions from unknown senders CONN
ephemeral Don’t persist to database PRES (presence)
approvable Can be approved/rejected by recipient CONN, FLLW
requires_subscription Sender must be subscribed MSG, CONV
deliver_subject Deliver to subject owner CMNT, REACT
deliver_to_subject_owner Alternative delivery to subject owner CMNT

Behavior Examples

POST Action:

Behavior {
    broadcast: true,              // Send to all followers
    allow_unknown: false,         // Must be from followed/connected user
    ephemeral: false,             // Persist to database
    approvable: false,            // No approval needed
    requires_subscription: false, // No subscription required
    deliver_subject: false,       // Not a reply
}

CONN Action:

Behavior {
    broadcast: false,             // Send only to audience
    allow_unknown: true,          // Accept from anyone (with PoW)
    ephemeral: false,             // Persist to database
    approvable: true,             // Recipient can accept/reject
    requires_subscription: false, // No subscription required
    deliver_subject: false,       // Direct to audience
}

MSG Action:

Behavior {
    broadcast: false,             // Send to conversation participants
    allow_unknown: false,         // Must be conversation member
    ephemeral: false,             // Persist to database
    approvable: false,            // No approval needed
    requires_subscription: true,  // Must be subscribed to conversation
    deliver_subject: false,       // Uses subscription delivery
}

PRES (Presence) Action:

Behavior {
    broadcast: true,              // Send to connections
    allow_unknown: false,         // Must be connected
    ephemeral: true,              // DO NOT persist
    approvable: false,            // No approval needed
    requires_subscription: false, // No subscription required
    deliver_subject: false,       // Direct broadcast
}

Hook System

Hooks allow custom logic at different points in the action lifecycle:

struct Hooks {
    on_create: Option<HookFn>,   // Called when action is created locally
    on_receive: Option<HookFn>,  // Called when action is received from federation
    on_accept: Option<HookFn>,   // Called when approvable action is accepted
    on_reject: Option<HookFn>,   // Called when approvable action is rejected
}

Hook Use Cases

on_create:

  • Validate business logic
  • Generate related actions
  • Update statistics

on_receive:

  • Trigger notifications
  • Update local state
  • Forward to WebSocket clients

on_accept:

  • Establish connection (CONN)
  • Add to subscriber list (SUBS)
  • Grant permissions

on_reject:

  • Clean up pending state
  • Notify sender of rejection

Hook Example: CONN Action

on_create:
    # Validate PoW nonce
    verify_proof_of_work(action.nonce)
    # Store as pending connection
    store_pending_connection(action)

on_receive:
    # Notify user of connection request
    send_notification(action.audience, "New connection request")
    # Forward to WebSocket
    forward_to_websocket(action)

on_accept:
    # Create bidirectional connection
    create_connection(action.issuer, action.audience)
    # Update follower counts
    update_connection_counts()

on_reject:
    # Remove pending connection
    remove_pending_connection(action.id)
    # Optionally notify sender

Permissions

Define who can perform which operations:

struct Permissions {
    create: PermissionRule,    // Who can create this action type
    read: PermissionRule,      // Who can read actions of this type
    delete: PermissionRule,    // Who can delete (owner only typically)
}

Permission Rules

Rule Description
Owner Only the action issuer
Audience The action’s audience field
Connected Mutually connected users
Follower Users who follow the issuer
Verified Any authenticated user
Public Anyone, including unauthenticated

Key Pattern

The key_pattern determines how actions are stored and whether they can be overridden:

Action Type Key Pattern Override Behavior
POST {iss}:{id} Each post is unique
CMNT {iss}:{p}:{id} Each comment is unique (p = parent)
REACT {iss}:{sub} One reaction per user per subject
FLLW {iss}:{aud} One follow per user per target
CONN {iss}:{aud} One connection request per pair

Override example:

User reacts to post with LIKE
  → Stored at key: "alice.example.com:a1~post123"

User changes reaction to LOVE
  → Same key: "alice.example.com:a1~post123"
  → Previous LIKE is REPLACED by LOVE

Example Definitions

POST Definition

ActionDefinition {
    action_type: "POST",
    version: 1,
    metadata: Metadata {
        display_name: "Post",
        description: "A public post or status update",
        category: Category::Content,
    },
    subtypes: vec!["IMG", "VID", "LNK", "TXT"],
    fields: vec![
        FieldDef { name: "c", field_type: String, required: true },
        FieldDef { name: "a", field_type: Array, required: false },
        FieldDef { name: "vis", field_type: String, required: false },
        FieldDef { name: "f", field_type: String, required: false },
    ],
    behavior: Behavior {
        broadcast: true,
        allow_unknown: false,
        ephemeral: false,
        approvable: false,
        requires_subscription: false,
        deliver_subject: false,
    },
    hooks: Hooks {
        on_create: Some(post_on_create),
        on_receive: Some(post_on_receive),
        on_accept: None,
        on_reject: None,
    },
    permissions: Permissions {
        create: PermissionRule::Owner,
        read: PermissionRule::ByVisibility,
        delete: PermissionRule::Owner,
    },
    key_pattern: "{iss}:{id}",
}

CMNT Definition

ActionDefinition {
    action_type: "CMNT",
    version: 1,
    metadata: Metadata {
        display_name: "Comment",
        description: "A comment on another action",
        category: Category::Content,
    },
    subtypes: vec![],
    fields: vec![
        FieldDef { name: "c", field_type: String, required: true },
        FieldDef { name: "p", field_type: String, required: true },  // Parent required
        FieldDef { name: "a", field_type: Array, required: false },
    ],
    behavior: Behavior {
        broadcast: false,
        allow_unknown: false,
        ephemeral: false,
        approvable: false,
        requires_subscription: false,
        deliver_subject: true,  // Deliver to parent owner
    },
    hooks: Hooks {
        on_create: Some(cmnt_on_create),
        on_receive: Some(cmnt_on_receive),
        on_accept: None,
        on_reject: None,
    },
    permissions: Permissions {
        create: PermissionRule::Connected,
        read: PermissionRule::ByVisibility,
        delete: PermissionRule::Owner,
    },
    key_pattern: "{iss}:{p}:{id}",
}

CONV Definition

ActionDefinition {
    action_type: "CONV",
    version: 1,
    metadata: Metadata {
        display_name: "Conversation",
        description: "A group conversation",
        category: Category::Communication,
    },
    subtypes: vec![],
    fields: vec![
        FieldDef { name: "c", field_type: Object, required: true },  // {name, participants}
    ],
    behavior: Behavior {
        broadcast: false,
        allow_unknown: false,
        ephemeral: false,
        approvable: false,
        requires_subscription: false,
        deliver_subject: false,
    },
    hooks: Hooks {
        on_create: Some(conv_on_create),  // Creates SUBS for participants
        on_receive: Some(conv_on_receive),
        on_accept: None,
        on_reject: None,
    },
    permissions: Permissions {
        create: PermissionRule::Verified,
        read: PermissionRule::Subscriber,
        delete: PermissionRule::Owner,
    },
    key_pattern: "{iss}:{id}",
}

DSL Validation

When an action is created or received, it’s validated against its DSL definition:

validate_action(action, definition):
    1. Check required fields exist
    2. Validate field types
    3. Run schema validation
    4. Check behavior constraints:
       - If requires_subscription: verify sender is subscribed
       - If !allow_unknown: verify sender is known (following/connected)
       - If approvable: set initial status to pending
    5. Return validation result

Extending with New Action Types

To add a new action type:

  1. Define the ActionDefinition with all fields, behavior, and hooks
  2. Register the definition in the action registry
  3. Implement hooks for custom behavior (optional)
  4. Add client support for creating/displaying the action

Example: Adding a new POLL action type:

ActionDefinition {
    action_type: "POLL",
    version: 1,
    metadata: Metadata {
        display_name: "Poll",
        description: "A poll with voting options",
        category: Category::Content,
    },
    subtypes: vec![],
    fields: vec![
        FieldDef { name: "c", field_type: Object, required: true },  // {question, options, expires}
    ],
    behavior: Behavior {
        broadcast: true,
        allow_unknown: false,
        ephemeral: false,
        approvable: false,
        requires_subscription: false,
        deliver_subject: false,
    },
    hooks: Hooks {
        on_create: Some(poll_on_create),
        on_receive: Some(poll_on_receive),
        on_accept: None,
        on_reject: None,
    },
    permissions: Permissions {
        create: PermissionRule::Verified,
        read: PermissionRule::ByVisibility,
        delete: PermissionRule::Owner,
    },
    key_pattern: "{iss}:{id}",
}

See Also