213 lines
7.7 KiB
Python
213 lines
7.7 KiB
Python
from datetime import datetime, time
|
|
from typing import Any, Optional
|
|
|
|
DATE_FORMAT = "%Y-%m-%d"
|
|
TIME_FORMAT = "%H:%M"
|
|
|
|
|
|
def validate_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
errors: list[str] = []
|
|
|
|
if not isinstance(config, dict):
|
|
raise ValueError("config skal være et JSON-objekt")
|
|
|
|
start_date_value = config.get("start_date")
|
|
start_time_value = config.get("start_time")
|
|
|
|
if not isinstance(start_date_value, str):
|
|
errors.append("start_date skal være en streng i format YYYY-MM-DD")
|
|
start_date_obj = None
|
|
else:
|
|
try:
|
|
start_date_obj = datetime.strptime(start_date_value, DATE_FORMAT).date()
|
|
except ValueError:
|
|
errors.append("start_date skal være i format YYYY-MM-DD")
|
|
start_date_obj = None
|
|
|
|
if not isinstance(start_time_value, str):
|
|
errors.append("start_time skal være en streng i format HH:MM")
|
|
start_time_obj = None
|
|
else:
|
|
try:
|
|
t_hour, t_min = map(int, start_time_value.split(":"))
|
|
start_time_obj = time(t_hour, t_min)
|
|
except (ValueError, TypeError):
|
|
errors.append("start_time skal være i format HH:MM")
|
|
start_time_obj = None
|
|
|
|
blocked_weeks = config.get("blocked_weeks", [])
|
|
if blocked_weeks is None:
|
|
blocked_weeks = []
|
|
if not isinstance(blocked_weeks, list):
|
|
errors.append("blocked_weeks skal være en liste af heltal")
|
|
else:
|
|
invalid_weeks = [
|
|
week for week in blocked_weeks
|
|
if not isinstance(week, int) or week < 1 or week > 53
|
|
]
|
|
if invalid_weeks:
|
|
errors.append(
|
|
"blocked_weeks skal indeholde heltal mellem 1 og 53: "
|
|
f"{invalid_weeks}"
|
|
)
|
|
|
|
blocked_dates = config.get("blocked_dates", [])
|
|
if blocked_dates is None:
|
|
blocked_dates = []
|
|
if not isinstance(blocked_dates, list):
|
|
errors.append("blocked_dates skal være en liste af dato-strenge")
|
|
else:
|
|
invalid_dates = []
|
|
blocked_full_dates: list[str] = []
|
|
blocked_month_days: list[str] = []
|
|
for date_value in blocked_dates:
|
|
if not isinstance(date_value, str):
|
|
invalid_dates.append(date_value)
|
|
continue
|
|
try:
|
|
date_obj = datetime.strptime(date_value, DATE_FORMAT).date()
|
|
except ValueError:
|
|
month_day = _parse_month_day(date_value)
|
|
if month_day:
|
|
blocked_month_days.append(month_day)
|
|
else:
|
|
invalid_dates.append(date_value)
|
|
else:
|
|
blocked_full_dates.append(date_obj.strftime(DATE_FORMAT))
|
|
if invalid_dates:
|
|
errors.append(
|
|
"blocked_dates skal være i format YYYY-MM-DD eller MM-DD: "
|
|
f"{invalid_dates}"
|
|
)
|
|
|
|
if "skip_day_after_ascension" in config and not isinstance(
|
|
config["skip_day_after_ascension"], bool
|
|
):
|
|
errors.append("skip_day_after_ascension skal være true/false")
|
|
|
|
for key in ("repo_url", "organizer_email", "uid_namespace"):
|
|
if key in config and config[key] is not None and not isinstance(config[key], str):
|
|
errors.append(f"{key} skal være en streng")
|
|
|
|
if errors:
|
|
raise ValueError("Konfigurationsfejl:\n- " + "\n- ".join(errors))
|
|
|
|
config["start_date_obj"] = start_date_obj
|
|
config["start_time_obj"] = start_time_obj
|
|
config["blocked_weeks"] = blocked_weeks
|
|
config["blocked_dates"] = blocked_full_dates
|
|
config["blocked_month_days"] = blocked_month_days
|
|
return config
|
|
|
|
|
|
def validate_story_data(story_data: Any) -> dict[str, Any]:
|
|
errors: list[str] = []
|
|
|
|
if not isinstance(story_data, dict):
|
|
raise ValueError("Story JSON skal være et objekt med meta og events")
|
|
|
|
meta = story_data.get("meta")
|
|
if not isinstance(meta, dict):
|
|
errors.append("meta skal være et objekt")
|
|
else:
|
|
name = meta.get("name")
|
|
if not isinstance(name, str) or not name.strip():
|
|
errors.append("meta.name skal være en ikke-tom streng")
|
|
for key in ("id", "theme_color", "text_color", "bg_color", "font", "log_prefix"):
|
|
value = meta.get(key)
|
|
if value is not None and not isinstance(value, str):
|
|
errors.append(f"meta.{key} skal være en streng hvis den er sat")
|
|
|
|
events = story_data.get("events")
|
|
if not isinstance(events, list):
|
|
errors.append("events skal være en liste")
|
|
else:
|
|
for idx, event_entry in enumerate(events, start=1):
|
|
if not isinstance(event_entry, dict):
|
|
errors.append(f"events[{idx}] skal være et objekt")
|
|
continue
|
|
title = event_entry.get("title")
|
|
if not isinstance(title, str) or not title.strip():
|
|
errors.append(f"events[{idx}].title skal være en ikke-tom streng")
|
|
story = event_entry.get("story")
|
|
if not isinstance(story, str) or not story.strip():
|
|
errors.append(f"events[{idx}].story skal være en ikke-tom streng")
|
|
hint = event_entry.get("hint")
|
|
if hint is not None and not isinstance(hint, str):
|
|
errors.append(f"events[{idx}].hint skal være en streng hvis den er sat")
|
|
event_id = event_entry.get("id")
|
|
if event_id is not None and not isinstance(event_id, str):
|
|
errors.append(f"events[{idx}].id skal være en streng hvis den er sat")
|
|
uid = event_entry.get("uid")
|
|
if uid is not None and not isinstance(uid, str):
|
|
errors.append(f"events[{idx}].uid skal være en streng hvis den er sat")
|
|
|
|
if errors:
|
|
raise ValueError("Historie-fejl:\n- " + "\n- ".join(errors))
|
|
|
|
return story_data
|
|
|
|
|
|
def _parse_month_day(value: str) -> Optional[str]:
|
|
parts = value.split("-")
|
|
if len(parts) != 2:
|
|
return None
|
|
if not all(part.isdigit() for part in parts):
|
|
return None
|
|
month = int(parts[0])
|
|
day = int(parts[1])
|
|
try:
|
|
datetime(2000, month, day)
|
|
except ValueError:
|
|
return None
|
|
return f"{month:02d}-{day:02d}"
|
|
|
|
|
|
def _events_missing_ids(events_data: list[dict[str, Any]]) -> list[int]:
|
|
missing: list[int] = []
|
|
for idx, event_entry in enumerate(events_data, start=1):
|
|
uid_value = event_entry.get("uid")
|
|
if isinstance(uid_value, str):
|
|
uid_value = uid_value.strip() or None
|
|
if not event_entry.get("id") and not uid_value:
|
|
missing.append(idx)
|
|
return missing
|
|
|
|
|
|
def validate_uid_mapping(
|
|
events_data: list[dict[str, Any]],
|
|
duplicate_titles: set[str],
|
|
log_totals: set[int],
|
|
) -> bool:
|
|
ambiguous_reasons: list[str] = []
|
|
|
|
if duplicate_titles:
|
|
titles_list = ", ".join(sorted(duplicate_titles))
|
|
ambiguous_reasons.append(f"duplikerede titler i eksisterende .ics ({titles_list})")
|
|
|
|
if len(log_totals) > 1:
|
|
totals_list = ", ".join(str(total) for total in sorted(log_totals))
|
|
ambiguous_reasons.append(f"flere log-totaler i eksisterende .ics ({totals_list})")
|
|
elif len(log_totals) == 1:
|
|
total = next(iter(log_totals))
|
|
if total != len(events_data):
|
|
ambiguous_reasons.append(
|
|
"log-total matcher ikke antal events i story "
|
|
f"({total} != {len(events_data)})"
|
|
)
|
|
|
|
if not ambiguous_reasons:
|
|
return True
|
|
|
|
missing = _events_missing_ids(events_data)
|
|
if missing:
|
|
missing_list = ", ".join(str(index) for index in missing)
|
|
raise ValueError(
|
|
"Ambivalent UID-mapping: "
|
|
+ "; ".join(ambiguous_reasons)
|
|
+ ". Tilfoej events[].id eller events[].uid for events: "
|
|
+ missing_list
|
|
)
|
|
|
|
return False
|