From 091c0f06869ff3dd2070504159c58db1abf01080 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 15 Aug 2026 23:41:53 +0200 Subject: [PATCH] Added an optional per-asset folder to the Asset protocol and the AssetExtension extension in the Infrastructure package. --- .../Public/Enumerations/AssetExtension.swift | 4 ++-- .../Sources/Public/Protocols/Asset.swift | 13 +++++++++++-- .../Public/Enumerations/AssetExtensionTests.swift | 10 +++++----- .../Tests/Cases/Public/Protocols/AssetTests.swift | 14 ++++++++++++-- .../Tests/Utils/Assets/StubAsset.swift | 3 ++- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Packages/Infrastructure/Sources/Public/Enumerations/AssetExtension.swift b/Packages/Infrastructure/Sources/Public/Enumerations/AssetExtension.swift index 9d1ae00..bdcd25e 100644 --- a/Packages/Infrastructure/Sources/Public/Enumerations/AssetExtension.swift +++ b/Packages/Infrastructure/Sources/Public/Enumerations/AssetExtension.swift @@ -40,8 +40,8 @@ public extension AssetExtension { } } - /// The sub-directory within the static root that holds files with this extension, if any. - var subdirectory: String? { + /// The folder within the static root that holds files with this extension, if any. + var folder: String? { switch self { case .css: "css" case .js: "js" diff --git a/Packages/Infrastructure/Sources/Public/Protocols/Asset.swift b/Packages/Infrastructure/Sources/Public/Protocols/Asset.swift index be5506d..32173a9 100644 --- a/Packages/Infrastructure/Sources/Public/Protocols/Asset.swift +++ b/Packages/Infrastructure/Sources/Public/Protocols/Asset.swift @@ -1,11 +1,15 @@ /// An asset shipped with a website: a file stored under the static files root and served by Hummingbird's `FileMiddleware` middleware. /// /// A conforming asset supplies its file name and the extensions it is available with, each resolving to its own file; the protocol derives the paths from them: -/// the file's path within the static files root and the URL path it is served at, optionally versioned to bust caches. +/// the file's path within the static files root and the URL path it is served at, optionally versioned to bust caches. Each file lands in its extension's own +/// folder unless the asset names a ``folder`` of its own. public protocol Asset: Sendable { // MARK: Properties + /// The folder within the static files root that holds the asset's files, or `nil` (the default) to use each extension's own folder. + var folder: String? { get } + /// The file extensions the asset is available with. var fileExtensions: [AssetExtension] { get } @@ -18,6 +22,11 @@ public protocol Asset: Sendable { public extension Asset { + // MARK: Computed + + /// The asset's files live in each extension's own folder by default. + var folder: String? { nil } + // MARK: Methods /// Resolves the asset's path against the given base directory. @@ -50,7 +59,7 @@ public extension Asset { ) -> String { let file = "\(fileName).\(fileExtension.rawValue)" - return fileExtension.subdirectory + return (folder ?? fileExtension.folder) .map { "\($0)/\(file)" } ?? file } diff --git a/Packages/Infrastructure/Tests/Cases/Public/Enumerations/AssetExtensionTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Enumerations/AssetExtensionTests.swift index f49dbea..f1fff74 100644 --- a/Packages/Infrastructure/Tests/Cases/Public/Enumerations/AssetExtensionTests.swift +++ b/Packages/Infrastructure/Tests/Cases/Public/Enumerations/AssetExtensionTests.swift @@ -23,13 +23,13 @@ struct AssetExtensionTests { @Test(arguments: zip( Self.extensions, - Self.subdirectories + Self.folders )) - func `subdirectory`( + func `folder`( for fileExtension: AssetExtension, - expects subdirectory: String? + expects folder: String? ) { - #expect(fileExtension.subdirectory == subdirectory) + #expect(fileExtension.folder == folder) } } @@ -60,7 +60,7 @@ private extension AssetExtensionTests { "application/manifest+json", "application/xml" ] - static let subdirectories: [String?] = [ + static let folders: [String?] = [ "css", "js", nil, diff --git a/Packages/Infrastructure/Tests/Cases/Public/Protocols/AssetTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Protocols/AssetTests.swift index 08b392b..013e2eb 100644 --- a/Packages/Infrastructure/Tests/Cases/Public/Protocols/AssetTests.swift +++ b/Packages/Infrastructure/Tests/Cases/Public/Protocols/AssetTests.swift @@ -14,6 +14,11 @@ struct AssetTests { fileExtensions: [.png], fileName: "icon" ) + private let portrait = StubAsset( + folder: "img/organizer", + fileExtensions: [.jpg], + fileName: "portrait" + ) private let shared = StubAsset( fileExtensions: [.css, .js], fileName: "shared" @@ -22,13 +27,18 @@ struct AssetTests { // MARK: Method tests @Test - func `relative path nests the file inside its extension's sub-directory`() { + func `relative path nests the file inside its extension's folder`() { #expect(shared.relativePath(for: .css) == "css/shared.css") #expect(shared.relativePath(for: .js) == "js/shared.js") } @Test - func `relative path keeps the file at the root without a sub-directory`() { + func `relative path prefers the asset's own folder over the extension's`() { + #expect(portrait.relativePath(for: .jpg) == "img/organizer/portrait.jpg") + } + + @Test + func `relative path keeps the file at the root without a folder`() { #expect(image.relativePath(for: .png) == "icon.png") } diff --git a/Packages/Infrastructure/Tests/Utils/Assets/StubAsset.swift b/Packages/Infrastructure/Tests/Utils/Assets/StubAsset.swift index c508008..9000a1c 100644 --- a/Packages/Infrastructure/Tests/Utils/Assets/StubAsset.swift +++ b/Packages/Infrastructure/Tests/Utils/Assets/StubAsset.swift @@ -1,10 +1,11 @@ import Infrastructure -/// An ``Asset`` with a fixed file name and set of extensions. +/// An ``Asset`` with a fixed file name and set of extensions, optionally held in a folder of its own. struct StubAsset: Asset { // MARK: Properties + var folder: String? = nil let fileExtensions: [AssetExtension] let fileName: String