Scaffold v1.0.0

This commit is contained in:
T. R. Bernstein
2026-04-17 01:08:29 +02:00
commit b49d642dfd
22 changed files with 983 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import Testing
@testable import EmbedderTool
@Suite("FileExtensionAllowList") struct FileExtensionAllowListTests {
@Test("permits common textual formats")
func permitsTextual() {
let textual = ["json", "yaml", "yml", "html", "eml", "txt", "md", "xml", "csv", "svg"]
for fileExtension in textual {
#expect(FileExtensionAllowList.permits(fileExtension))
}
}
@Test("ignores letter casing")
func caseInsensitive() {
#expect(FileExtensionAllowList.permits("JSON"))
#expect(FileExtensionAllowList.permits("Html"))
}
@Test("rejects known binary extensions")
func rejectsBinary() {
let binary = ["png", "jpg", "jpeg", "pdf", "zip", "gif", "ttf", "woff", "mp3", "mp4"]
for fileExtension in binary {
#expect(!FileExtensionAllowList.permits(fileExtension))
}
}
@Test("rejects files without an extension")
func rejectsMissingExtension() {
#expect(!FileExtensionAllowList.permits(""))
}
}

View File

@@ -0,0 +1,53 @@
import Testing
@testable import EmbedderTool
@Suite("IdentifierSanitizer") struct IdentifierSanitizerTests {
@Test("converts a simple filename with extension to lowerCamelCase")
func simpleFilename() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "welcome.html") == "welcomeHtml")
#expect(IdentifierSanitizer.propertyName(fromFilename: "config.json") == "configJson")
}
@Test("normalizes uppercase and mixed case extensions")
func uppercaseExtension() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "welcome.HTML") == "welcomeHtml")
#expect(IdentifierSanitizer.propertyName(fromFilename: "Data.Yaml") == "dataYaml")
}
@Test("splits snake_case, kebab-case and camelCase into words")
func splitWords() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "user_profile.json") == "userProfileJson")
#expect(IdentifierSanitizer.propertyName(fromFilename: "email-template.eml") == "emailTemplateEml")
#expect(IdentifierSanitizer.propertyName(fromFilename: "fooBar.json") == "fooBarJson")
}
@Test("prefixes an underscore when a filename starts with a digit")
func digitPrefix() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "404.html") == "_404Html")
}
@Test("escapes Swift reserved keywords with backticks")
func reservedKeyword() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "class") == "`class`")
#expect(IdentifierSanitizer.propertyName(fromFilename: "return") == "`return`")
}
@Test("collapses runs of non-alphanumeric characters")
func multipleSeparators() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "user--profile__v2.json") == "userProfileV2Json")
}
@Test("produces UpperCamelCase type names for directories")
func typeNames() {
#expect(IdentifierSanitizer.typeName(from: "emails") == "Emails")
#expect(IdentifierSanitizer.typeName(from: "user-templates") == "UserTemplates")
#expect(IdentifierSanitizer.typeName(from: "api_v2") == "ApiV2")
}
@Test("returns an underscore fallback when the identifier would be empty")
func emptyFallback() {
#expect(IdentifierSanitizer.propertyName(fromFilename: "---") == "_")
#expect(IdentifierSanitizer.typeName(from: "") == "_")
}
}

View File

@@ -0,0 +1,67 @@
import Foundation
import Testing
@testable import EmbedderTool
@Suite("NamespaceTreeBuilder") struct NamespaceTreeBuilderTests {
@Test("places top-level files directly inside the root namespace")
func topLevelFiles() {
let files = [
makeFile(relativePath: "config.json"),
makeFile(relativePath: "welcome.html")
]
let tree = NamespaceTreeBuilder().buildTree(from: files)
#expect(tree.name == "Embedded")
#expect(tree.files.map(\.filename) == ["config.json", "welcome.html"])
#expect(tree.subNamespaces.isEmpty)
}
@Test("nests subdirectories as child enums")
func nestedDirectories() {
let files = [
makeFile(relativePath: "emails/welcome.html"),
makeFile(relativePath: "emails/receipt.eml"),
makeFile(relativePath: "root.json")
]
let tree = NamespaceTreeBuilder().buildTree(from: files)
#expect(tree.files.map(\.filename) == ["root.json"])
#expect(tree.subNamespaces.count == 1)
#expect(tree.subNamespaces[0].name == "Emails")
#expect(tree.subNamespaces[0].files.map(\.filename) == ["receipt.eml", "welcome.html"])
}
@Test("merges multiple files under the same sanitized directory name")
func mergesSiblingNamespaces() {
let files = [
makeFile(relativePath: "user-templates/a.json"),
makeFile(relativePath: "user-templates/b.json")
]
let tree = NamespaceTreeBuilder().buildTree(from: files)
#expect(tree.subNamespaces.count == 1)
#expect(tree.subNamespaces[0].name == "UserTemplates")
#expect(tree.subNamespaces[0].files.count == 2)
}
@Test("preserves deep hierarchies")
func deepHierarchy() {
let files = [
makeFile(relativePath: "api/v2/users/list.json")
]
let tree = NamespaceTreeBuilder().buildTree(from: files)
#expect(tree.subNamespaces.count == 1)
#expect(tree.subNamespaces[0].name == "Api")
#expect(tree.subNamespaces[0].subNamespaces[0].name == "V2")
#expect(tree.subNamespaces[0].subNamespaces[0].subNamespaces[0].name == "Users")
#expect(tree.subNamespaces[0].subNamespaces[0].subNamespaces[0].files.map(\.filename) == ["list.json"])
}
private func makeFile(relativePath: String) -> EmbeddableFile {
let components = relativePath.split(separator: "/").map(String.init)
let absoluteURL = URL(fileURLWithPath: "/fake/Static Inline").appending(path: relativePath)
return EmbeddableFile(absoluteURL: absoluteURL, relativePathComponents: components)
}
}

