// Moonlight - Validation モジュール
// 要素の操作可能性を検証し、UI層での操作制御に使用
// ============================================================
// 検証レベルと結果
// ============================================================
///|
/// 検証問題の重大度
pub(all) enum ValidationLevel {
Error // 致命的な問題(操作不可)
Warning // 警告(操作可能だが注意が必要)
Info // 情報(参考)
} derive(Show, Eq)
///|
/// 検証で見つかった問題
pub(all) struct ValidationIssue {
level : ValidationLevel
element_id : String? // 問題のある要素ID(None = 全体的な問題)
code : String // 問題コード(例: "INVALID_PARENT", "ORPHAN_CONNECTION")
message : String // 人間が読めるメッセージ
} derive(Show, Eq)
///|
/// 要素の操作権限
pub(all) struct ElementCapability {
can_move : Bool // 移動可能
can_resize : Bool // リサイズ可能
can_edit : Bool // プロパティ編集可能
can_delete : Bool // 削除可能
can_connect : Bool // 接続可能(Line要素)
can_add_child : Bool // 子要素追加可能
} derive(Show, Eq)
///|
/// 全操作可能
pub fn ElementCapability::full() -> ElementCapability {
{
can_move: true,
can_resize: true,
can_edit: true,
can_delete: true,
can_connect: true,
can_add_child: true,
}
}
///|
/// 移動のみ可能
pub fn ElementCapability::move_only() -> ElementCapability {
{
can_move: true,
can_resize: false,
can_edit: false,
can_delete: false,
can_connect: false,
can_add_child: false,
}
}
///|
/// 移動と削除のみ可能(問題のある要素を除去可能にする)
pub fn ElementCapability::move_and_delete() -> ElementCapability {
{
can_move: true,
can_resize: false,
can_edit: false,
can_delete: true,
can_connect: false,
can_add_child: false,
}
}
///|
/// 全操作不可
pub fn ElementCapability::none() -> ElementCapability {
{
can_move: false,
can_resize: false,
can_edit: false,
can_delete: false,
can_connect: false,
can_add_child: false,
}
}
///|
/// 検証結果
pub(all) struct ValidationResult {
is_valid : Bool // 全体として有効か
issues : Array[ValidationIssue] // 見つかった問題
capabilities : Map[String, ElementCapability] // 要素ごとの操作権限
} derive(Show)
///|
/// 空の検証結果(有効)
pub fn ValidationResult::valid() -> ValidationResult {
{ is_valid: true, issues: [], capabilities: {} }
}
// ============================================================
// 問題コード定数
// ============================================================
///|
pub let issue_invalid_id : String = "INVALID_ID"
///|
pub let issue_duplicate_id : String = "DUPLICATE_ID"
///|
pub let issue_invalid_parent : String = "INVALID_PARENT"
///|
pub let issue_circular_parent : String = "CIRCULAR_PARENT"
///|
pub let issue_orphan_connection : String = "ORPHAN_CONNECTION"
///|
pub let issue_self_connection : String = "SELF_CONNECTION"
///|
pub let issue_invalid_shape : String = "INVALID_SHAPE"
///|
pub let issue_negative_dimension : String = "NEGATIVE_DIMENSION"
///|
pub let issue_line_same_points : String = "LINE_SAME_POINTS"
///|
pub let issue_text_empty : String = "TEXT_EMPTY"
///|
/// 無効な座標(NaN, Infinity)
pub let issue_invalid_coordinates : String = "INVALID_COORDINATES"
///|
/// プレーンSVG要素(Moonlight形式でない)
pub let issue_plain_svg_element : String = "PLAIN_SVG_ELEMENT"
///|
/// 未サポートのシェープタイプ
pub let issue_unsupported_shape : String = "UNSUPPORTED_SHAPE"
// ============================================================
// ヘルパー関数
// ============================================================
///|
/// 値が有限値かどうかをチェック(NaN, Infinity でない)
fn is_finite(value : Double) -> Bool {
// NaN は自分自身と等しくない
if value != value {
return false
}
// Infinity チェック
if value > 1.0e308 || value < -1.0e308 {
return false
}
true
}
///|
/// ValidationIssue を追加するヘルパー
fn add_issue(
issues : Array[ValidationIssue],
level : ValidationLevel,
element_id : String,
code : String,
message : String,
) -> Unit {
issues.push({ level, element_id: Some(element_id), code, message })
}
///|
/// 要素が存在するかチェック
fn element_exists(elements : Array[Element], id : String) -> Bool {
elements.iter().any(fn(e) { e.id == id })
}
///|
/// IDで要素を検索
fn find_element(elements : Array[Element], id : String) -> Element? {
for el in elements {
if el.id == id {
return Some(el)
}
}
None
}
// ============================================================
// 検証関数
// ============================================================
///|
/// 要素配列を検証し、結果を返す
pub fn validate_elements(elements : Array[Element]) -> ValidationResult {
let issues : Array[ValidationIssue] = []
let capabilities : Map[String, ElementCapability] = {}
// ID の収集と重複チェック
let id_set : Map[String, Int] = {} // id -> count
for el in elements {
match id_set.get(el.id) {
Some(count) => id_set[el.id] = count + 1
None => id_set[el.id] = 1
}
}
// 各要素を検証
for el in elements {
let el_issues = validate_element(el, elements, id_set)
for issue in el_issues {
issues.push(issue)
}
// 操作権限を決定
let cap = determine_capability(el, el_issues)
capabilities[el.id] = cap
}
// 全体の有効性を判定(Errorがなければ有効)
let has_errors = issues.iter().any(fn(i) { i.level == Error })
{ is_valid: not(has_errors), issues, capabilities }
}
///|
/// 単一要素を検証
fn validate_element(
el : Element,
all_elements : Array[Element],
id_counts : Map[String, Int],
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
// ID検証
if el.id == "" {
add_issue(issues, Error, el.id, issue_invalid_id, "Element has empty ID")
}
// 座標の有効性検証(NaN, Infinity チェック)
if not(is_finite(el.x)) || not(is_finite(el.y)) {
add_issue(
issues,
Error,
el.id,
issue_invalid_coordinates,
"Element has invalid coordinates (NaN or Infinity)",
)
}
// 重複ID検証
match id_counts.get(el.id) {
Some(count) if count > 1 =>
add_issue(
issues,
Error,
el.id,
issue_duplicate_id,
"Duplicate element ID: \{el.id}",
)
_ => ()
}
// 親要素検証
match el.parent_id {
Some(pid) => {
if not(element_exists(all_elements, pid)) {
add_issue(
issues,
Warning,
el.id,
issue_invalid_parent,
"Parent element '\{pid}' does not exist",
)
}
// 循環参照チェック
if has_circular_parent(el.id, all_elements) {
add_issue(
issues,
Error,
el.id,
issue_circular_parent,
"Circular parent reference detected",
)
}
}
None => ()
}
// 接続検証(Line要素)
match el.connections {
Some(conns) => {
validate_connection(el.id, conns.start, all_elements, issues)
validate_connection(el.id, conns.end, all_elements, issues)
// 自己接続チェック
let self_connected = match (conns.start, conns.end) {
(Some(s), Some(e)) => s.element_id == e.element_id
_ => false
}
if self_connected {
add_issue(
issues,
Warning,
el.id,
issue_self_connection,
"Line connects element to itself",
)
}
}
None => ()
}
// 形状固有の検証
validate_shape(el, issues)
issues
}
///|
/// 接続を検証
fn validate_connection(
line_id : String,
conn : Connection?,
all_elements : Array[Element],
issues : Array[ValidationIssue],
) -> Unit {
match conn {
Some(c) =>
if not(element_exists(all_elements, c.element_id)) {
add_issue(
issues,
Warning,
line_id,
issue_orphan_connection,
"Connected element '\{c.element_id}' does not exist",
)
}
None => ()
}
}
///|
/// 形状固有の検証
fn validate_shape(el : Element, issues : Array[ValidationIssue]) -> Unit {
match el.shape {
Rect(w, h, _, _) =>
if w < 0.0 || h < 0.0 {
add_issue(
issues,
Error,
el.id,
issue_negative_dimension,
"Rectangle has negative dimensions",
)
}
Circle(r) =>
if r < 0.0 {
add_issue(
issues,
Error,
el.id,
issue_negative_dimension,
"Circle has negative radius",
)
}
Ellipse(rx, ry) =>
if rx < 0.0 || ry < 0.0 {
add_issue(
issues,
Error,
el.id,
issue_negative_dimension,
"Ellipse has negative radius",
)
}
Line(x2, y2) =>
if el.x == x2 && el.y == y2 {
add_issue(
issues,
Warning,
el.id,
issue_line_same_points,
"Line start and end points are the same",
)
}
Text(content, _) =>
if content == "" {
add_issue(
issues,
Info,
el.id,
issue_text_empty,
"Text element has empty content",
)
}
_ => ()
}
}
///|
/// 循環参照をチェック
fn has_circular_parent(element_id : String, elements : Array[Element]) -> Bool {
let visited : Array[String] = []
let mut current_id = element_id
// 最大 elements.length() + 1 回のイテレーションでサイクルを検出
for _ in 0..<(elements.length() + 1) {
if visited.contains(current_id) {
return true
}
visited.push(current_id)
// 現在の要素を探して親IDを取得
match find_element(elements, current_id) {
Some(el) =>
match el.parent_id {
Some(pid) => current_id = pid
None => return false
}
None => return false
}
}
false
}
///|
/// 問題に基づいて操作権限を決定
fn determine_capability(
el : Element,
issues : Array[ValidationIssue],
) -> ElementCapability {
// エラーがあるかチェック
let has_errors = issues.iter().any(fn(i) { i.level == Error })
if has_errors {
// エラーがあっても移動と削除は許可
return ElementCapability::move_and_delete()
}
// 警告でも move_only にすべき重大なケースをチェック
let has_critical_warning = issues
.iter()
.any(fn(i) {
i.level == Warning &&
(i.code == issue_invalid_parent || i.code == issue_orphan_connection)
})
if has_critical_warning {
return ElementCapability::move_and_delete()
}
// 形状に応じた権限
match el.shape {
Line(_, _) =>
{
can_move: true,
can_resize: true,
can_edit: true,
can_delete: true,
can_connect: true,
can_add_child: false, // Line には子要素を追加できない
}
Text(_, _) =>
{
can_move: true,
can_resize: true,
can_edit: true,
can_delete: true,
can_connect: false, // Text には接続できない
can_add_child: false,
}
_ => ElementCapability::full()
}
}
// ============================================================
// 便利な検証ヘルパー
// ============================================================
///|
/// 要素が操作可能か判定
pub fn is_element_operable(
result : ValidationResult,
element_id : String,
) -> Bool {
match result.capabilities.get(element_id) {
Some(cap) =>
cap.can_move || cap.can_resize || cap.can_edit || cap.can_delete
None => false
}
}
///|
/// 要素の操作権限を取得
pub fn get_element_capability(
result : ValidationResult,
element_id : String,
) -> ElementCapability {
result.capabilities.get(element_id).unwrap_or(ElementCapability::none())
}
///|
/// 特定要素の問題を取得
pub fn get_element_issues(
result : ValidationResult,
element_id : String,
) -> Array[ValidationIssue] {
result.issues
.iter()
.filter(fn(i) {
match i.element_id {
Some(id) => id == element_id
None => false
}
})
.collect()
}
///|
/// エラーのみ取得
pub fn get_errors(result : ValidationResult) -> Array[ValidationIssue] {
result.issues.iter().filter(fn(i) { i.level == Error }).collect()
}
///|
/// 警告のみ取得
pub fn get_warnings(result : ValidationResult) -> Array[ValidationIssue] {
result.issues.iter().filter(fn(i) { i.level == Warning }).collect()
}
// ============================================================
// 増分検証(パフォーマンス最適化)
// ============================================================
///|
/// 単一要素の検証(他の要素との関係も考慮)
pub fn validate_single_element(
el : Element,
all_elements : Array[Element],
) -> Array[ValidationIssue] {
// ID カウントを作成
let id_counts : Map[String, Int] = {}
for e in all_elements {
match id_counts.get(e.id) {
Some(count) => id_counts[e.id] = count + 1
None => id_counts[e.id] = 1
}
}
validate_element(el, all_elements, id_counts)
}
///|
/// 要素が追加可能か事前チェック
pub fn can_add_element(
new_element : Element,
existing_elements : Array[Element],
) -> (Bool, Array[ValidationIssue]) {
// ID重複チェック
if element_exists(existing_elements, new_element.id) {
let issues : Array[ValidationIssue] = []
add_issue(
issues,
Error,
new_element.id,
issue_duplicate_id,
"Element with ID '\{new_element.id}' already exists",
)
return (false, issues)
}
// 新しい要素を追加した配列で検証
let all_elements = existing_elements.copy()
all_elements.push(new_element)
let issues = validate_single_element(new_element, all_elements)
let has_errors = issues.iter().any(fn(i) { i.level == Error })
(not(has_errors), issues)
}
///|
/// 要素が削除可能か事前チェック
pub fn can_delete_element(
element_id : String,
all_elements : Array[Element],
) -> (Bool, Array[ValidationIssue]) {
let issues : Array[ValidationIssue] = []
// 子要素があるかチェック
let has_children = all_elements
.iter()
.any(fn(e) {
match e.parent_id {
Some(pid) => pid == element_id
None => false
}
})
if has_children {
add_issue(
issues,
Warning,
element_id,
"HAS_CHILDREN",
"Element has child elements that will become orphaned",
)
}
// この要素に接続している Line があるかチェック
let connection_count = all_elements
.iter()
.filter(fn(el) {
match el.connections {
Some(conns) =>
match (conns.start, conns.end) {
(Some(s), _) if s.element_id == element_id => true
(_, Some(e)) if e.element_id == element_id => true
_ => false
}
None => false
}
})
.count()
if connection_count > 0 {
add_issue(
issues,
Warning,
element_id,
"HAS_CONNECTIONS",
"Element is connected by \{connection_count} line(s)",
)
}
// 警告のみなら削除可能
let has_errors = issues.iter().any(fn(i) { i.level == Error })
(not(has_errors), issues)
}
// ============================================================
// 整合性修復のヒント
// ============================================================
///|
/// 修復アクション
pub(all) enum RepairAction {
RemoveConnection(String, String) // (line_id, which: "start" | "end")
RemoveParent(String) // element_id
DeleteElement(String) // element_id
RegenerateId(String, String) // (old_id, new_id)
} derive(Show, Eq)
///|
/// 問題に対する修復アクションを提案
pub fn suggest_repairs(issues : Array[ValidationIssue]) -> Array[RepairAction] {
let actions : Array[RepairAction] = []
for issue in issues {
match (issue.code, issue.element_id) {
(code, Some(id)) if code == issue_orphan_connection =>
// 孤立した接続を削除
actions.push(RemoveConnection(id, "auto"))
(code, Some(id)) if code == issue_invalid_parent =>
// 無効な親を解除
actions.push(RemoveParent(id))
(code, Some(id)) if code == issue_circular_parent =>
// 循環参照を解消
actions.push(RemoveParent(id))
(code, Some(id)) if code == issue_duplicate_id =>
// 重複IDを再生成
actions.push(RegenerateId(id, id + "_copy"))
_ => ()
}
}
actions
}