import Foundation // Shapes of the server JSON (see server/timetable.js and server/directory.js), as in the // Android client's Models.kt. Missing keys fall back to defaults, so an older or newer // server never breaks decoding. struct Timetable: Codable, Hashable { var group: String var document: DocumentRef var period: String? var semester: Semester? var note: String? var subgroups: [String] = [] var bells: [Bell] = [] var from: String var days: [Day] = [] var weekly: [WeeklyDay] = [] var notes: [String] = [] } struct DocumentRef: Codable, Hashable { var id: String var name: String var faculty: String = "" var course: String = "" var modified: String? } struct Semester: Codable, Hashable { var id: String var label: String var start: String var end: String } struct Bell: Codable, Hashable { var pair: Int var start: String var end: String var halves: [[String]] = [] } struct Week: Codable, Hashable { var n: Int var color: String var white: Bool { color == "white" } } struct Day: Codable, Hashable { var date: String var weekday: Int var week: Week? var lessons: [Lesson] = [] } struct WeeklyDay: Codable, Hashable { var day: Int var lessons: [Lesson] = [] } struct Lesson: Codable, Hashable { var key: String var start: String var end: String? var pair: Int? var subject: String var type: String = "" var teachers: [String] = [] var rooms: [String] = [] var roomMoved: Bool = false var distance: Bool = false var condition: String = "" var variant: String = "all" var subgroup: String? var groups: [String] = [] var uncertain: Bool = false var raw: String = "" /** A recurring lesson the student may hide: the subject with its kind, in every week. */ var hideKey: String { subject.trimmed.lowercased() + "|" + type.trimmed.lowercased() } } struct GroupsResponse: Codable { var results: [GroupResult] = [] var facets: Facets = Facets() var total: Int = 0 var year: String = "" } struct GroupResult: Codable, Hashable, Identifiable { var group: String var fileId: String var file: String = "" var faculty: String = "" var course: String = "" var form: String = "" var period: String = "" var id: String { fileId + "|" + group } } struct Facets: Codable { var faculties: [Facet] = [] var courses: [Facet] = [] var forms: [Facet] = [] } struct Facet: Codable, Hashable { var name: String var count: Int } struct DirectorySearch: Codable { var teachers: [TeacherHit] = [] var rooms: [RoomHit] = [] } struct TeacherHit: Codable, Hashable { var name: String var count: Int = 0 var faculties: [String] = [] } struct RoomHit: Codable, Hashable { var room: String var count: Int = 0 } struct DirectoryDay: Codable { var date: String var lessons: [DirectoryLesson] = [] var coverage: Coverage } struct DirectoryLesson: Codable, Hashable { var start: String var end: String? var subject: String var type: String = "" var teachers: [String] = [] var rooms: [String] = [] var moved: Bool = false var distance: Bool = false var groups: [String] = [] var condition: String = "" var uncertain: Bool = false } struct Coverage: Codable { var documents: Int var total: Int var failed: [String] = [] var withoutSemester: [String] = [] var year: String = "" } struct CalendarInfo: Codable { var bells: [Bell] = [] } /** An exam, credit or consultation of the group, from any document of the catalog (/api/exams). */ struct Exam: Codable, Hashable { var date: String var start: String var end: String? var type: String var subject: String var teachers: [String] = [] var rooms: [String] = [] /** Consultations go into the list but not into the countdown. */ var counts: Bool { type != "Консультация" } } struct ExamsResponse: Codable { var group: String = "" var exams: [Exam] = [] } /** A group the student opened; kept on the device for quick switching. */ struct SavedGroup: Codable, Hashable { var docId: String var group: String var faculty: String = "" var course: String = "" /** "docId|group": notes, marks and choices of one group never show up in another. */ var key: String { docId + "|" + group } } /** * A homework task or a plain note pinned to one lesson (subject, date, start time), * or a general plan with an empty subject. Tasks can be ticked off; notes just remind. */ struct Note: Codable, Hashable, Identifiable { var id: String = UUID().uuidString var group: String var subject: String var date: String var start: String var text: String var task: Bool = true var done: Bool = false var created: Double = Date().timeIntervalSince1970 * 1000 /** A plan not pinned to any lesson: just a day. */ var general: Bool { subject.trimmed.isEmpty } } /** Whether the student was at one lesson. */ struct Mark: Codable, Hashable { var group: String var subject: String var date: String var start: String var present: Bool var id: String { [group, date, start, subject.trimmed.lowercased()].joined(separator: "|") } } extension String { var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } } // MARK: - Lenient decoding /// Keys may be missing in older answers or caches: every optional field falls back to its default. extension Timetable { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) group = try c.decode(String.self, forKey: .group) document = try c.decode(DocumentRef.self, forKey: .document) period = try c.decodeIfPresent(String.self, forKey: .period) semester = try c.decodeIfPresent(Semester.self, forKey: .semester) note = try c.decodeIfPresent(String.self, forKey: .note) subgroups = try c.decodeIfPresent([String].self, forKey: .subgroups) ?? [] bells = try c.decodeIfPresent([Bell].self, forKey: .bells) ?? [] from = try c.decodeIfPresent(String.self, forKey: .from) ?? "" days = try c.decodeIfPresent([Day].self, forKey: .days) ?? [] weekly = try c.decodeIfPresent([WeeklyDay].self, forKey: .weekly) ?? [] notes = try c.decodeIfPresent([String].self, forKey: .notes) ?? [] } } extension DocumentRef { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) name = try c.decodeIfPresent(String.self, forKey: .name) ?? "" faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? "" course = try c.decodeIfPresent(String.self, forKey: .course) ?? "" modified = try c.decodeIfPresent(String.self, forKey: .modified) } } extension Bell { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) pair = try c.decode(Int.self, forKey: .pair) start = try c.decode(String.self, forKey: .start) end = try c.decode(String.self, forKey: .end) halves = try c.decodeIfPresent([[String]].self, forKey: .halves) ?? [] } } extension Day { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) date = try c.decode(String.self, forKey: .date) weekday = try c.decodeIfPresent(Int.self, forKey: .weekday) ?? 0 week = try c.decodeIfPresent(Week.self, forKey: .week) lessons = try c.decodeIfPresent([Lesson].self, forKey: .lessons) ?? [] } } extension WeeklyDay { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) day = try c.decode(Int.self, forKey: .day) lessons = try c.decodeIfPresent([Lesson].self, forKey: .lessons) ?? [] } } extension Lesson { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) key = try c.decodeIfPresent(String.self, forKey: .key) ?? UUID().uuidString start = try c.decode(String.self, forKey: .start) end = try c.decodeIfPresent(String.self, forKey: .end) pair = try c.decodeIfPresent(Int.self, forKey: .pair) subject = try c.decode(String.self, forKey: .subject) type = try c.decodeIfPresent(String.self, forKey: .type) ?? "" teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? [] rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? [] roomMoved = try c.decodeIfPresent(Bool.self, forKey: .roomMoved) ?? false distance = try c.decodeIfPresent(Bool.self, forKey: .distance) ?? false condition = try c.decodeIfPresent(String.self, forKey: .condition) ?? "" variant = try c.decodeIfPresent(String.self, forKey: .variant) ?? "all" subgroup = try c.decodeIfPresent(String.self, forKey: .subgroup) groups = try c.decodeIfPresent([String].self, forKey: .groups) ?? [] uncertain = try c.decodeIfPresent(Bool.self, forKey: .uncertain) ?? false raw = try c.decodeIfPresent(String.self, forKey: .raw) ?? "" } } extension GroupsResponse { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) results = try c.decodeIfPresent([GroupResult].self, forKey: .results) ?? [] facets = try c.decodeIfPresent(Facets.self, forKey: .facets) ?? Facets() total = try c.decodeIfPresent(Int.self, forKey: .total) ?? 0 year = try c.decodeIfPresent(String.self, forKey: .year) ?? "" } } extension GroupResult { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) group = try c.decode(String.self, forKey: .group) fileId = try c.decode(String.self, forKey: .fileId) file = try c.decodeIfPresent(String.self, forKey: .file) ?? "" faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? "" course = try c.decodeIfPresent(String.self, forKey: .course) ?? "" form = try c.decodeIfPresent(String.self, forKey: .form) ?? "" period = try c.decodeIfPresent(String.self, forKey: .period) ?? "" } } extension Facets { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) faculties = try c.decodeIfPresent([Facet].self, forKey: .faculties) ?? [] courses = try c.decodeIfPresent([Facet].self, forKey: .courses) ?? [] forms = try c.decodeIfPresent([Facet].self, forKey: .forms) ?? [] } } extension DirectorySearch { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) teachers = try c.decodeIfPresent([TeacherHit].self, forKey: .teachers) ?? [] rooms = try c.decodeIfPresent([RoomHit].self, forKey: .rooms) ?? [] } } extension TeacherHit { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) name = try c.decode(String.self, forKey: .name) count = try c.decodeIfPresent(Int.self, forKey: .count) ?? 0 faculties = try c.decodeIfPresent([String].self, forKey: .faculties) ?? [] } } extension RoomHit { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) room = try c.decode(String.self, forKey: .room) count = try c.decodeIfPresent(Int.self, forKey: .count) ?? 0 } } extension DirectoryDay { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) date = try c.decode(String.self, forKey: .date) lessons = try c.decodeIfPresent([DirectoryLesson].self, forKey: .lessons) ?? [] coverage = try c.decode(Coverage.self, forKey: .coverage) } } extension DirectoryLesson { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) start = try c.decode(String.self, forKey: .start) end = try c.decodeIfPresent(String.self, forKey: .end) subject = try c.decode(String.self, forKey: .subject) type = try c.decodeIfPresent(String.self, forKey: .type) ?? "" teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? [] rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? [] moved = try c.decodeIfPresent(Bool.self, forKey: .moved) ?? false distance = try c.decodeIfPresent(Bool.self, forKey: .distance) ?? false groups = try c.decodeIfPresent([String].self, forKey: .groups) ?? [] condition = try c.decodeIfPresent(String.self, forKey: .condition) ?? "" uncertain = try c.decodeIfPresent(Bool.self, forKey: .uncertain) ?? false } } extension Coverage { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) documents = try c.decodeIfPresent(Int.self, forKey: .documents) ?? 0 total = try c.decodeIfPresent(Int.self, forKey: .total) ?? 0 failed = try c.decodeIfPresent([String].self, forKey: .failed) ?? [] withoutSemester = try c.decodeIfPresent([String].self, forKey: .withoutSemester) ?? [] year = try c.decodeIfPresent(String.self, forKey: .year) ?? "" } } extension CalendarInfo { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) bells = try c.decodeIfPresent([Bell].self, forKey: .bells) ?? [] } } extension Exam { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) date = try c.decode(String.self, forKey: .date) start = try c.decodeIfPresent(String.self, forKey: .start) ?? "" end = try c.decodeIfPresent(String.self, forKey: .end) type = try c.decodeIfPresent(String.self, forKey: .type) ?? "" subject = try c.decode(String.self, forKey: .subject) teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? [] rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? [] } } extension ExamsResponse { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) group = try c.decodeIfPresent(String.self, forKey: .group) ?? "" exams = try c.decodeIfPresent([Exam].self, forKey: .exams) ?? [] } } extension SavedGroup { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) docId = try c.decode(String.self, forKey: .docId) group = try c.decode(String.self, forKey: .group) faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? "" course = try c.decodeIfPresent(String.self, forKey: .course) ?? "" } }