View File

@@ -0,0 +1,37 @@
import Testing
@testable import EmbedderTool
@Suite("StringLiteralEscaper") struct StringLiteralEscaperTests {
@Test("wraps plain content with a single hash delimiter")
func plainContent() {
let literal = StringLiteralEscaper.rawTripleQuotedLiteral(from: "hello")
#expect(literal == "#\"\"\"\nhello\n\"\"\"#")
}
@Test("uses two hashes when content contains a triple-quote followed by one hash")
func escalatesForSingleHashCollision() {
let literal = StringLiteralEscaper.rawTripleQuotedLiteral(from: "before\"\"\"#after")
#expect(literal == "##\"\"\"\nbefore\"\"\"#after\n\"\"\"##")
}
@Test("escalates hash count past the longest run seen in content")
func escalatesForLongerHashRun() {
let literal = StringLiteralEscaper.rawTripleQuotedLiteral(from: "x\"\"\"####y")
#expect(literal.hasPrefix("#####\"\"\""))
#expect(literal.hasSuffix("\"\"\"#####"))
}
@Test("accepts empty content")
func emptyContent() {
let literal = StringLiteralEscaper.rawTripleQuotedLiteral(from: "")
#expect(literal == "#\"\"\"\n\n\"\"\"#")
}
@Test("leaves triple-quotes without trailing hashes untouched")
func tripleQuotesWithoutHashes() {
let literal = StringLiteralEscaper.rawTripleQuotedLiteral(from: "a\"\"\"b")
#expect(literal.hasPrefix("#\"\"\""))
#expect(literal.hasSuffix("\"\"\"#"))
}
}

View File

@@ -0,0 +1,79 @@
import Foundation
import Testing
@testable import EmbedderTool
@Suite("SwiftCodeGenerator") struct SwiftCodeGeneratorTests {
@Test("generates a compilable file for top-level files")
func topLevelFiles() throws {
let directory = try TemporaryDirectory()
let configFile = try directory.write(contents: #"{"key":"value"}"#, toRelativePath: "config.json")
let root = NamespaceNode(
name: "Embedded",
files: [
EmbeddableFile(
absoluteURL: configFile,
relativePathComponents: ["config.json"]
)
]
)
let output = try SwiftCodeGenerator().generate(from: root)
#expect(output.contains("enum Embedded {"))
#expect(output.contains("static let configJson: String = #\"\"\""))
#expect(output.contains(#"{"key":"value"}"#))
#expect(output.contains("\"\"\"#"))
}
@Test("nests child enums for subdirectories")
func nestedNamespaces() throws {
let directory = try TemporaryDirectory()
let welcomeFile = try directory.write(contents: "<html></html>", toRelativePath: "emails/welcome.html")
let root = NamespaceNode(
name: "Embedded",
subNamespaces: [
NamespaceNode(
name: "Emails",
files: [
EmbeddableFile(
absoluteURL: welcomeFile,
relativePathComponents: ["emails", "welcome.html"]
)
]
)
]
)
let output = try SwiftCodeGenerator().generate(from: root)
#expect(output.contains("enum Embedded {"))
#expect(output.contains("enum Emails {"))
#expect(output.contains("static let welcomeHtml: String = #\"\"\""))
#expect(output.contains("<html></html>"))
}
@Test("includes a generated-file header")
func header() throws {
let output = try SwiftCodeGenerator().generate(from: NamespaceNode(name: "Embedded"))
#expect(output.hasPrefix("// Generated by the Embedder plugin"))
}
}
private struct TemporaryDirectory {
let url: URL
init() throws {
let baseURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
self.url = baseURL.appending(path: "EmbedderToolTests-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
}
func write(contents: String, toRelativePath relativePath: String) throws -> URL {
let fileURL = url.appending(path: relativePath)
let parent = fileURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: parent, withIntermediateDirectories: true)
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
return fileURL
}
}