Validate SongProject consistency

This commit is contained in:
diyaa
2026-09-15 12:34:48 +02:00
parent 1ebb5aadc3
commit f42d782629
3 changed files with 136 additions and 1 deletions
@@ -0,0 +1,83 @@
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)
}
}
@@ -0,0 +1,52 @@
import MusicAssistantCore
import XCTest
final class SongProjectValidatorTests: XCTestCase {
func testValidatorReportsInvalidProjectRelationshipsAndTiming() {
let project = SongProject(
title: "Invalid Project",
idea: "Validate structure",
duration: SongDuration(seconds: 0),
bpm: ManualAutoValue(mode: .manual, value: 0),
sections: [
SongSection(id: "verse", type: .verse, title: "Verse", startTime: 12, endTime: 8),
SongSection(id: "verse", type: .chorus, title: "Chorus")
],
instruments: [
InstrumentTrack(
instrumentId: "piano",
placements: [InstrumentPlacement(sectionId: "missing", startTime: -1, endTime: 4)]
)
],
vocalists: [Vocalist(label: "Lead", assignedSections: ["missing"])]
)
let issues = SongProjectValidator().validate(project)
XCTAssertEqual(
issues,
[
.invalidDuration,
.invalidBPM(0),
.duplicateSectionID("verse"),
.invalidSectionTiming("verse"),
.invalidInstrumentTiming("piano"),
.unknownSectionReference("missing")
]
)
}
func testValidatorAcceptsConsistentProject() {
let project = SongProject(
title: "Valid Project",
idea: "Validate structure",
duration: SongDuration(seconds: 180),
bpm: ManualAutoValue(mode: .manual, value: 100),
sections: [SongSection(id: "verse", type: .verse, title: "Verse", startTime: 0, endTime: 30)],
instruments: [InstrumentTrack(instrumentId: "piano", placements: [InstrumentPlacement(sectionId: "verse", startTime: 0, endTime: 30)])],
vocalists: [Vocalist(label: "Lead", assignedSections: ["verse"])]
)
XCTAssertTrue(SongProjectValidator().validate(project).isEmpty)
}
}
+1 -1
View File
@@ -112,7 +112,7 @@ requirement is missing and blocks implementation, record it in
## Phase 9 --- Quality ## Phase 9 --- Quality
- [ ] Add validation for contradictory/invalid project settings. - [x] Add validation for contradictory/invalid project settings.
- [ ] Add loading, offline and provider-error states. - [ ] Add loading, offline and provider-error states.
- [ ] Add accessibility labels and keyboard navigation. - [ ] Add accessibility labels and keyboard navigation.
- [ ] Test project/version recovery. - [ ] Test project/version recovery.