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