Files

84 lines
2.9 KiB
Swift

import Foundation
public protocol SongProjectValidating: Sendable {
func validate(_ project: SongProject) -> [SongProjectValidationIssue]
}
public enum SongProjectValidationIssue: Equatable, Sendable {
case invalidDuration
case invalidBPM(Int)
case duplicateSectionID(String)
case invalidSectionTiming(String)
case invalidInstrumentTiming(String)
case unknownSectionReference(String)
}
public struct SongProjectValidator: SongProjectValidating {
public init() {}
public func validate(_ project: SongProject) -> [SongProjectValidationIssue] {
var issues: [SongProjectValidationIssue] = []
let sectionIDs = Set(project.sections.map(\.id))
if project.duration?.seconds.map({ $0 <= 0 }) == true {
issues.append(.invalidDuration)
}
if let bpm = project.bpm?.value, bpm <= 0 {
issues.append(.invalidBPM(bpm))
}
for section in project.sections {
if project.sections.filter({ $0.id == section.id }).count > 1 {
append(.duplicateSectionID(section.id), to: &issues)
}
if invalidTiming(start: section.startTime, end: section.endTime) {
issues.append(.invalidSectionTiming(section.id))
}
}
for instrument in project.instruments {
for placement in instrument.placements {
if invalidTiming(start: placement.startTime, end: placement.endTime) {
issues.append(.invalidInstrumentTiming(instrument.instrumentId))
}
if let sectionID = placement.sectionId, !sectionIDs.contains(sectionID) {
append(.unknownSectionReference(sectionID), to: &issues)
}
}
}
for vocalist in project.vocalists {
for sectionID in vocalist.assignedSections where !sectionIDs.contains(sectionID) {
append(.unknownSectionReference(sectionID), to: &issues)
}
}
for point in project.emotionalArc {
if let sectionID = point.sectionID, !sectionIDs.contains(sectionID) {
append(.unknownSectionReference(sectionID), to: &issues)
}
}
for direction in project.productionDirections {
if let sectionID = direction.sectionID, !sectionIDs.contains(sectionID) {
append(.unknownSectionReference(sectionID), to: &issues)
}
}
return issues
}
private func invalidTiming(start: TimeInterval?, end: TimeInterval?) -> Bool {
guard let start, let end else { return start.map { $0 < 0 } ?? end.map { $0 < 0 } ?? false }
return start < 0 || end < 0 || end <= start
}
private func append(_ issue: SongProjectValidationIssue, to issues: inout [SongProjectValidationIssue]) {
guard !issues.contains(issue) else { return }
issues.append(issue)
}
}