Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ahmed-Ali committed Apr 23, 2024
0 parents commit d35fd49
Show file tree
Hide file tree
Showing 36 changed files with 2,556 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift

name: Build and Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build-and-test:
runs-on: macos-latest
steps:
- uses: swift-actions/setup-swift@v2
with:
swift-version: "5.10.0"
- name: Check out code
uses: actions/checkout@v2
- name: Build
run: swift build -v
- name: Test
run: swift test -v
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
/.build
/.swiftpm
/Packages
/*.swiftinterface
/*.xcodeproj
xcuserdata/
32 changes: 32 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# file options

--swiftversion 5.9
--exclude .build, **/*/Diagnostics.swift, **/*/DiagnosticsType.swift

# format options
--voidtype void
--ifdef no-indent
--indent 4
--importgrouping testable-bottom
--maxwidth 100
--stripunusedargs always
--trimwhitespace always
--wraparguments before-first
--wrapcollections before-first
--wrapconditions after-first
--typeattributes prev-line
--funcattributes prev-line
--varattributes prev-line
--lineaftermarks true
--typeblanklines remove
--extensionacl on-declarations
--asynccapturing explicit
--throwcapturing explicit
--guardelse same-line
--elseposition same-line
--header \n {file}\n\n Created by Ahmed Ali (github.com/Ahmed-Ali) on {created}.\n

## Rules
--enable preferKeyPath, leadingDelimiters, linebreakAtEndOfFile, conditionalAssignment, consecutiveBlankLines, redundantReturn, redundantObjc, redundantSelf, redundantVoidReturnType, redundantFileprivate, redundantType, redundantExtensionACL, redundantGet, redundantInit, redundantLet, redundantPattern, redundantRawValues, duplicateImports, emptyBraces, spaceAroundParens, spaceInsideBraces, spaceInsideBrackets, strongifiedSelf, todos, trailingClosures, wrapSingleLineComments, wrapSwitchCases, yodaConditions

--disable trailingCommas, andOperator, wrapMultilineStatementBraces
14 changes: 14 additions & 0 deletions .swiftlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
disabled_rules:
- line_length # Handled by swiftformat
- identifier_name # Not great for for loops

excluded: # paths or files to ignore during linting. Takes precedence over `included`.
- .build
- Package.swift
- Tests

strict: true # If true, SwiftLint will treat all warnings as errors.

force_exclusion: true # if true, will fail if there are SwiftLint violations in the excluded files

reporter: "emoji" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Ahmed Ali

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
24 changes: 24 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"originHash" : "f50bfa61790990bf4bfe36a55c7c976fd799f9af19aecf85a907235440d59514",
"pins" : [
{
"identity" : "lefthook-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/csjones/lefthook-plugin.git",
"state" : {
"revision" : "348e8fccfa863b3805c21e25aa2a6f08cd45d94a",
"version" : "1.6.10"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd",
"version" : "510.0.1"
}
}
],
"version" : 3
}
59 changes: 59 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to
// build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "BuildDSL",
platforms: [
.macOS(.v11), .iOS(.v13), .tvOS(.v13), .watchOS(.v6),
.macCatalyst(.v13), .visionOS(.v1)
],
products: [
.library(
name: "BuildDSL",
targets: ["BuildDSL"]
),
.executable(
name: "BuildDSLClient",
targets: ["BuildDSLClient"]
)
],
dependencies: [
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "510.0.1"
),
.package(url: "https://github.com/csjones/lefthook-plugin.git", from: "1.6.10")
],
targets: [
.macro(
name: "BuildDSLMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.target(
name: "BuildDSL",
dependencies: ["BuildDSLMacros"]
),
.executableTarget(
name: "BuildDSLClient",
dependencies: ["BuildDSL"]
),
.testTarget(
name: "BuildDSLTests",
dependencies: [
"BuildDSL",
"BuildDSLMacros",
.product(
name: "SwiftSyntaxMacrosTestSupport",
package: "swift-syntax"
)
]
)
]
)
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# BuildDSL

BuildDSL is a Swift package that offers a robust Domain-Specific Language (DSL) for crafting intuitive builder APIs for Swift structs. It streamlines the creation of complex objects with a clean, type-safe syntax, utilizing Swift's `@resultBuilder`, protocols, and generics, along with an auto-generated Builder pattern.

## Features

- **Type-Safe Builder Pattern**: Auto-generate builders for Swift structs with compile-time type checks.
- **Declarative Syntax**: Employ a succinct DSL to outline your object construction.
- **Automatic Code Generation**: Minimize boilerplate with auto-generated builder code.
- **Nested Builders**: Seamlessly construct complex objects using nested builders.
- **Error Handling**: Utilize `Result` types for comprehensive error handling.
- **Customizable Defaults**: Specify default values for immutable fields with `@Default`.
- **Property Exclusion**: Omit properties from the builder with `@Ignore`.

## Installation

### Swift Package Manager

Add BuildDSL to your project with Swift Package Manager by including the following in your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/Ahmed-Ali/BuildDSL.git", from:"0.1.0")
]
```

## Usage

Annotate your struct with `@Builder` and use `@Default`, `@Ignore`, and `@Escaping` for struct fields. Here's an example:

```swift
import BuildDSL

@Builder
struct Post {
let title: String
let content: String
@Default(Date())
let createdOn: Date
@Ignore
var popularityScore: Int = 5
}

// Create a Post using the generated Builder
let post = Post { $0
.title("BuildDSL")
.content("Building Intuitive APIs with BuildDSL")
}

// Handle errors with try-catch
do {
let mustHavePost = try Post.build { $0
// ...
}.get()
} catch {
// Error handling
}

// Or use a switch statement
let result = Post.build { $0.title("Title") }
switch result {
case .success(let post):
// Required fields are set
case .failure(let error):
// Inspect error.container and error.property
}
```

For more examples, see [Sources/BuildDSLClient/main.swift](Sources/BuildDSLClient/main.swift) and [Tests/BuildDSLTests/MacroUsageTests.swift](Tests/BuildDSLTests/MacroUsageTests.swift).
The [Sources/BuildDSL/Macros.swift](Sources/BuildDSL/Macros.swift) also documents each macro in details

## FAQ
- **Why use `@resultBuilder` instead of a closure?**
`@resultBuilder` ensures the closure is solely used for object construction, preventing arbitrary code execution and potential misuse.

- **Are there benefits to using Builder pattern + `@resultBuilder` over just a Builder?**
Yes, it enhances discoverability and IDE assistance, making it easier to understand how to initialize objects with complex configurations.

## Known Limitations & Workarounds
- **Initialization**: Structs must have a memberwise initializer. Exclude properties with `@Ignore` and provide default values or implement the initializer yourself.
- **Buildable Dependencies**: Declare dependent structs before dependees to ensure proper macro execution and avoid compilation errors.
- **Autocomplete**: While macros and Swift's Generics with `@resultBuilder` are powerful, IDE autocomplete may be less helpful with complex nested types. Patience and manual code entry may be required at times.

## Contribution
PRs and issues are always welcome. I will create a more comprehensive cotribution guidance if needed.

Happy building with BuildDSL!
38 changes: 38 additions & 0 deletions Sources/BuildDSL/ExportedTypes/BuilderError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// BuilderError.swift
//
// Created by Ahmed Ali (github.com/Ahmed-Ali) on 22/04/2024.
//

import Foundation

/**
Every generated Builder will return Result.failure(BuilderError)
if a non-optional field without a default value hasn't been set
*/
public enum BuilderError: Swift.Error {
case missingValueFor(_ property: String, container: String)

public var property: String? {
switch self {
case let .missingValueFor(property, container: _):
return property
}
}

public var container: String? {
switch self {
case let .missingValueFor(_, container: container):
return container
}
}
}

