Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "Swinject/Swinject" ~> 2.0.0
github "Swinject/Swinject" ~> 2.9.1
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "Swinject/Swinject" "2.7.1"
github "Swinject/Swinject" "2.9.1"
github "jspahrsummers/xcconfigs" "1.1"
23 changes: 23 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SwinjectPropertyLoader",
platforms: [
.iOS(.v15),
.macOS(.v12),
.tvOS(.v15),
.watchOS(.v8)
],
products: [
.library(
name: "SwinjectPropertyLoader",
targets: ["SwinjectPropertyLoader"]),
],
dependencies: [
.package(url: "https://github.com/Swinject/Swinject.git", from: "2.9.1"),
.package(url: "https://github.com/LebJe/TOMLKit.git", from: "0.6.0")
],
targets: [
.target(
name: "SwinjectPropertyLoader",
dependencies: ["Swinject", "TOMLKit"],
path: "Sources"),
.testTarget(
name: "SwinjectPropertyLoaderTests",
dependencies: ["SwinjectPropertyLoader"],
path: "Tests",
resources: [
.process("Resources")
]),
],
swiftLanguageVersions: [.v5, .version("6")]
)
249 changes: 220 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,57 @@ SwinjectPropertyLoader
[![CocoaPods Version](https://img.shields.io/cocoapods/v/SwinjectPropertyLoader.svg?style=flat)](http://cocoapods.org/pods/SwinjectPropertyLoader)
[![License](https://img.shields.io/cocoapods/l/SwinjectPropertyLoader.svg?style=flat)](http://cocoapods.org/pods/SwinjectPropertyLoader)
[![Platform](https://img.shields.io/cocoapods/p/SwinjectPropertyLoader.svg?style=flat)](http://cocoapods.org/pods/SwinjectPropertyLoader)
[![Swift Version](https://img.shields.io/badge/Swift-2.2--3.0.x-F16D39.svg?style=flat)](https://developer.apple.com/swift)
[![Swift Version](https://img.shields.io/badge/Swift-6.0-F16D39.svg?style=flat)](https://developer.apple.com/swift)


SwinjectPropertyLoader is an extension of Swinject to load property values from resources that are bundled with your application or framework.

## Requirements

- iOS 8.0+ / Mac OS X 10.10+ / watchOS 2.0+ / tvOS 9.0+
- Swift 2.2 or 2.3
- Xcode 7.0+
- Swift 3.0.x
- Xcode 8.0+
- Carthage 0.18+ (if you use)
- CocoaPods 1.1.1+ (if you use)
- iOS 15.0+ / macOS 12.0+ / watchOS 8.0+ / tvOS 15.0+
- Swift 5.9+
- Xcode 15.0+

## Installation

Swinject is available through [Carthage](https://github.com/Carthage/Carthage) or [CocoaPods](https://cocoapods.org).
### Swift Package Manager

To install SwinjectPropertyLoader using Swift Package Manager, add the following to your `Package.swift` file:

```swift
dependencies: [
.package(url: "https://github.com/Swinject/SwinjectPropertyLoader.git", from: "2.0.0")
]
```

Or add it through Xcode:
1. File > Add Package Dependencies...
2. Enter package URL: `https://github.com/Swinject/SwinjectPropertyLoader.git`
3. Select version 2.0.0 or later

### Carthage

To install Swinject with Carthage, add the following line to your `Cartfile`.
To install SwinjectPropertyLoader with Carthage, add the following line to your `Cartfile`:

```
github "Swinject/Swinject" ~> 2.0.0
github "Swinject/SwinjectPropertyLoader" ~> 1.0.0
github "Swinject/Swinject" ~> 2.9.1
github "Swinject/SwinjectPropertyLoader" ~> 2.0.0
```

Then run `carthage update --no-use-binaries` command or just `carthage update`. For details of the installation and usage of Carthage, visit [its project page](https://github.com/Carthage/Carthage).


### CocoaPods

To install Swinject with CocoaPods, add the following lines to your `Podfile`.
To install SwinjectPropertyLoader with CocoaPods, add the following lines to your `Podfile`:

```ruby
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0' # or platform :osx, '10.10' if your target is OS X.
platform :ios, '15.0' # or platform :osx, '12.0' for macOS
use_frameworks!

pod 'Swinject', '~> 2.0.0'
pod 'SwinjectPropertyLoader', '~> 1.0.0'
pod 'Swinject', '~> 2.9.1'
pod 'SwinjectPropertyLoader', '~> 2.0.0'
```

Then run `pod install` command. For details of the installation and usage of CocoaPods, visit [its official website](https://cocoapods.org).
Expand All @@ -57,19 +66,23 @@ Then run `pod install` command. For details of the installation and usage of Coc
Properties are values that can be loaded from resources that are bundled with your application/framework.
Properties can then be used when assembling definitions in your container.

There are 2 types of support property formats:
There are 4 types of supported property loaders:

- JSON (`JsonPropertyLoader`)
- Plist (`PlistPropertyLoader`)
- JSON (`JsonPropertyLoader`) - Load from JSON files
- Plist (`PlistPropertyLoader`) - Load from Plist files
- TOML (`TomlPropertyLoader`) - Load from TOML files with dot notation
- Struct (`StructPropertyLoader`) - Load from Swift struct/class instances using reflection

Each format supports the types specified by the format itself. If JSON format is used
then your basic types: `Bool`, `Int`, `Double`, `String`, `Array` and `Dictionary` are
supported. For Plist, all types supported by the Plist are supported which include all
JSON types plus `NSDate` and `NSData`.
Each loader supports different value types:
- **JSON**: `Bool`, `Int`, `Double`, `String`, `Array`, `Dictionary` (with comment support)
- **Plist**: All JSON types plus `NSDate` and `NSData`
- **TOML**: All TOML types (integers, floats, booleans, strings, dates, arrays, tables) - nested tables are auto-flattened to dot notation
- **Struct**: All Swift types via reflection - nested structs/classes are auto-flattened to dot notation

JSON property files also support comments which allow you to provide more context to
your properties besides your property key names. For example:
JSON and TOML property files support comments which allow you to provide more context to
your properties besides your property key names.

**JSON comments:**
```js
{
// Comment type 1
Expand All @@ -85,17 +98,65 @@ your properties besides your property key names. For example:
}
```

**TOML comments and nested tables:**
```toml
# TOML natively supports comments
foo = "bar"
baz = 100

# Nested tables are automatically flattened to dot notation
[api]
base_url = "https://api.example.com"
timeout = 30

[packages.unlimited]
cost = 99.99
features = ["feature1", "feature2"]
```

TOML nested tables are automatically flattened, so `[api]` with `base_url = "..."` becomes
accessible as `r.property("api.base_url")` in your container.

Loading properties into the container is as simple as:

```swift
let container = Container()

// will load "properties.json" from the main app bundle
let loader = JsonPropertyLoader(bundle: .mainBundle(), name: "properties")

try! container.applyPropertyLoader(loader)
// Load from bundle (traditional approach)
let jsonLoader = JsonPropertyLoader(bundle: .main, name: "properties")
try container.applyPropertyLoader(jsonLoader)

// Or load from a URL (decoupled from bundle)
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let configURL = documentsURL.appendingPathComponent("config.json")
let urlLoader = JsonPropertyLoader(url: configURL)
try container.applyPropertyLoader(urlLoader)

// Load TOML with automatic dot notation for nested tables
let tomlLoader = TomlPropertyLoader(bundle: .main, name: "config")
try container.applyPropertyLoader(tomlLoader)

// TOML from URL
let tomlURL = documentsURL.appendingPathComponent("config.toml")
let tomlURLLoader = TomlPropertyLoader(url: tomlURL)
try container.applyPropertyLoader(tomlURLLoader)

// Load from Swift struct/class using reflection (no file needed!)
struct AppConfig {
let apiKey = "secret123"
let timeout = 30
}
let config = AppConfig()
let structLoader = StructPropertyLoader(config)
try container.applyPropertyLoader(structLoader)
```

The URL-based loading allows you to load properties from anywhere in the file system, making it useful for:
- Downloaded configuration files
- User-specific settings stored in Documents
- Temporary configuration files
- Files in Application Support directory

Now you can inject properties into definitions registered into the container.

Consider the following definition:
Expand Down Expand Up @@ -189,6 +250,136 @@ And:

The resulting value for `items` would be: `[ "hello from B" ]`

### TOML Dot Notation Example

TOML's nested table structure is particularly useful for organizing hierarchical configuration:

```toml
# config.toml
[api]
base_url = "https://api.example.com"
timeout = 30
api_key = "secret123"

[database]
host = "localhost"
port = 5432
name = "myapp"

[packages.unlimited]
cost = 99.99
features = ["feature1", "feature2", "feature3"]

[packages.basic]
cost = 9.99
features = ["feature1"]
```

This automatically becomes accessible via dot notation:

```swift
container.register(APIClient.self) { r in
let client = APIClient()
client.baseURL = r.property("api.base_url") // "https://api.example.com"
client.timeout = r.property("api.timeout") // 30
client.apiKey = r.property("api.api_key") // "secret123"
return client
}

container.register(Database.self) { r in
let db = Database()
db.host = r.property("database.host")! // "localhost"
db.port = r.property("database.port")! // 5432
db.name = r.property("database.name")! // "myapp"
return db
}

container.register(PricingService.self) { r in
let service = PricingService()
service.unlimitedCost = r.property("packages.unlimited.cost")! // 99.99
return service
}
```

### Struct Reflection Example

For type-safe, programmatic configuration without external files, use `StructPropertyLoader`:

```swift
// Define a configuration struct
struct AppConfig {
struct API {
let baseURL = "https://api.example.com"
let timeout = 30
let apiKey = "secret123"
}

let api = API()
let appName = "MyApp"
let debugMode = false
}

// Load properties from struct instance
let config = AppConfig()
let loader = StructPropertyLoader(config)
try container.applyPropertyLoader(loader)

// Access with dot notation (nested structs are auto-flattened)
container.register(APIClient.self) { r in
let client = APIClient()
client.baseURL = r.property("api.baseURL")! // "https://api.example.com"
client.timeout = r.property("api.timeout")! // 30
client.apiKey = r.property("api.apiKey")! // "secret123"
return client
}

let appName: String? = container.property("appName") // "MyApp"
let debugMode: Bool? = container.property("debugMode") // false
```

**Benefits of StructPropertyLoader:**
- **Type-safe**: Compile-time checking of property types
- **No files**: Pure Swift configuration, no external resources
- **Dot notation**: Nested structs automatically flatten (like TOML)
- **Testing**: Perfect for default configs and test fixtures
- **Optionals**: Nil optionals are automatically skipped

### Type-Safe Property Keys

For better autocomplete, compile-time safety, and refactoring support, use `PropertyKey` instead of strings:

```swift
// Define your keys (anywhere in any module)
extension PropertyKey {
// Recommended: Explicit constructor (self-documenting)
static let apiBaseURL = PropertyKey("api.baseURL")
static let apiTimeout = PropertyKey("api.timeout")
static let apiKey = PropertyKey("api.key")

// Alternative: String literal with type annotation (also valid)
static let debugMode: PropertyKey = "debug.enabled"
}

// Type-safe access with autocomplete
container.register(APIClient.self) { r in
let client = APIClient()

// Type-safe property access
client.baseURL = r.property(forKey: .apiBaseURL)
client.timeout = r.property(forKey: .apiTimeout) ?? 30 // Use ?? for defaults

return client
}
```

**Benefits:**
- ✅ **Autocomplete**: All defined keys appear in Xcode autocomplete
- ✅ **Type-safe**: Compiler catches typos and missing keys
- ✅ **Refactoring**: Rename works correctly across your codebase
- ✅ **Extensible**: Define keys in any module via extensions
- ✅ **Backward compatible**: String-based API still works


## Contributors

SwinjectPropertyLoader has been originally written by [Mike Owens](https://github.com/mowens).
Expand Down
Loading