Skip to content

Commit b99ab74

Browse files
committed
feat: Behaviors & Assemblies
1 parent df5fa0e commit b99ab74

33 files changed

+1619
-321
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Contributing to Astroject
2+
Contributions are welcome! Please feel free to submit pull requests or open issues via these guidelines.
3+
4+
## Creating Issues
5+
// TODO
6+
7+
## Creating Pull Requests
8+
// TODO

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2025 Astroject Contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 323 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,329 @@
11
# Astroject
2-
Library for Dependency Injection with Swift concurrency in mind.
2+
3+
A lightweight and flexible dependency injection container for Swift.
4+
5+
[![Swift Version](https://img.shields.io/badge/Swift-5.5+-orange.svg)](https://swift.org)
6+
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37

48
## Overview
5-
Astroject is an advanced dependency injection library designed to help make dependency trees manageable and sufficient from the smallest to the largest scaled software project.
9+
Astroject is designed to simplify dependency management in your Swift projects. It provides a clean and intuitive API for registering and resolving dependencies, supporting both synchronous and asynchronous factories, various instance scopes (singleton, prototype, weak), and extensible behaviors. Many ideas came from a Sister Library [Swinject](https://github.com/Swinject/Swinject)
10+
11+
## API Documentation
12+
Coming Soon...
613

714
## Features
8-
- **Thread Safety**
9-
- **Swift Concurrency**
10-
- **Ease of Use & Adaptation**
15+
- **Synchronous and Asynchronous Registrations:** Register dependencies with both synchronous and asynchronous factory closures.
16+
- **Instance Scopes:** Supports singleton, prototype, and weak instance scopes.
17+
- **Named Registrations:** Register dependencies with optional names for disambiguation.
18+
- **Circular Dependency Detection:** Prevents and reports circular dependency issues.
19+
- **Extensible Behaviors:** Add custom behaviors to the container's registration process.
20+
- **Assemblies:** Organize registrations into reusable modules.
21+
- **Thread-Safe Operations:** Designed for safe concurrent access.
22+
23+
## Requirements
24+
- iOS 16.0+ / MacOS 13.0+ / WatchOS ??.?+ / tvOS ??.?+
25+
- Swift 5.5+
26+
- Xcode 16.0+
27+
28+
## Installation
29+
30+
### Swift Package Manager
31+
32+
Add Astroject as a dependency to your `Package.swift` file:
33+
34+
```swift
35+
dependencies: [
36+
.package(url: "https://github.com/astro-bytes/Astroject", from: "1.0.0")
37+
]
38+
```
39+
Then, import Astroject in your Swift files:
40+
```swift
41+
import Astroject
42+
```
43+
44+
## Usage
45+
### Basic Registration and Resolution
46+
This process is the most common usage of Astroject. Registering and resolving synchronous objects.
47+
48+
```swift
49+
import Astroject
50+
51+
protocol Service {
52+
func doSomething()
53+
}
54+
55+
class ConcreteService: Service {
56+
func doSomething() {}
57+
}
58+
59+
let container = Container()
60+
61+
// Register a dependency
62+
try container.register(Service.self) { _ in
63+
ConcreteService()
64+
}
65+
66+
// Resolve the dependency
67+
let service: Service = try container.resolve(Service.self)
68+
service.doSomething()
69+
```
70+
71+
### Asynchronous Registration
72+
Asynchronous registration primary supports any initializer that needs to run asynchronously. This tends to happen often with classes wrapped with @MainActor.
73+
74+
```swift
75+
import Astroject
76+
77+
protocol AsyncService {
78+
func doWork()
79+
}
80+
81+
@MainActor
82+
class ConcreteAsyncService: AsyncService {
83+
func doWord() {}
84+
}
85+
86+
let container = Container()
87+
88+
// Register an asynchronous dependency
89+
try container.registerAsync(AsyncService.self) { _ in
90+
await ConcreteAsyncService()
91+
}
92+
93+
// Resolve the dependency asynchronously
94+
let asyncService: AsyncService = try await container.resolveAsync(AsyncService.self)
95+
asyncService.doWork()
96+
```
97+
98+
### Named Registrations
99+
Additionally with Synchronous or Asynchronous Registrations you can provide a name attribute to ensure the registration is uniquely stored when storing the same type more than once.
100+
```swift
101+
import Astroject
102+
103+
protocol Database {
104+
func connect()
105+
}
106+
107+
class MySQLDatabase: Database {
108+
func connect() {}
109+
}
110+
111+
class PostgreSQLDatabase: Database {
112+
func connect() {}
113+
}
114+
115+
let container = Container()
116+
117+
try container.register(Database.self, name: "mysql") { _ in
118+
MySQLDatabase()
119+
}
120+
121+
try container.register(Database.self, name: "postgresql") { _ in
122+
PostgreSQLDatabase()
123+
}
124+
125+
let mysql: Database = try container.resolve(Database.self, name: "mysql")
126+
mysql.connect()
127+
128+
let postgresql: Database = try container.resolve(Database.self, name: "postgresql")
129+
postgresql.connect()
130+
```
131+
132+
### Intuitive Registration
133+
Coming Soon...
134+
135+
### Resource File Registration
136+
Coming Soon...
137+
138+
### Overriding Registrations
139+
Astroject naturally supports overridable registrations, meaning that when one registration is registered under the same type multiple times the latest registration will be used.
140+
141+
Here we will first register 42 as an Int in container and then immediately register 99 under the same key.
142+
```swift
143+
let container = Container()
144+
try container.register(Int.self) { _ in 42 }
145+
try container.register(Int.self) { _ in 99 }
146+
let value = container.resolve(Int.self) // Returns 99
147+
```
148+
As you can see the value will no longer only return 99.
149+
This is a powerful natural methodology. Allowing for your code to automatically override objects if you need to inject a Mocked Version of your class when running unit tests.
150+
151+
Additionally we also provide a way to prevent this natural overriding if you ever want to enforce only one registration at a time. This can be done during the registration process like such.
152+
```swift
153+
let container = Container()
154+
container.register(Int.self, overridable: false) { _ in 42}
155+
```
156+
Now if we try to register another Int anywhere else during our registration process the register will fail and throw a `RegistrationError.alreadyRegistered` error. This gives you as the developer more control over registrations and their flows.
157+
```swift
158+
try container.register(Int.self) { _ in 99 } // Fails and throws an error
159+
```
160+
161+
### Arguments
162+
Coming soon...
163+
164+
### Instance Scopes
165+
Astroject out of the box supports the following instance scopes.
166+
```swift
167+
class MyClass {
168+
init() {
169+
print("MyClass initialized")
170+
}
171+
}
172+
173+
let container = Container()
174+
```
175+
- Prototype(default) - A new instance is generated through every resolve for that object.
176+
```swift
177+
try container.register(MyClass.self, name: "prototype") { _ in
178+
MyClass()
179+
}
180+
181+
// Output: MyClass initialized
182+
let prototypeInstance1: MyClass = try container.resolve(MyClass.self, name: "prototype")
183+
184+
// Output: MyClass initialized (new instance)
185+
let prototypeInstance2: MyClass = try container.resolve(MyClass.self, name: "prototype")
186+
```
187+
- Singleton - Instance maintained and kep throughout the life time of the container.
188+
```swift
189+
try container.register(MyClass.self) { _ in
190+
MyClass()
191+
}
192+
.asSingleton()
193+
194+
// Output: MyClass initialized
195+
let instance1: MyClass = try container.resolve(MyClass.self)
196+
197+
// No output (same instance)
198+
let instance2: MyClass = try container.resolve(MyClass.self) ```
199+
- Weak - As long as you retain an instance to the object the instance remains in the container when asked for. If you have no references to the class then container will deallocate its reference.
200+
```swift
201+
try container.register(MyClass.self) { _ in
202+
MyClass()
203+
}
204+
.asWeak()
205+
206+
// Output: MyClass initialized
207+
var instance1: MyClass? = try container.resolve(MyClass.self)
208+
// No output (same instance)
209+
var instance2: MyClass? = try container.resolve(MyClass.self)
210+
211+
// Release instance1 reference
212+
instance1 = nil
213+
214+
// No output (same instance) because instance2 was not deallocated
215+
var instance3: MyClass? = try container.resolve(MyClass.self)
216+
217+
// Release instance 2 & 3 reference
218+
instance2 = nil
219+
instance3 = nil
220+
221+
// Output: MyClass initialized
222+
let instance4: MyClass = try container.resolve(MyClass.self)
223+
```
224+
#### Custom Scopes
225+
Additionally you can create your own Scopes through utilization of the `Instance` protocol and the `as` function on `any Registrable`.
226+
```swift
227+
class ExampleInstance: Instance {
228+
// Conform to Instance methods
229+
}
230+
231+
// Insert through `as` function
232+
container.register(Int.self) { _ in 42 }.as(ExampleInstance())
233+
```
234+
Convenience functions can also be created by extending `Registrable`
235+
```swift
236+
extension Registrable {
237+
@discardableResult
238+
func exampleInstance() -> Self {
239+
self.as(ExampleInstance())
240+
}
241+
}
242+
```
243+
244+
If you need a combination of multiple scopes just create the scopes you need then add them to our `Composite` Instance object. `Composite` takes the first instance not nil from a list of `Instance` objects.
245+
```swift
246+
let composite = Composite([ExampleInstance(), Weak()])
247+
container.register(Int.self) { _ in 42 }.as { composite }
248+
```
249+
250+
### Behaviors
251+
Behaviors allow for additional functionality in the container. They are applied to each registration as they are registered.
252+
253+
```Swift
254+
import Astroject
255+
256+
class LoggingBehavior: Behavior {
257+
func didRegister<Product>(
258+
type: Product.Type,
259+
to container: Container,
260+
as registration: Registration<Product>,
261+
with name: String?
262+
) {
263+
print("Registered \(type) with name: \(name ?? "nil")")
264+
}
265+
}
266+
267+
let container = Container()
268+
container.add(LoggingBehavior())
269+
270+
// Output: Registered Int with name: 42
271+
try container.register(Int.self, name: "42") { _ in 42 }
272+
```
273+
274+
### Nested Containers
275+
Coming Soon...
276+
277+
### Assemblies
278+
Assemblies allow for modularized code structure and can be used to assemble how ever you would like. They provide a function where registration can best be structured as well as provide a function to hook into when all registrations are complete.
279+
```Swift
280+
import Astroject
281+
282+
class MyAssembly: Assembly {
283+
func assemble(container: Container) {
284+
try? container.register(String.self) { _ in "Hello, Astroject!" }
285+
}
286+
287+
func loaded(resolver: Resolver) {
288+
if let message: String = try? resolver.resolve(String.self) {
289+
print("Assembly loaded with message: \(message)")
290+
}
291+
}
292+
}
293+
294+
let container = Container()
295+
let assembler = Assembler(container: container)
296+
// Output: Assembly loaded with message: Hello, Astroject!
297+
assembler.apply(assembly: MyAssembly())
298+
299+
let message: String = try container.resolve(String.self)
300+
// Output: Hello, Astroject!
301+
print(message)
302+
```
303+
304+
### Error Handling
305+
Astroject provides detailed error handling through the `ResolutionError`, `InstanceError` and `RegistrationError` enums.
306+
307+
## Sample Code
308+
Checkout our sample code under the [playgrounds](/Playgrounds) directory.
309+
310+
## Looking to the Future
311+
Listed below are features maintainers plan to bring to this library.
312+
- Add Sample Code via Playgrounds
313+
- Arguments - Pass in additional arguments to a factory or resolution
314+
- Parent Containers
315+
- Intuitive Registration - Registration is based on the initializer of an object
316+
- Resource File Registration - Automatically register object from bundle and resource files
317+
- DocC - Swift Documentation
318+
319+
## Contributing
320+
See [documentation](CONTRIBUTING.md) for more details and guidelines.
321+
322+
## Credits
323+
Astroject was inspired by
324+
- [Swinject](https://github.com/Swinject/Swinject).
325+
- [SwinjectAutoRegistration](https://github.com/Swinject/SwinjectAutoregistration)
326+
- [SwinjectPropertyLoader](https://github.com/Swinject/SwinjectPropertyLoader)
11327

328+
## License
329+
Astroject is released under the MIT License. See [LICENSE](LICENSE) for details.

0 commit comments

Comments
 (0)