rship-entities-core 4.0.0-canary.53

Core entities for rship
Documentation
use std::sync::Arc;

use myko::{
    command::{CommandContext, CommandError, CommandHandler},
    myko_command, myko_item, myko_query,
    prelude::*,
};

use crate::{Project, ProjectId};

#[myko_item]
pub struct Session {
    /// Display name
    #[searchable]
    #[default_value("Main Session")]
    #[myko_rename("SetSessionName")]
    pub name: String,

    /// Color
    #[default_value("#0096c7")]
    #[myko_setter]
    pub color: String,

    /// Reference to parent scope
    #[belongs_to(Project)]
    #[ensure_for(Project)]
    pub scope_id: ProjectId,

    /// Reference to parent calendar
    #[serde(default)]
    pub calendar_id: Option<String>,

    /// Reference to parent overview scene
    #[serde(default)]
    pub overview_scene_id: Option<String>,
}

// ─────────────────────────────────────────────────────────────────────────────
// Queries
// ─────────────────────────────────────────────────────────────────────────────

#[myko_query(Session)]
pub struct GetSessionsByScopeId {
    pub scope_id: ProjectId,
}

impl QueryHandler for GetSessionsByScopeId {
    fn test_entity(ctx: QueryTestCtx<Self>) -> bool {
        ctx.item.scope_id.as_ref() == ctx.query.scope_id.as_ref()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────

/// Command to set the calendar ID for a session
#[myko_command]
pub struct SetCalendar {
    pub id: SessionId,
    #[serde(default)]
    pub calendar_id: Option<Arc<str>>,
}

impl CommandHandler for SetCalendar {
    fn execute(self, ctx: CommandContext) -> Result<(), CommandError> {
        let session = ctx
            .exec_query_first(GetSessionsByIds {
                ids: vec![self.id.clone()],
            })?
            .ok_or_else(|| CommandError {
                tx: ctx.tx().to_string(),
                command_id: ctx.command_id.to_string(),
                message: format!("Session {} not found", self.id),
            })?;

        let updated = Session {
            calendar_id: self.calendar_id.map(|s| s.to_string()),
            ..session.as_ref().clone()
        };

        ctx.emit_set(&updated)?;
        Ok(())
    }
}

#[myko_command(Arc<str>)]
pub struct CreateSession {
    pub name: String,
    pub scope_id: ProjectId,
    #[serde(default)]
    pub color: Option<String>,
}

impl CommandHandler for CreateSession {
    fn execute(self, ctx: CommandContext) -> Result<Arc<str>, CommandError> {
        let id: Arc<str> = Uuid::new_v4().to_string().into();
        let random_bright_color = bright_color_from_id(id.as_ref());
        let session = Session {
            id: SessionId(id.clone()),
            name: self.name,
            color: self.color.unwrap_or(random_bright_color),
            scope_id: self.scope_id,
            calendar_id: None,
            overview_scene_id: None,
        };

        ctx.emit_set(&session)?;
        Ok(id)
    }
}

fn bright_color_from_id(seed: &str) -> String {
    // FNV-1a style hash to spread hue values for nearby seeds.
    let mut hash: u32 = 0x811C9DC5;
    for b in seed.bytes() {
        hash ^= u32::from(b);
        hash = hash.wrapping_mul(0x01000193);
    }

    // Keep colors bright and saturated.
    let hue = (hash % 360) as f32;
    let saturation = 0.82_f32;
    let value = 0.95_f32;
    let (r, g, b) = hsv_to_rgb(hue, saturation, value);
    format!("#{r:02X}{g:02X}{b:02X}")
}

fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
    let c = v * s;
    let h_prime = (h / 60.0) % 6.0;
    let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
    let (r1, g1, b1) = if h_prime < 1.0 {
        (c, x, 0.0)
    } else if h_prime < 2.0 {
        (x, c, 0.0)
    } else if h_prime < 3.0 {
        (0.0, c, x)
    } else if h_prime < 4.0 {
        (0.0, x, c)
    } else if h_prime < 5.0 {
        (x, 0.0, c)
    } else {
        (c, 0.0, x)
    };

    let m = v - c;
    let to_u8 = |channel: f32| ((channel + m) * 255.0).round().clamp(0.0, 255.0) as u8;
    (to_u8(r1), to_u8(g1), to_u8(b1))
}

// SetSessionColor and RenameSession are auto-generated by #[myko_setter] and #[myko_rename]

#[myko_command]
pub struct CloneSession {
    pub id: SessionId,
}

impl CommandHandler for CloneSession {
    fn execute(self, ctx: CommandContext) -> Result<(), CommandError> {
        // TODO: Implement CloneSession handler.
        Err(CommandError {
            tx: ctx.tx().to_string(),
            command_id: ctx.command_id.to_string(),
            message: "CloneSession handler is not implemented".to_string(),
        })
    }
}

#[myko_command]
pub struct SetOverviewScene {
    pub id: SessionId,
    #[serde(default)]
    pub overview_scene_id: Option<Arc<str>>,
}

impl CommandHandler for SetOverviewScene {
    fn execute(self, ctx: CommandContext) -> Result<(), CommandError> {
        // TODO: Implement SetOverviewScene handler.
        Err(CommandError {
            tx: ctx.tx().to_string(),
            command_id: ctx.command_id.to_string(),
            message: "SetOverviewScene handler is not implemented".to_string(),
        })
    }
}