84 lines
2.9 KiB
Swift
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)
|
|
}
|
|
}
|