extension BuilderError: CustomStringConvertible {
public var description: String {
switch self {
case let .missingValueFor(property, container: container):
return "Missing a non-optional value for \(container).\(property)"
}
}
}
36 changes: 36 additions & 0 deletions Sources/BuildDSL/ExportedTypes/DSLResultBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// DSLResultBuilder.swift
//
// Created by Ahmed Ali (github.com/Ahmed-Ali) on 22/04/2024.
//

import Foundation

/**
This result builder is one of the pieces that helps ensure the safety of the builder closures.
Without it, the user of the builder closure can excute any code in the closure.
Using result builder, ensure only the builder APIs are usable within that closure
*/

@resultBuilder
public struct DSLResultBuilder<Builder: BuilderAPI> {
public static func buildExpression(_ instance: Builder) -> Builder {
instance
}

public static func buildEither(first instance: Builder) -> Builder {
instance
}

public static func buildEither(second instance: Builder) -> Builder {
instance
}

public static func buildBlock(_ builder: Builder) -> Builder {
builder
}

public static func buildFinalResult(_ builder: Builder) -> Builder.Result {
builder.build()
}
}
33 changes: 33 additions & 0 deletions Sources/BuildDSL/ExportedTypes/Protocols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Protocols.swift
//
// Created by Ahmed Ali (github.com/Ahmed-Ali) on 22/04/2024.
//

import Foundation

public protocol BuildableAPI<Builder> {
associatedtype Builder: BuilderAPI where Builder.Buildable == Self

typealias ResultBuilder = DSLResultBuilder<Builder>
typealias Result = Swift.Result<Self, BuilderError>
typealias Closure = (Builder) -> Self.Builder.Result
}

public protocol BuilderAPI<Buildable> {
associatedtype Buildable: BuildableAPI
typealias Result = Buildable.Result

init()

func build() -> Result
}

extension BuildableAPI {
public static func build(
@ResultBuilder _ resBuilder: Self
.Closure
) -> Self.Result {
resBuilder(Self.Builder())
}
}
Loading

0 comments on commit d35fd49

Please sign in to comment.