diff --git a/Package.swift b/Package.swift index 3ea312104..10b9968e9 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,10 @@ let package = Package( name: "SnapshotTesting", targets: ["SnapshotTesting"] ), + .library( + name: "SnapshotTestingPlugin", + targets: ["SnapshotTestingPlugin"] + ), .library( name: "InlineSnapshotTesting", targets: ["InlineSnapshotTesting"] @@ -25,8 +29,10 @@ let package = Package( ], targets: [ .target( - name: "SnapshotTesting" + name: "SnapshotTesting", + dependencies: ["SnapshotTestingPlugin"] ), + .target(name: "SnapshotTestingPlugin"), .target( name: "InlineSnapshotTesting", dependencies: [ diff --git a/Sources/SnapshotTesting/Documentation.docc/Articles/Plugins.md b/Sources/SnapshotTesting/Documentation.docc/Articles/Plugins.md new file mode 100644 index 000000000..21e133c8e --- /dev/null +++ b/Sources/SnapshotTesting/Documentation.docc/Articles/Plugins.md @@ -0,0 +1,19 @@ +# Plugins + +SnapshotTesting offers a wide range of built-in snapshot strategies, and over the years, third-party developers have introduced new ones. However, when there’s a need for functionality that spans multiple strategies, plugins become essential. + +## Overview + +Plugins provide greater flexibility and extensibility by enabling shared behavior across different strategies without the need to duplicate code or modify each strategy individually. They can be dynamically discovered, registered, and executed at runtime, making them ideal for adding new functionality without altering the core system. This architecture promotes modularity and decoupling, allowing features to be easily added or swapped out without impacting existing functionality. + +### Plugin architecture + +The plugin architecture is designed around the concept of **dynamic discovery and registration**. Plugins conform to specific protocols, such as `SnapshotTestingPlugin`, and are registered automatically by the `PluginRegistry`. This registry manages plugin instances, allowing them to be retrieved by identifier or filtered by the protocols they conform to. + +The primary components of the plugin system include: + +- **Plugin Protocols**: Define the behavior that plugins must implement. +- **PluginRegistry**: Manages plugin discovery, registration, and retrieval. +- **Objective-C Runtime Integration**: Allows automatic discovery of plugins that conform to specific protocols. + +The `PluginRegistry` is a singleton that registers plugins during its initialization. Plugins can be retrieved by their identifier or cast to specific types, allowing flexible interaction. diff --git a/Sources/SnapshotTesting/Documentation.docc/SnapshotTesting.md b/Sources/SnapshotTesting/Documentation.docc/SnapshotTesting.md index 8704d920d..42ed0d4e2 100644 --- a/Sources/SnapshotTesting/Documentation.docc/SnapshotTesting.md +++ b/Sources/SnapshotTesting/Documentation.docc/SnapshotTesting.md @@ -23,6 +23,10 @@ Powerfully flexible snapshot testing. - ``withSnapshotTesting(record:diffTool:operation:)-2kuyr`` - ``SnapshotTestingConfiguration`` +### Plugins + +- + ### Deprecations - diff --git a/Sources/SnapshotTesting/Plugins/PluginRegistry.swift b/Sources/SnapshotTesting/Plugins/PluginRegistry.swift new file mode 100644 index 000000000..7a1639093 --- /dev/null +++ b/Sources/SnapshotTesting/Plugins/PluginRegistry.swift @@ -0,0 +1,109 @@ +#if canImport(SwiftUI) && canImport(ObjectiveC) +import Foundation +import ObjectiveC.runtime +import SnapshotTestingPlugin + +/// A singleton class responsible for managing and registering plugins conforming to the `SnapshotTestingPlugin` protocol. +/// +/// The `PluginRegistry` automatically discovers and registers classes conforming to the `SnapshotTestingPlugin` protocol +/// within the Objective-C runtime. It allows retrieval of specific plugins by identifier, access to all registered plugins, +/// and filtering of plugins that conform to the `ImageSerialization` protocol. +public class PluginRegistry { + + /// Shared singleton instance of `PluginRegistry`. + private static let shared = PluginRegistry() + + /// Dictionary holding registered plugins, keyed by their identifier. + private var plugins: [String: AnyObject] = [:] + + /// Private initializer enforcing the singleton pattern. + /// + /// Automatically triggers `automaticPluginRegistration()` to discover and register plugins. + private init() { + defer { automaticPluginRegistration() } + } + + // MARK: - Public Methods + + /// Registers a plugin. + /// + /// - Parameter plugin: An instance conforming to `SnapshotTestingPlugin`. + public static func registerPlugin(_ plugin: SnapshotTestingPlugin) { + PluginRegistry.shared.registerPlugin(plugin) + } + + /// Retrieves a plugin by its identifier, casting it to the specified type. + /// + /// - Parameter identifier: The unique identifier for the plugin. + /// - Returns: The plugin instance cast to `Output` if found and castable, otherwise `nil`. + public static func plugin(for identifier: String) -> Output? { + PluginRegistry.shared.plugin(for: identifier) + } + + /// Returns all registered plugins cast to the specified type. + /// + /// - Returns: An array of all registered plugins that can be cast to `Output`. + public static func allPlugins() -> [Output] { + PluginRegistry.shared.allPlugins() + } + + // MARK: - Internal Methods + + /// Registers a plugin. + /// + /// - Parameter plugin: An instance conforming to `SnapshotTestingPlugin`. + private func registerPlugin(_ plugin: SnapshotTestingPlugin) { + plugins[type(of: plugin).identifier] = plugin + } + + /// Retrieves a plugin by its identifier, casting it to the specified type. + /// + /// - Parameter identifier: The unique identifier for the plugin. + /// - Returns: The plugin instance cast to `Output` if found and castable, otherwise `nil`. + private func plugin(for identifier: String) -> Output? { + return plugins[identifier] as? Output + } + + /// Returns all registered plugins cast to the specified type. + /// + /// - Returns: An array of all registered plugins that can be cast to `Output`. + private func allPlugins() -> [Output] { + return Array(plugins.values.compactMap { $0 as? Output }) + } + + /// Discovers and registers all classes conforming to the `SnapshotTestingPlugin` protocol. + /// + /// This method iterates over all Objective-C runtime classes, identifying those that conform to `SnapshotTestingPlugin`, + /// instantiating them, and registering them as plugins. + private func automaticPluginRegistration() { + let classCount = objc_getClassList(nil, 0) + guard classCount > 0 else { return } + + let classes = UnsafeMutablePointer.allocate(capacity: Int(classCount)) + defer { classes.deallocate() } + + let autoreleasingClasses = AutoreleasingUnsafeMutablePointer(classes) + objc_getClassList(autoreleasingClasses, classCount) + + for i in 0..