Improve logging during element scanning

This commit is contained in:
Christoph Hagen 2022-12-01 14:50:26 +01:00
parent 4c2c4b7dd3
commit 1ceba25d4f
19 changed files with 370 additions and 233 deletions

View File

@ -0,0 +1,6 @@
import Foundation
protocol DefaultValueProvider {
static var defaultValue: Self { get }
}

View File

@ -57,7 +57,7 @@ extension Element {
- Note: If this value is inherited from the parent, if it is not defined. There must be at least one
element in the path that defines this property.
*/
let moreLinkText: String
let moreLinkText: String?
/**
The text on the back navigation link of **contained** elements.
@ -145,77 +145,52 @@ extension Element {
extension Element.LocalizedMetadata {
init?(atRoot folder: URL, data: GenericMetadata.LocalizedMetadata) {
init?(atRoot folder: URL, data: GenericMetadata.LocalizedMetadata, log: MetadataInfoLogger) {
// Go through all elements and check them for completeness
// In the end, check that all required elements are present
var isComplete = true
func markAsIncomplete() {
isComplete = false
}
var isValid = true
let source = "root"
self.language = log
.required(data.language, name: "language", source: source)
.ifNil(markAsIncomplete) ?? ""
self.title = log
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.language = log.required(data.language, name: "language", source: source, &isValid)
self.title = log.required(data.title, name: "title", source: source, &isValid)
self.subtitle = data.subtitle
self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
self.linkPreviewImage = data.linkPreviewImage
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source)
.ifNil(markAsIncomplete) ?? ""
self.moreLinkText = data.moreLinkText ?? Element.LocalizedMetadata.moreLinkDefaultText
self.backLinkText = log
.required(data.backLinkText, name: "backLinkText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid)
self.moreLinkText = data.moreLinkText
self.backLinkText = log.required(data.backLinkText, name: "backLinkText", source: source, &isValid)
self.parentBackLinkText = "" // Root has no parent
self.placeholderTitle = log
.required(data.placeholderTitle, name: "placeholderTitle", source: source)
.ifNil(markAsIncomplete) ?? ""
self.placeholderText = log
.required(data.placeholderText, name: "placeholderText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.placeholderTitle = log.required(data.placeholderTitle, name: "placeholderTitle", source: source, &isValid)
self.placeholderText = log.required(data.placeholderText, name: "placeholderText", source: source, &isValid)
self.titleSuffix = data.titleSuffix
self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source)
self.cornerText = log.unused(data.cornerText, "cornerText", source: source)
self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source)
self.relatedContentText = log
.required(data.relatedContentText, name: "relatedContentText", source: source) ?? ""
self.navigationTextAsNextPage = log
.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source) ?? ""
self.navigationTextAsPreviousPage = log
.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source) ?? ""
self.externalUrl = log.unused(data.externalUrl, "externalUrl", source: source)
self.relatedContentText = log.required(data.relatedContentText, name: "relatedContentText", source: source, &isValid)
self.navigationTextAsNextPage = log.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source, &isValid)
self.navigationTextAsPreviousPage = log.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source, &isValid)
guard isComplete else {
guard isValid else {
return nil
}
}
init?(folder: URL, data: GenericMetadata.LocalizedMetadata, source: String, parent: Element.LocalizedMetadata) {
init?(folder: URL, data: GenericMetadata.LocalizedMetadata, source: String, parent: Element.LocalizedMetadata, log: MetadataInfoLogger) {
// Go through all elements and check them for completeness
// In the end, check that all required elements are present
var isComplete = true
func markAsIncomplete() {
isComplete = false
}
var isValid = true
self.language = parent.language
self.title = log
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.title = log.required(data.title, name: "title", source: source, &isValid)
self.subtitle = data.subtitle
self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
self.linkPreviewImage = data.linkPreviewImage
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source)
.ifNil(markAsIncomplete) ?? ""
self.moreLinkText = log.moreLinkText(data.moreLinkText, parent: parent.moreLinkText, source: source)
self.linkPreviewDescription = log.required(linkPreviewDescription, name: "linkPreviewDescription", source: source, &isValid)
self.moreLinkText = log.required(data.moreLinkText ?? parent.moreLinkText, name: "moreLinkText", source: source, &isValid)
self.backLinkText = data.backLinkText ?? data.title ?? ""
self.parentBackLinkText = parent.backLinkText
self.placeholderTitle = data.placeholderTitle ?? parent.placeholderTitle
@ -228,7 +203,7 @@ extension Element.LocalizedMetadata {
self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage
self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage
guard isComplete else {
guard isValid else {
return nil
}
}

View File

@ -157,122 +157,130 @@ struct Element {
- Parameter folder: The root folder of the site content.
- Note: Uses global objects.
*/
init?(atRoot folder: URL) {
init?(atRoot folder: URL, log: MetadataInfoLogger) {
self.inputFolder = folder
self.path = ""
let source = GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else {
guard let metadata = GenericMetadata(source: source, log: log) else {
return nil
}
var isValid = true
self.id = metadata.customId ?? Element.defaultRootId
self.author = log.required(metadata.author, name: "author", source: source) ?? "author"
self.topBarTitle = log
.required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website"
self.date = log.unused(metadata.date, "date", source: source)
self.endDate = log.unused(metadata.endDate, "endDate", source: source)
self.state = log.state(metadata.state, source: source)
self.author = log.required(metadata.author, name: "author", source: source, &isValid)
self.topBarTitle = log.required(metadata.topBarTitle, name: "topBarTitle", source: source, &isValid)
self.date = log.castUnused(metadata.date, "date", source: source)
self.endDate = log.castUnused(metadata.endDate, "endDate", source: source)
self.state = log.cast(metadata.state, "state", source: source)
self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source)
self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? []
self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName
self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large
self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true
self.thumbnailStyle = log.castUnused(metadata.thumbnailStyle, "thumbnailStyle", source: source)
self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source)
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
self.headerType = log.headerType(metadata.headerType, source: source)
self.languages = log.required(metadata.languages, name: "languages", source: source)?
self.headerType = log.cast(metadata.headerType, "headerType", source: source)
self.showMostRecentSection = metadata.showMostRecentSection ?? false
self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid)
.compactMap { language in
.init(atRoot: folder, data: language)
} ?? []
.init(atRoot: folder, data: language, log: log)
}
// All properties initialized
guard !languages.isEmpty else {
log.add(error: "No languages found", source: source)
log.error("No languages found", source: source)
return nil
}
guard isValid else {
return nil
}
files.add(page: path, id: id)
self.readElements(in: folder, source: nil)
self.readElements(in: folder, source: nil, log: log)
}
mutating func readElements(in folder: URL, source: String?) {
mutating func readElements(in folder: URL, source: String?, log: MetadataInfoLogger) {
let subFolders: [URL]
do {
subFolders = try FileManager.default
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
.filter { $0.isDirectory }
} catch {
log.add(error: "Failed to read subfolders", source: source ?? "root", error: error)
log.error("Failed to read subfolders: \(error)", source: source ?? "root")
return
}
self.elements = subFolders.compactMap { subFolder in
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent
return Element(parent: self, folder: subFolder, path: s)
return Element(parent: self, folder: subFolder, path: s, log: log)
}
}
init?(parent: Element, folder: URL, path: String) {
init?(parent: Element, folder: URL, path: String, log: MetadataInfoLogger) {
self.inputFolder = folder
self.path = path
let source = path + "/" + GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else {
guard let metadata = GenericMetadata(source: source, log: log) else {
return nil
}
var isValid = true
self.id = metadata.customId ?? folder.lastPathComponent
self.author = metadata.author ?? parent.author
self.topBarTitle = log
.unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle
let date = log.date(from: metadata.date, property: "date", source: source).ifNil {
if !parent.useManualSorting {
log.add(error: "No 'date', but parent defines 'useManualSorting' = false", source: source)
}
}
self.date = date
self.endDate = log.date(from: metadata.endDate, property: "endDate", source: source).ifNotNil {
if date == nil {
log.add(warning: "Set 'endDate', but no 'date'", source: source)
}
}
let state = log.state(metadata.state, source: source)
self.state = state
self.sortIndex = metadata.sortIndex.ifNil {
if state != .hidden, parent.useManualSorting {
log.add(error: "No 'sortIndex', but parent defines 'useManualSorting' = true", source: source)
}
}
self.topBarTitle = log.unused(metadata.topBarTitle, "topBarTitle", source: source)
self.date = metadata.date.unwrapped { log.cast($0, "date", source: source) }
self.endDate = metadata.date.unwrapped { log.cast($0, "endDate", source: source) }
self.state = log.cast(metadata.state, "state", source: source)
self.sortIndex = metadata.sortIndex
// TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path)
self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path)
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? []
self.thumbnailPath = metadata.thumbnailPath ?? Element.defaultThumbnailName
self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source)
self.thumbnailStyle = log.cast(metadata.thumbnailStyle, "thumbnailStyle", source: source)
self.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
self.headerType = log.headerType(metadata.headerType, source: source)
self.headerType = log.cast(metadata.headerType, "headerType", source: source)
self.showMostRecentSection = metadata.showMostRecentSection ?? false
self.languages = parent.languages.compactMap { parentData in
guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else {
log.add(info: "Language '\(parentData.language)' not found", source: source)
log.warning("Language '\(parentData.language)' not found", source: source)
return nil
}
return .init(folder: folder, data: data, source: source, parent: parentData)
return .init(folder: folder, data: data, source: source, parent: parentData, log: log)
}
// Check that each 'language' tag is present, and that all languages appear in the parent
log.required(metadata.languages, name: "languages", source: source)?
.compactMap { log.required($0.language, name: "language", source: source) }
log.required(metadata.languages, name: "languages", source: source, &isValid)
.compactMap { log.required($0.language, name: "language", source: source, &isValid) }
.filter { language in
!parent.languages.contains { $0.language == language }
}
.forEach {
log.add(warning: "Language '\($0)' not found in parent, so not generated", source: source)
log.warning("Language '\($0)' not found in parent, so not generated", source: source)
}
// All properties initialized
if self.date == nil, !parent.useManualSorting {
log.error("No 'date', but parent defines 'useManualSorting' = false", source: source)
}
if date == nil {
log.unused(self.endDate, "endDate", source: source)
}
if self.sortIndex == nil, state != .hidden, parent.useManualSorting {
log.error("No 'sortIndex', but parent defines 'useManualSorting' = true", source: source)
}
guard isValid else {
return nil
}
files.add(page: path, id: id)
self.readElements(in: folder, source: path)
self.readElements(in: folder, source: path, log: log)
}
}

