import Foundation final class MetadataInfoLogger { private let input: URL private var numberOfMetadataFiles = 0 private var unusedProperties: [(name: String, source: String)] = [] private var invalidProperties: [(name: String, source: String, reason: String)] = [] private var unknownProperties: [(name: String, source: String)] = [] private var missingProperties: [(name: String, source: String)] = [] private var unreadableMetadata: [(source: String, error: Error)] = [] private var warnings: [(source: String, message: String)] = [] private var errors: [(source: String, message: String)] = [] init(input: URL) { self.input = input } /** Adds an info message if a value is set for an unused property. - Note: Unused properties do not cause an element to be skipped. */ @discardableResult func unused(_ value: Optional, _ name: String, source: String) -> T where T: DefaultValueProvider { if let value { unusedProperties.append((name, source)) return value } return T.defaultValue } /** Cast a string value to another value, and using a default in case of errors. - Note: Invalid string values do not cause an element to be skipped. */ func cast(_ value: String, _ name: String, source: String) -> T where T: DefaultValueProvider, T: StringProperty { guard let result = T.init(value) else { invalidProperties.append((name: name, source: source, reason: T.castFailureReason)) return T.defaultValue } return result } /** Cast a string value to another value, and using a default in case of errors or missing values. - Note: Invalid string values do not cause an element to be skipped. */ func cast(_ value: String?, _ name: String, source: String) -> T where T: DefaultValueProvider, T: StringProperty { guard let value else { return T.defaultValue } guard let result = T.init(value) else { invalidProperties.append((name: name, source: source, reason: T.castFailureReason)) return T.defaultValue } return result } /** Cast the string value of an unused property to another value, and using a default in case of errors. - Note: Invalid string values do not cause an element to be skipped. */ func castUnused(_ value: String?, _ name: String, source: String) -> R where R: DefaultValueProvider, R: StringProperty { unused(value.unwrapped { cast($0, name, source: source) }, name, source: source) } /** Note an unknown property. - Note: Unknown properties do not cause an element to be skipped. */ func unknown(property: String, source: String) { unknownProperties.append((name: property, source: source)) } /** Ensure that a property is set, and aborting metadata decoding. - Note: Missing required properties cause an element to be skipped. */ func required(_ value: T?, name: String, source: String, _ valid: inout Bool) -> T where T: DefaultValueProvider { guard let value = value else { missingProperties.append((name, source)) valid = false return T.defaultValue } return value } func warning(_ message: String, source: String) { warnings.append((source, message)) } func error(_ message: String, source: String) { errors.append((source, message)) } func failedToDecodeMetadata(source: String, error: Error) { unreadableMetadata.append((source, error)) } func readPotentialMetadata(atPath path: String, source: String) -> Data? { let url = input.appendingPathComponent(path) guard url.exists else { return nil } numberOfMetadataFiles += 1 printMetadataScanUpdate() do { return try Data(contentsOf: url) } catch { unreadableMetadata.append((source, error)) return nil } } // MARK: Printing private func printMetadataScanUpdate() { print(" Pages found: \(numberOfMetadataFiles) \r", terminator: "") } func printMetadataScanOverview(languages: Int) { var notes = [String]() func addIfNotZero(_ sequence: Array, _ name: String) { guard sequence.count > 0 else { return } notes.append("\(sequence.count) \(name)") } addIfNotZero(warnings, "warnings") addIfNotZero(errors, "errors") addIfNotZero(unreadableMetadata, "unreadable files") addIfNotZero(unusedProperties, "unused properties") addIfNotZero(invalidProperties, "invalid properties") addIfNotZero(unknownProperties, "unknown properties") addIfNotZero(missingProperties, "missing properties") print(" Pages found: \(numberOfMetadataFiles) ") print(" Languages: \(languages)") if !notes.isEmpty { print(" Notes: " + notes.joined(separator: ", ")) } } func writeResults(to file: URL) { guard !errors.isEmpty || !warnings.isEmpty || !unreadableMetadata.isEmpty || !unusedProperties.isEmpty || !invalidProperties.isEmpty || !unknownProperties.isEmpty || !missingProperties.isEmpty else { do { if FileManager.default.fileExists(atPath: file.path) { try FileManager.default.removeItem(at: file) } } catch { print(" Failed to delete metadata log: \(error)") } return } var lines: [String] = [] func add(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence { let elements = property.map { " " + convert($0) }.sorted() guard !elements.isEmpty else { return } lines.append("\(name):") lines.append(contentsOf: elements) } add("Errors", errors) { "\($0.source): \($0.message)" } add("Warnings", warnings) { "\($0.source): \($0.message)" } add("Unreadable files", unreadableMetadata) { "\($0.source): \($0.error)" } add("Unused properties", unusedProperties) { "\($0.source): \($0.name)" } add("Invalid properties", invalidProperties) { "\($0.source): \($0.name) (\($0.reason))" } add("Unknown properties", unknownProperties) { "\($0.source): \($0.name)" } add("Missing properties", missingProperties) { "\($0.source): \($0.name)" } let data = lines.joined(separator: "\n").data(using: .utf8)! do { try data.createFolderAndWrite(to: file) } catch { print(" Failed to save metadata log: \(error)") } } }