A powerful Swift library implementing the Specification Pattern with support for context providers, property wrappers, and composable business rules. Perfect for feature flags, conditional logic, banner display rules, and complex business requirements.
- Features
- What's New in v3.0.0
- Installation
- Quick Start
- Core Components
- Advanced Usage
- Testing
- Performance Benchmarks
- Debugging and Tracing
- Demo App
- Architecture
- Documentation
- Roadmap
- Migration Guide
- Contributing
- License
- Support
- π§© Composable Specifications - Build complex business rules from simple, reusable components
- π― Property Wrapper Support - Declarative syntax with
@Satisfies,@Decides,@Maybe,@CachedSatisfies, and reactive wrappers@ObservedSatisfies,@ObservedDecides,@ObservedMaybefor SwiftUI - π Context Providers - Flexible context injection and dependency management including
DefaultContextProvider,EnvironmentContextProvider,NetworkContextProvider,PersistentContextProvider, andCompositeContextProvider - π Decision Specifications - Return typed results beyond boolean values with
DecisionSpecandFirstMatchSpec - π² Advanced Specifications -
WeightedSpecfor A/B testing,HistoricalSpecfor time-series analysis,ComparativeSpecfor relative comparisons,ThresholdSpecfor dynamic thresholds - π§ Date & Flag Specs - Built-ins:
DateRangeSpec,DateComparisonSpec,FeatureFlagSpec,UserSegmentSpec,SubscriptionStatusSpec - βοΈ Async Capable - Evaluate rules asynchronously via
AsyncSpecification,AnyAsyncSpecification, and async property wrappers - π Observation for SwiftUI - Auto-update views when context changes via
ContextUpdatesProviding - π Conditional Evaluation - Runtime specification selection with
@ConditionalSatisfies - π§ͺ Testing Support - Built-in mock providers and
MockSpecificationBuilderfor comprehensive testing - π± Cross-Platform - Works on iOS, macOS, tvOS, and watchOS with platform-specific providers
- π Type-Safe - Leverages Swift's type system for compile-time safety
- β‘ Performance Optimized - Lightweight with
@inlinablemethods and specialized storage
Choose specifications dynamically based on runtime conditions:
@ConditionalSatisfies(
condition: { context in context.flag(for: "use_strict_mode") },
whenTrue: StrictValidationSpec(),
whenFalse: BasicValidationSpec()
)
var validationPassed: Bool
// Platform-specific specs
@ConditionalSatisfies.iOS(
iOS: MobileLayoutSpec(),
other: DesktopLayoutSpec()
)
var shouldUseMobileLayout: BoolWeightedSpec for probability-based selection (A/B testing):
let abTestSpec = WeightedSpec([
(FeatureFlagSpec(flag: "variant_a"), 0.5, "variant_a"),
(FeatureFlagSpec(flag: "variant_b"), 0.3, "variant_b"),
(FeatureFlagSpec(flag: "control"), 0.2, "control")
])HistoricalSpec for time-series analysis:
let performanceSpec = HistoricalSpec(
provider: MetricsHistoryProvider(),
window: .lastN(30),
aggregation: .median
)ThresholdSpec for dynamic threshold evaluation:
let alertSpec = ThresholdSpec(
keyPath: \.responseTime,
threshold: .adaptive { getCurrentBaseline() },
operator: .greaterThan
)- @inlinable Methods: Cross-module compiler optimizations
- Specialized Storage: Different strategies for predicates, constants, and specifications
- Collection Extensions: Early-return optimizations for
allSatisfied()andanySatisfied() - <0.1ms Evaluation: Baseline performance for typical specifications
@ObservedDecides for reactive decision specifications:
@ObservedDecides([
(PremiumUserSpec(), "premium_layout"),
(TabletDeviceSpec(), "tablet_layout"),
(CompactSizeSpec(), "mobile_layout")
], or: "default_layout")
var layoutType: String@ObservedMaybe for reactive optional decisions:
@ObservedMaybe(provider: DefaultContextProvider.shared,
firstMatch: [
(FeatureFlagSpec(flagKey: "feature_x"), "Enabled")
])
private var featureMessage: String?- SpecificationProfiler: Runtime performance analysis with microsecond precision
- 13 Performance Test Cases: Comprehensive validation of optimization effectiveness
- Benchmark Baselines: Automated performance regression detection
Context providers for iOS, macOS, watchOS, and tvOS with platform-specific capabilities:
// Cross-platform device capability checking
let darkModeSpec = PlatformContextProviders.createDeviceCapabilitySpec(.darkMode)
@Satisfies(using: darkModeSpec)
var supportsDarkMode: Bool // Works on all platforms with graceful fallbacksAdd SpecificationKit to your project in Xcode:
- Go to File β Add Package Dependencies
- Enter the repository URL:
https://github.com/SoundBlaster/SpecificationKit - Select version
3.0.0or later
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/SoundBlaster/SpecificationKit", from: "3.0.0")
]import SpecificationKit
// Define your context
let context = EvaluationContext(
timeSinceLaunch: 15.0,
counters: ["banner_shown": 1],
events: ["last_banner": Date().addingTimeInterval(-3600)]
)
// Create specifications
let timeSinceLaunch = TimeSinceEventSpec.sinceAppLaunch(seconds: 10)
let maxShowCount = MaxCountSpec(counterKey: "banner_shown", limit: 3)
let cooldownPeriod = CooldownIntervalSpec(eventKey: "last_banner", hours: 1)
// Combine specifications
let canShowBanner = timeSinceLaunch.and(maxShowCount).and(cooldownPeriod)
// Evaluate
if canShowBanner.isSatisfiedBy(context) {
print("Show the banner!")
}The @specs macro simplifies composite specification creation by automatically generating init() and isSatisfiedBy(_:) methods:
@specs(
MaxCountSpec(counterKey: "display_count", limit: 3),
TimeSinceEventSpec(eventKey: "last_shown", minimumInterval: 3600)
)
struct BannerSpec: Specification {
typealias T = EvaluationContext
}
// Usage
let bannerSpec = BannerSpec()
if bannerSpec.isSatisfiedBy(context) {
print("Show the banner!")
}Macro Diagnostics: The macro validates argument types, detects mixed contexts, identifies async specs, and ensures proper protocol conformance with clear error messages.
class BannerController {
// Simple boolean specification
@Satisfies(using: TimeSinceEventSpec.sinceAppLaunch(seconds: 10))
var canShowAfterDelay: Bool
// Decision specification (non-optional)
@Decides([
(isVipSpec, 50),
(promoSpec, 20),
(birthdaySpec, 10)
], or: 0)
var discountPercentage: Int
// Decision specification (optional)
@Maybe([
(isVipSpec, 50),
(promoSpec, 20),
(birthdaySpec, 10)
])
var discount: Int?
func checkStatus() {
if canShowAfterDelay {
print("Showing banner with \(discountPercentage)% discount")
}
}
}Evaluate specs asynchronously when inputs require awaiting:
// Async spec with delay
let asyncSpec = AnyAsyncSpecification<EvaluationContext> { ctx in
try? await Task.sleep(nanoseconds: 50_000_000)
return ctx.flag(for: "feature_enabled")
}
let result = try await asyncSpec.isSatisfiedBy(
EvaluationContext(flags: ["feature_enabled": true])
)
// Async evaluation with provider
struct Gate {
@Satisfies(provider: DefaultContextProvider.shared,
predicate: { $0.flag(for: "feature_async") })
var isOn: Bool
func check() async throws -> Bool {
try await _isOn.evaluateAsync()
}
}Use @ObservedSatisfies to keep views synchronized with provider changes:
struct ObservationExample: View {
@ObservedSatisfies(provider: DefaultContextProvider.shared,
using: MaxCountSpec(counterKey: "attempts", limit: 3))
private var underLimit: Bool
var body: some View {
VStack {
Text(underLimit ? "Below limit" : "Limit reached")
Button("+1") {
_ = DefaultContextProvider.shared.incrementCounter("attempts")
}
Button("Reset") {
DefaultContextProvider.shared.setCounter("attempts", to: 0)
}
}
}
}Checks if enough time has passed since an event:
// Check if 5 minutes passed since app launch
let spec = TimeSinceEventSpec.sinceAppLaunch(minutes: 5)
// Check if 24 hours passed since last notification
let cooldown = TimeSinceEventSpec(eventKey: "last_notification", hours: 24)Ensures a counter hasn't exceeded a maximum value:
// Allow maximum 3 banner displays
let spec = MaxCountSpec(counterKey: "banner_count", limit: 3)
// One-time only actions
let onceOnly = MaxCountSpec.onlyOnce("onboarding_completed")Implements cooldown periods between events:
// 7-day cooldown between promotions
let spec = CooldownIntervalSpec.weekly("promo_shown")
// Custom cooldown period
let custom = CooldownIntervalSpec(eventKey: "feature_used", minutes: 30)Flexible specification using custom predicates:
// Custom business logic
let spec = PredicateSpec<EvaluationContext> { context in
context.flag(for: "premium_user") && context.counter(for: "usage_count") > 10
}
// Time-based conditions
let businessHours = PredicateSpec<EvaluationContext>.currentHour(in: 9...17)Evaluates specifications in priority order:
let discountSpec = FirstMatchSpec<UserContext, Int>([
(isVipSpec, 50),
(promoSpec, 20),
(birthdaySpec, 10),
(AlwaysTrueSpec(), 0) // fallback
])
let discount = discountSpec.decide(userContext) // e.g., 50 if user is VIPDateRangeSpec - Check if current date is within range:
let spec = DateRangeSpec(
start: Date(timeIntervalSinceNow: -86400),
end: Date(timeIntervalSinceNow: 86400)
)DateComparisonSpec - Compare event date to reference:
let spec = DateComparisonSpec(eventKey: "last_login", comparison: .before, date: Date())FeatureFlagSpec - Match boolean flags:
let enabled = FeatureFlagSpec(flagKey: "feature_enabled")UserSegmentSpec - Check segment membership:
let isVip = UserSegmentSpec(.vip)SubscriptionStatusSpec - Match subscription status:
let isPremium = SubscriptionStatusSpec(.premium)Production-ready context provider with thread-safe state management:
let provider = DefaultContextProvider.shared
// Manage counters
provider.incrementCounter("app_opens")
provider.setCounter("feature_usage", to: 5)
// Track events
provider.recordEvent("user_login")
provider.recordEvent("purchase_made", at: specificDate)
// Boolean flags
provider.setFlag("premium_user", to: true)
provider.toggleFlag("dark_mode")Bridge SwiftUI @Environment and @AppStorage into EvaluationContext:
let envProvider = EnvironmentContextProvider()
envProvider.locale = locale // from @Environment(\.locale)
envProvider.interfaceStyle = (colorScheme == .dark ? "dark" : "light")
envProvider.flags["promo_enabled"] = promoEnabled // from @AppStorageFetch context from remote endpoints with caching and retry policies:
let config = NetworkContextProvider.Configuration(
endpoint: URL(string: "https://api.yourservice.com/context")!,
refreshInterval: 300,
retryPolicy: .exponentialBackoff(maxAttempts: 3),
fallbackValues: ["feature_enabled": true]
)
let networkProvider = NetworkContextProvider(configuration: config)
let context = try await networkProvider.currentContextAsync()Features: Intelligent caching, retry policies, offline support, Swift 6 ready, reactive updates
Persist context data locally using Core Data:
let config = PersistentContextProvider.Configuration(
modelName: "SpecificationContext",
storeType: .sqliteStoreType,
migrationPolicy: .automatic,
encryptionEnabled: true
)
let persistentProvider = PersistentContextProvider(configuration: config)
await persistentProvider.setValue("premium", for: "user_tier")
await persistentProvider.setCounter(42, for: "login_count")Features: Core Data integration, data expiration, thread safety, multiple data types, migration support, encryption
Combine multiple providers into a single context source:
let provider = CompositeContextProvider(
providers: [defaults, env],
strategy: .preferLast // Later providers override earlier ones
)Strategies: .preferLast, .preferFirst, .custom { [EvaluationContext] in ... }
Perfect for unit testing with controllable state:
let mockProvider = MockContextProvider()
.withCounter("test_counter", value: 5)
.withFlag("test_flag", value: true)
.withEvent("test_event", date: Date())struct RouteDecisionSpec: DecisionSpec {
typealias Context = RequestContext
typealias Result = Route
func decide(_ context: RequestContext) -> Route? {
if context.isAuthenticated {
return .dashboard
} else if context.hasSession {
return .login
} else {
return .welcome
}
}
}
// Use with property wrappers
@Decides(decide: { ctx in
if ctx.flag(for: "authenticated") { return .dashboard }
if ctx.flag(for: "has_session") { return .login }
return nil
}, or: .welcome)
var currentRoute: Routestruct OnboardingSpec: Specification {
typealias T = EvaluationContext
private let composite: AnySpecification<EvaluationContext>
init() {
let userEngaged = PredicateSpec<EvaluationContext>.counter(
"screen_views", .greaterThanOrEqual, 3
)
let firstWeek = TimeSinceEventSpec.sinceAppLaunch(days: 7).not()
let notCompleted = PredicateSpec<EvaluationContext>.flag(
"onboarding_completed", equals: false
)
composite = AnySpecification(
userEngaged.and(firstWeek).and(notCompleted)
)
}
func isSatisfiedBy(_ context: EvaluationContext) -> Bool {
composite.isSatisfiedBy(context)
}
}// Boolean specification builder
let complexSpec = Satisfies<EvaluationContext>.builder(
provider: DefaultContextProvider.shared
)
.with(TimeSinceEventSpec.sinceAppLaunch(minutes: 2))
.with(MaxCountSpec(counterKey: "attempts", limit: 3))
.with { context in context.flag(for: "feature_enabled") }
.buildAll()
// Decision specification builder
let discountSpec = FirstMatchSpec<UserContext, Int>.builder()
.add(isVipSpec, result: 50)
.add(promoSpec, result: 20)
.add(birthdaySpec, result: 10)
.fallback(0)
.build()Choose specifications dynamically at runtime:
// Condition-based selection
@ConditionalSatisfies(
condition: { context in context.flag(for: "use_strict_mode") },
whenTrue: StrictValidationSpec(),
whenFalse: BasicValidationSpec()
)
var validationPassed: Bool
// Platform-specific selection
@ConditionalSatisfies.iOS(
iOS: MobileLayoutSpec(),
other: DesktopLayoutSpec()
)
var shouldUseMobileLayout: Bool
// Builder pattern for complex scenarios
@ConditionalSatisfies.builder()
.when({ ctx in ctx.flag(for: "experimental") }, use: ExperimentalSpec())
.when({ ctx in ctx.flag(for: "beta") }, use: BetaSpec())
.otherwise(use: ProductionSpec())
var featureEnabled: BoolUse @CachedSatisfies to cache expensive evaluations with automatic TTL expiration:
class PerformanceController {
// Cache result for 5 minutes
@CachedSatisfies(using: ExpensiveAnalysisSpec(), ttl: 300.0)
var analysisComplete: Bool
// Cache user permission check for 60 seconds
@CachedSatisfies(provider: DefaultContextProvider.shared,
predicate: { $0.flag(for: "user_premium") },
ttl: 60.0)
var isPremiumUser: Bool
}Cache Management:
// Force refresh
_analysisComplete.invalidateCache()
// Check cache status
if _analysisComplete.isCached {
print("Using cached result")
}
// Get cache statistics
if let info = _analysisComplete.cacheInfo {
print("Expires in: \(info.remainingTTL)s")
}struct ContentView: View {
@Satisfies(using: CompositeSpec.promoBanner)
var shouldShowPromo: Bool
@Decides([
(vipSpec, 50),
(promoSpec, 20),
(birthdaySpec, 10)
], or: 0)
var discountPercentage: Int
var body: some View {
VStack {
if shouldShowPromo {
PromoBannerView(discountPercentage: discountPercentage)
}
MainContentView()
}
}
}SpecificationKit includes comprehensive testing utilities including MockSpecificationBuilder for creating deterministic or intentionally flaky specifications:
let mock = MockSpecificationBuilder<EvaluationContext>()
.withSequence([true, false])
.withExecutionTime(0.01)
.build()
XCTAssertTrue(mock.isSatisfiedBy(EvaluationContext()))
XCTAssertFalse(mock.isSatisfiedBy(EvaluationContext()))
XCTAssertEqual(mock.allResults, [true, false])
mock.reset()Convenience helpers:
.alwaysTrue()- Always returns true.flaky(successRate:)- Probabilistic results for testing edge cases.slow(delay:)- Add synthetic latency.willThrow(_:)- Surface fatal-error scenarios
Testing with MockContextProvider:
class MyFeatureTests: XCTestCase {
func testBannerLogic() {
// Given
let mockProvider = MockContextProvider.launchDelayScenario(
timeSinceLaunch: 30
)
.withCounter("banner_shown", value: 1)
.withEvent("last_banner", date: Date().addingTimeInterval(-3600))
let spec = CompositeSpec.promoBanner
// When
let result = spec.isSatisfiedBy(mockProvider.currentContext())
// Then
XCTAssertTrue(result)
XCTAssertEqual(mockProvider.contextRequestCount, 1)
}
}SpecificationKit includes comprehensive performance benchmarking infrastructure to ensure optimal performance and detect regressions.
# Run all performance benchmarks
swift test --filter PerformanceBenchmarks
# Run specific categories
swift test --filter testSpecificationEvaluationPerformance
swift test --filter testMemoryUsageOptimization
swift test --filter testConcurrentAccessPerformanceSpecification Evaluation Performance: Tests core evaluation of simple and composite specifications
- Target: < 0.1ms per evaluation for simple specifications
Memory Usage Optimization: Monitors memory allocation patterns
- Target: < 1KB memory per specification evaluation
Concurrent Access Performance: Validates thread-safe performance under load
- Ensures linear performance scaling with thread count
Built-in runtime performance analysis:
let profiler = SpecificationProfiler.shared
let spec = MaxCountSpec(counterKey: "attempts", limit: 5)
let context = EvaluationContext(counters: ["attempts": 3])
// Profile evaluation
let result = profiler.profile(spec, context: context)
// Get performance data
let data = profiler.getProfileData()
print("Average time: \(data.averageTime)ms")
print("Memory usage: \(data.memoryUsage)KB")
// Generate detailed report
let report = profiler.generateReport()
print(report)| Operation | Performance | Memory |
|---|---|---|
| Simple Spec Evaluation | avg 0.05ms | 0.8KB |
| Composite Spec (5 components) | < 0.5ms | < 2KB |
| Context Provider Access | 0.02ms | 0.5KB |
| Property Wrapper Overhead | +2.3% | Negligible |
SpecificationKit v3.0.0 includes SpecificationTracer for detailed execution analysis:
let tracer = SpecificationTracer.shared
let sessionId = tracer.startTracing()
// Traced evaluation
let result = tracer.trace(specification: complexSpec, context: context)
if let session = tracer.stopTracing() {
print("Traced \(session.entries.count) evaluations")
print("Total time: \(session.totalExecutionTime * 1000)ms")
// Print execution tree
for tree in session.traceTree {
tree.printTree()
}
// Generate DOT graph for Graphviz visualization
let dotGraph = session.traceTree.first?.generateDotGraph()
}Features:
- Hierarchical tracing with parent-child relationships
- Performance monitoring with precise timing
- Visual representation via tree and DOT graph generation
- Thread-safe concurrent tracing
- Zero overhead when disabled
The repository includes a complete SwiftUI demo app showing real-world usage:
cd DemoApp
swift run SpecificationKitDemo
# Or run CLI version
swift run SpecificationKitDemo --cliThe demo showcases:
- Real-time specification evaluation
- Context provider management
- Property wrapper integration
- Decisions screen (
@Decides,@Maybe,FirstMatchSpec) - Async Specs screen (delays, error handling)
- Environment Context screen (
@Environment/@AppStoragebridging) - Observation screen (live updates with
@ObservedSatisfies) - Context Composition screen (
CompositeContextProviderstrategies)
SpecificationKit follows a clean, layered architecture:
βββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β (@Satisfies, @Decides, @Maybe, Views) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Property Wrapper Layer β
β (@Satisfies, @Decides, @Maybe, β
β @CachedSatisfies, @ObservedDecides, β
β @ConditionalSatisfies) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Definitions Layer β
β (CompositeSpec, FirstMatchSpec, β
β WeightedSpec, ThresholdSpec) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Specifications Layer β
β (Specification, DecisionSpec) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Context Layer β
β (EvaluationContext, Providers) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Core Layer β
β (Specification Protocol, Operators) β
βββββββββββββββββββββββββββββββββββββββββββ
Comprehensive DocC documentation is available online:
π View Documentation
The documentation includes:
- Complete API reference with examples
- Usage guides for all property wrappers
- Macro system documentation
- Context provider integration patterns
- SwiftUI and async/await examples
# Generate static documentation website
swift package generate-documentation --target SpecificationKit \
--output-path ./docs --transform-for-static-hosting
# Serve locally
cd docs && python3 -m http.server 8000
# Open http://localhost:8000 in your browserXcode Documentation:
- Open the project:
open Package.swift - Product β Build Documentation (ββ§βD)
See our comprehensive ROADMAP.md for planned features and future development.
@AutoContext Enhancements (Infrastructure Ready)
- π±
@AutoContext(environment)- SwiftUI Environment integration - π§
@AutoContext(infer)- Smart provider inference - π―
@AutoContext(CustomProvider.self)- Custom provider types
Parsing infrastructure is complete and readyβthese features will activate when Swift's macro capabilities evolve.
Performance & Platform
- β‘ Benchmark baseline capture on macOS hardware
- π± Enhanced platform-specific context providers
- π¨ Advanced macro composition features
Use @Decides instead of @Spec for decision specifications. The old @Spec remains available as a deprecated alias and will be removed in a future release.
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
SpecificationKit is available under the MIT license. See LICENSE for details.
- π Documentation
- π¬ Discussions
- π Issue Tracker
- Inspired by the Specification Pattern
- Built with modern Swift features and best practices
- Designed for real-world iOS/macOS application needs
Made with β€οΈ by the SpecificationKit team