View File

@ -14,8 +14,8 @@ extension GenericMetadata {
let language: String?
/**
- Note: This field is mandatory
The title used in the page header.
- Note: This field is mandatory
*/
let title: String?

View File

@ -169,8 +169,8 @@ extension GenericMetadata {
- Note: The decoding routine also checks for unknown properties, and writes them to the output.
- Note: Uses global objects
*/
init?(source: String) {
guard let data = files.dataOfOptionalFile(atPath: source, source: source) else {
init?(source: String, log: MetadataInfoLogger) {
guard let data = log.readPotentialMetadata(atPath: source, source: source) else {
return nil
}
@ -196,8 +196,7 @@ extension GenericMetadata {
do {
self = try decoder.decode(from: data)
} catch {
print("Here \(data)")
log.failedToOpen(GenericMetadata.metadataFileName, requiredBy: source, error: error)
log.failedToDecodeMetadata(source: source, error: error)
return nil
}
}

View File

@ -17,3 +17,19 @@ enum HeaderType: String {
*/
case none
}
extension HeaderType: StringProperty {
init?(_ value: String) {
self.init(rawValue: value)
}
static var castFailureReason: String {
"Header type must be 'left', 'center' or 'none'"
}
}
extension HeaderType: DefaultValueProvider {
static var defaultValue: HeaderType { .left }
}

View File

@ -39,3 +39,19 @@ extension PageState {
}
}
}
extension PageState: StringProperty {
init?(_ value: String) {
self.init(rawValue: value)
}
static var castFailureReason: String {
"Page state must be 'standard', 'draft' or 'hidden'"
}
}
extension PageState: DefaultValueProvider {
static var defaultValue: PageState { .standard }
}

View File

@ -0,0 +1,8 @@
import Foundation
protocol StringProperty {
init?(_ value: String)
static var castFailureReason: String { get }
}

View File

@ -33,3 +33,19 @@ enum ThumbnailStyle: String, CaseIterable {
extension ThumbnailStyle: Codable {
}
extension ThumbnailStyle: StringProperty {
init?(_ value: String) {
self.init(rawValue: value)
}
static var castFailureReason: String {
"Thumbnail style must be 'large', 'square' or 'small'"
}
}
extension ThumbnailStyle: DefaultValueProvider {
static var defaultValue: ThumbnailStyle { .large }
}

View File

@ -0,0 +1,6 @@
import Foundation
extension Array: DefaultValueProvider {
static var defaultValue: Array<Element> { [] }
}

View File

@ -0,0 +1,6 @@
import Foundation
extension Bool: DefaultValueProvider {
static var defaultValue: Bool { true }
}

View File

@ -0,0 +1,26 @@
import Foundation
extension Date: StringProperty {
private static let metadataDate: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "dd.MM.yy"
return df
}()
init?(_ value: String) {
guard let date = Date.metadataDate.date(from: value) else {
return nil
}
self = date
}
static var castFailureReason: String {
"Date string format must be 'dd.MM.yy'"
}
}
extension Date: DefaultValueProvider {
static var defaultValue: Date { .init() }
}

View File

@ -0,0 +1,13 @@
import Foundation
extension Int: StringProperty {
static var castFailureReason: String {
"The string was not a valid integer"
}
}
extension Int: DefaultValueProvider {
static var defaultValue: Int { 0 }
}

View File

@ -3,7 +3,7 @@ import Metal
extension Optional {
func unwrapped<T>(_ closure: (Wrapped) -> T) -> T? {
func unwrapped<T>(_ closure: (Wrapped) -> T?) -> T? {
if case let .some(value) = self {
return closure(value)
}

View File

@ -79,3 +79,8 @@ extension String {
try data(using: .utf8)!.createFolderAndWrite(to: url)
}
}
extension String: DefaultValueProvider {
static var defaultValue: String { "" }
}

View File

@ -18,6 +18,8 @@ final class FileSystem {
input.appendingPathComponent(FileSystem.tempFileName)
}
let generatorInfoFolder: URL
/**
All files which should be copied to the output folder
*/
@ -70,7 +72,7 @@ final class FileSystem {
self.input = input
self.output = output
self.images = .init(input: input, output: output)
self.generatorInfoFolder = input.appendingPathComponent("run")
}
func urlInOutputFolder(_ path: String) -> URL {
@ -100,20 +102,6 @@ final class FileSystem {
}
}
func dataOfOptionalFile(atPath path: String, source: String) -> Data? {
let url = input.appendingPathComponent(path)
guard exists(url) else {
return nil
}
do {
return try Data(contentsOf: url)
} catch {
log.failedToOpen(path, requiredBy: source, error: error)
return nil
}
}
func contentOfOptionalFile(atPath path: String, source: String, createEmptyFileIfMissing: Bool = false) -> String? {
let url = input.appendingPathComponent(path)
guard exists(url) else {

View File

@ -46,119 +46,7 @@ final class ValidationLog {
add(info: .init(reason: reason, source: source, error: error))
}
@discardableResult
func unused<T, R>(_ value: Optional<T>, _ name: String, source: String) -> Optional<R> {
if value != nil {
add(info: "Unused property '\(name)'", source: source)
}
return nil
}
func unknown(property: String, source: String) {
add(info: "Unknown property '\(property)'", source: source)
}
func required<T>(_ value: Optional<T>, name: String, source: String) -> Optional<T> {
guard let value = value else {
add(error: "Missing property '\(name)'", source: source)
return nil
}
return value
}
func unexpected<T>(_ value: Optional<T>, name: String, source: String) -> Optional<T> {
if let value = value {
add(error: "Unexpected property '\(name)' = '\(value)'", source: source)
return nil
}
return nil
}
func missing(_ file: String, requiredBy source: String) {
print("[ERROR] Missing file '\(file)' required by \(source)")
}
func failedToOpen(_ file: String, requiredBy source: String, error: Error?) {
print("[ERROR] Failed to open file '\(file)' required by \(source): \(error?.localizedDescription ?? "No error provided")")
}
func state(_ raw: String?, source: String) -> PageState {
guard let raw = raw else {
return .standard
}
guard let state = PageState(rawValue: raw) else {
add(warning: "Invalid 'state' '\(raw)', using 'standard'", source: source)
return .standard
}
return state
}
func headerType(_ raw: String?, source: String) -> HeaderType {
guard let raw = raw else {
return .left
}
guard let type = HeaderType(rawValue: raw) else {
add(warning: "Invalid 'headerType' '\(raw)', using 'left'", source: source)
return .left
}
return type
}
func thumbnailStyle(_ raw: String?, source: String) -> ThumbnailStyle {
guard let raw = raw else {
return .large
}
guard let style = ThumbnailStyle(rawValue: raw) else {
add(warning: "Invalid 'thumbnailStyle' '\(raw)', using 'large'", source: source)
return .large
}
return style
}
func linkPreviewThumbnail(customFile: String?, for language: String, in folder: URL, source: String) -> String? {
guard let customFile = customFile else {
return nil
}
let customFileUrl: URL
if customFile.starts(with: "/") {
customFileUrl = URL(fileURLWithPath: customFile)
} else {
customFileUrl = folder.appendingPathComponent(customFile)
}
guard customFileUrl.exists else {
missing(customFile, requiredBy: "property 'linkPreviewImage' in metadata of \(source)")
return nil
}
return customFile
}
func moreLinkText(_ elementText: String?, parent parentText: String?, source: String) -> String {
if let elementText = elementText {
return elementText
}
let standard = Element.LocalizedMetadata.moreLinkDefaultText
guard let parentText = parentText, parentText != standard else {
add(error: "Missing property 'moreLinkText'", source: source)
return standard
}
return parentText
}
func date(from string: String?, property: String, source: String) -> Date? {
guard let string = string else {
return nil
}
guard let date = ValidationLog.metadataDate.date(from: string) else {
add(warning: "Invalid date string '\(string)' for property '\(property)'", source: source)
return nil
}
return date
}
private static let metadataDate: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "dd.MM.yy"
return df
}()
}

View File

@ -0,0 +1,150 @@
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<T>(_ value: Optional<T>, _ 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<T>(_ 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<T>(_ 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<R>(_ 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<T>(_ 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(String(format: "Scanning source files: %4d pages found \r", numberOfMetadataFiles), terminator: "")
}
func printMetadataScanOverview() {
print(" Number of pages: \(numberOfMetadataFiles)")
print(" Unreadable files: \(unreadableMetadata.count)")
print(" Unused properties: \(unusedProperties.count)")
print(" Invalid properties: \(invalidProperties.count)")
print(" Unknown properties: \(unknownProperties.count)")
print(" Missing properties: \(missingProperties.count)")
}
func writeResultsToFile(in folder: URL) throws {
let url = folder.appendingPathComponent("Metadata issues.txt")
var lines = ["Unreadable files:"] + unreadableMetadata.map { "\($0.source): \($0.error)" }
lines += ["Unused properties:"] + unusedProperties.map { "\($0.source): \($0.name)" }
lines += ["Invalid properties:"] + invalidProperties.map { "\($0.source): \($0.name) (\($0.reason))" }
lines += ["Unknown properties:"] + unknownProperties.map { "\($0.source): \($0.name)" }
lines += ["Missing properties:"] + missingProperties.map { "\($0.source): \($0.name)" }
let data = lines.joined(separator: "\n").data(using: .utf8)
try data?.createFolderAndWrite(to: url)
}
}

View File

@ -17,6 +17,16 @@ struct CHGenerator: ParsableCommand {
}
}
private func loadSiteData(in folder: URL) throws -> Element? {
let log = MetadataInfoLogger(input: folder)
print("--- SOURCE FILES -----------------------------------")
let root = Element(atRoot: folder, log: log)
print(" ")
log.printMetadataScanOverview()
try log.writeResultsToFile(in: files.generatorInfoFolder)
return root
}
private func generate(configPath: String) throws {
let configUrl = URL(fileURLWithPath: configPath)
let data = try Data(contentsOf: configUrl)
@ -26,7 +36,8 @@ private func generate(configPath: String) throws {
in: configuration.contentDirectory,
to: configuration.outputDirectory)
siteRoot = Element(atRoot: configuration.contentDirectory)
// 2. Scan site elements
siteRoot = try loadSiteData(in: configuration.contentDirectory)
guard siteRoot != nil else {
return
}