From 12b377f8e1df18994e1c9693f6c6399e7f9ddeb2 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 17 Jun 2025 16:19:58 -0400 Subject: [PATCH 1/9] create freestanding macro --- Macros/SKSampleMacro/.gitignore | 8 +++ Macros/SKSampleMacro/Package.resolved | 15 ++++ Macros/SKSampleMacro/Package.swift | 59 ++++++++++++++++ .../Sources/SKSampleMacro/SKSampleMacro.swift | 11 +++ .../Sources/SKSampleMacroClient/main.swift | 8 +++ .../SKSampleMacroMacro.swift | 42 +++++++++++ .../SKSampleMacroTests.swift | 48 +++++++++++++ Package.resolved | 2 +- Sources/SyntaxKit/CodeBlock+ExprSyntax.swift | 30 ++++++++ Sources/SyntaxKit/Infix.swift | 70 +++++++++++++++++++ Sources/SyntaxKit/Tuple.swift | 60 ++++++++++++++++ project.yml | 2 + 12 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 Macros/SKSampleMacro/.gitignore create mode 100644 Macros/SKSampleMacro/Package.resolved create mode 100644 Macros/SKSampleMacro/Package.swift create mode 100644 Macros/SKSampleMacro/Sources/SKSampleMacro/SKSampleMacro.swift create mode 100644 Macros/SKSampleMacro/Sources/SKSampleMacroClient/main.swift create mode 100644 Macros/SKSampleMacro/Sources/SKSampleMacroMacros/SKSampleMacroMacro.swift create mode 100644 Macros/SKSampleMacro/Tests/SKSampleMacroTests/SKSampleMacroTests.swift create mode 100644 Sources/SyntaxKit/CodeBlock+ExprSyntax.swift create mode 100644 Sources/SyntaxKit/Infix.swift create mode 100644 Sources/SyntaxKit/Tuple.swift diff --git a/Macros/SKSampleMacro/.gitignore b/Macros/SKSampleMacro/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Macros/SKSampleMacro/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Macros/SKSampleMacro/Package.resolved b/Macros/SKSampleMacro/Package.resolved new file mode 100644 index 0000000..25225ed --- /dev/null +++ b/Macros/SKSampleMacro/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "9eace0238bc49301f22ac682c8f3e981d6f1a63573efd4e1a727b71527c4ebb0", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + } + ], + "version" : 3 +} diff --git a/Macros/SKSampleMacro/Package.swift b/Macros/SKSampleMacro/Package.swift new file mode 100644 index 0000000..b0688ff --- /dev/null +++ b/Macros/SKSampleMacro/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "SKSampleMacro", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + .visionOS(.v1) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "SKSampleMacro", + targets: ["SKSampleMacro"] + ), + .executable( + name: "SKSampleMacroClient", + targets: ["SKSampleMacroClient"] + ), + ], + dependencies: [ + .package(path: "../.."), + .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Macro implementation that performs the source transformation of a macro. + .macro( + name: "SKSampleMacroMacros", + dependencies: [ + .product(name: "SyntaxKit", package: "SyntaxKit"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + + // Library that exposes a macro as part of its API, which is used in client programs. + .target(name: "SKSampleMacro", dependencies: ["SKSampleMacroMacros"]), + + // A client of the library, which is able to use the macro in its own code. + .executableTarget(name: "SKSampleMacroClient", dependencies: ["SKSampleMacro"]), + + // A test target used to develop the macro implementation. + .testTarget( + name: "SKSampleMacroTests", + dependencies: [ + "SKSampleMacroMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/Macros/SKSampleMacro/Sources/SKSampleMacro/SKSampleMacro.swift b/Macros/SKSampleMacro/Sources/SKSampleMacro/SKSampleMacro.swift new file mode 100644 index 0000000..d812512 --- /dev/null +++ b/Macros/SKSampleMacro/Sources/SKSampleMacro/SKSampleMacro.swift @@ -0,0 +1,11 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +/// A macro that produces both a value and a string containing the +/// source code that generated the value. For example, +/// +/// #stringify(x + y) +/// +/// produces a tuple `(x + y, "x + y")`. +@freestanding(expression) +public macro stringify(_ lhs: T, _ rhs: T) -> (T, String) = #externalMacro(module: "SKSampleMacroMacros", type: "StringifyMacro") diff --git a/Macros/SKSampleMacro/Sources/SKSampleMacroClient/main.swift b/Macros/SKSampleMacro/Sources/SKSampleMacroClient/main.swift new file mode 100644 index 0000000..1fdb254 --- /dev/null +++ b/Macros/SKSampleMacro/Sources/SKSampleMacroClient/main.swift @@ -0,0 +1,8 @@ +import SKSampleMacro + +let a = 17 +let b = 25 + +let (result, code) = #stringify(a, b) + +print("The value \(result) was produced by the code \"\(code)\"") diff --git a/Macros/SKSampleMacro/Sources/SKSampleMacroMacros/SKSampleMacroMacro.swift b/Macros/SKSampleMacro/Sources/SKSampleMacroMacros/SKSampleMacroMacro.swift new file mode 100644 index 0000000..014fb7e --- /dev/null +++ b/Macros/SKSampleMacro/Sources/SKSampleMacroMacros/SKSampleMacroMacro.swift @@ -0,0 +1,42 @@ +import SwiftCompilerPlugin +import SwiftSyntax +//import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SyntaxKit + +/// Implementation of the `stringify` macro, which takes an expression +/// of any type and produces a tuple containing the value of that expression +/// and the source code that produced the value. For example +/// +/// #stringify(x + y) +/// +/// will expand to +/// +/// (x + y, "x + y") +public struct StringifyMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) -> ExprSyntax { + let first = node.arguments.first?.expression + let second = node.arguments.last?.expression + guard let first, let second else { + fatalError("compiler bug: the macro does not have any arguments") + } + + return Tuple{ + Infix("+") { + VariableExp(first.description) + VariableExp(second.description) + } + Literal.string("\(first.description) + \(second.description)") + }.expr + } +} + +@main +struct SKSampleMacroPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + StringifyMacro.self, + ] +} diff --git a/Macros/SKSampleMacro/Tests/SKSampleMacroTests/SKSampleMacroTests.swift b/Macros/SKSampleMacro/Tests/SKSampleMacroTests/SKSampleMacroTests.swift new file mode 100644 index 0000000..ca69367 --- /dev/null +++ b/Macros/SKSampleMacro/Tests/SKSampleMacroTests/SKSampleMacroTests.swift @@ -0,0 +1,48 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. +#if canImport(SKSampleMacroMacros) +import SKSampleMacroMacros + +let testMacros: [String: Macro.Type] = [ + "stringify": StringifyMacro.self, +] +#endif + +final class SKSampleMacroTests: XCTestCase { + func testMacro() throws { + #if canImport(SKSampleMacroMacros) + assertMacroExpansion( + """ + #stringify(a + b) + """, + expandedSource: """ + (a + b, "a + b") + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroWithStringLiteral() throws { + #if canImport(SKSampleMacroMacros) + assertMacroExpansion( + #""" + #stringify("Hello, \(name)") + """#, + expandedSource: #""" + ("Hello, \(name)", #""Hello, \(name)""#) + """#, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} diff --git a/Package.resolved b/Package.resolved index 9a2ef48..fc91c87 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b5881f7ff763cf360a3639d99029de2b3ab4a457e0d8f08ce744bae51c2bf670", + "originHash" : "79e6b2b96efe3d22ee340e623938e0363d6ce8fb0edfd7ccdf90e98b766f59ec", "pins" : [ { "identity" : "swift-syntax", diff --git a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift new file mode 100644 index 0000000..59b8ec9 --- /dev/null +++ b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift @@ -0,0 +1,30 @@ +// +// CodeBlock+ExprSyntax.swift +// SyntaxKit +// +// Created by Leo Dion. +// Provides convenience for converting a CodeBlock into ExprSyntax when appropriate. +// + +import SwiftSyntax + +extension CodeBlock { + /// Attempts to treat this `CodeBlock` as an expression and return its `ExprSyntax` form. + /// + /// If the underlying syntax already *is* an `ExprSyntax`, it is returned directly. If the + /// underlying syntax is a bare `TokenSyntax` (commonly the case for `VariableExp` which + /// produces an identifier token), we wrap it in a `DeclReferenceExprSyntax` so that it becomes + /// a valid expression node. Any other kind of syntax results in a runtime error, because it + /// cannot be represented as an expression (e.g. declarations or statements). + public var expr: ExprSyntax { + if let expr = self.syntax.as(ExprSyntax.self) { + return expr + } + + if let token = self.syntax.as(TokenSyntax.self) { + return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(token.text))) + } + + fatalError("CodeBlock of type \(type(of: self.syntax)) cannot be represented as ExprSyntax") + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/Infix.swift b/Sources/SyntaxKit/Infix.swift new file mode 100644 index 0000000..33a2825 --- /dev/null +++ b/Sources/SyntaxKit/Infix.swift @@ -0,0 +1,70 @@ +// +// Infix.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +/// A generic binary (infix) operator expression, e.g. `a + b`. +public struct Infix: CodeBlock { + private let op: String + private let operands: [CodeBlock] + + /// Creates an infix operator expression. + /// - Parameters: + /// - op: The operator symbol as it should appear in source (e.g. "+", "-", "&&"). + /// - content: A ``CodeBlockBuilder`` that supplies the two operand expressions. + /// + /// Exactly two operands must be supplied – a left-hand side and a right-hand side. + public init(_ op: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.op = op + self.operands = content() + } + + public var syntax: SyntaxProtocol { + guard operands.count == 2 else { + fatalError("Infix expects exactly two operands, got \(operands.count).") + } + + let left = operands[0].expr + let right = operands[1].expr + + let operatorExpr = ExprSyntax( + BinaryOperatorExprSyntax( + operator: .binaryOperator(op, leadingTrivia: .space, trailingTrivia: .space) + ) + ) + + return SequenceExprSyntax( + elements: ExprListSyntax([ + left, + operatorExpr, + right, + ]) + ) + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/Tuple.swift b/Sources/SyntaxKit/Tuple.swift new file mode 100644 index 0000000..5becc9a --- /dev/null +++ b/Sources/SyntaxKit/Tuple.swift @@ -0,0 +1,60 @@ +// +// Tuple.swift +// SyntaxKit +// +// Created by Leo Dion. +// This file defines a `Tuple` code-block that generates a Swift tuple expression. +// It is primarily useful for macro expansions or DSL code that needs to group +// multiple expression `CodeBlock`s together, for example: +// +// Tuple { +// VariableExp("value") +// Literal.string("debug") +// }.expr // -> ExprSyntax for `(value, "debug")` +// +// The result is represented as a `TupleExprSyntax`, which naturally conforms to +// `ExprSyntax` and therefore plays nicely with our `CodeBlock.expr` convenience. +// + +import SwiftSyntax + +/// A tuple expression, e.g. `(a, b, c)`. +public struct Tuple: CodeBlock { + private let elements: [CodeBlock] + + /// Creates a tuple expression comprising the supplied elements. + /// - Parameter content: A ``CodeBlockBuilder`` producing the tuple elements **in order**. + /// Elements may be any `CodeBlock` that can be represented as an expression (see + /// `CodeBlock.expr`). + public init(@CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.elements = content() + } + + public var syntax: SyntaxProtocol { + guard !elements.isEmpty else { + fatalError("Tuple must contain at least one element.") + } + + let list = TupleExprElementListSyntax( + elements.enumerated().map { index, block in + let elementExpr = block.expr + return TupleExprElementSyntax( + label: nil, + colon: nil, + expression: elementExpr, + trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil + ) + } + ) + + let tupleExpr = ExprSyntax( + TupleExprSyntax( + leftParen: .leftParenToken(), + elements: list, + rightParen: .rightParenToken() + ) + ) + + return tupleExpr + } +} \ No newline at end of file diff --git a/project.yml b/project.yml index d39d844..b2cfbdc 100644 --- a/project.yml +++ b/project.yml @@ -4,6 +4,8 @@ settings: packages: SyntaxKit: path: . + SKSampleMacro: + path: Macros/SKSampleMacro aggregateTargets: Lint: buildScripts: From 747c35eb4919575d7c4172fb2963e2ffcefa4875 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 17 Jun 2025 17:11:02 -0400 Subject: [PATCH 2/9] git subrepo clone --branch=syntaxkit-sample git@github.com:brightdigit/Options.git Macros/Options subrepo: subdir: "Macros/Options" merged: "a2fd9e3" upstream: origin: "git@github.com:brightdigit/Options.git" branch: "syntaxkit-sample" commit: "a2fd9e3" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6301c2d31f" --- Macros/Options/.github/workflows/Options.yml | 242 ++++++++ Macros/Options/.gitignore | 133 +++++ Macros/Options/.gitrepo | 12 + Macros/Options/.hound.yml | 2 + Macros/Options/.periphery.yml | 1 + Macros/Options/.spi.yml | 4 + Macros/Options/.swift-version | 1 + Macros/Options/.swiftformat | 7 + Macros/Options/.swiftlint.yml | 118 ++++ Macros/Options/LICENSE | 22 + Macros/Options/Mintfile | 2 + Macros/Options/Package.swift | 34 ++ Macros/Options/Package@swift-5.10.swift | 65 +++ Macros/Options/README.md | 182 ++++++ Macros/Options/Scripts/docc.sh | 3 + Macros/Options/Scripts/gh-md-toc | 421 +++++++++++++ Macros/Options/Scripts/lint.sh | 44 ++ Macros/Options/Sources/Options/Array.swift | 58 ++ .../Sources/Options/CodingOptions.swift | 49 ++ .../Options/Sources/Options/Dictionary.swift | 51 ++ .../Documentation.docc/Documentation.md | 162 +++++ Macros/Options/Sources/Options/EnumSet.swift | 139 +++++ Macros/Options/Sources/Options/Macro.swift | 42 ++ .../Options/Sources/Options/MappedEnum.swift | 66 +++ .../MappedValueRepresentable+Codable.swift | 97 +++ .../Options/MappedValueRepresentable.swift | 68 +++ .../MappedValueRepresentableError.swift | 47 ++ .../Options/MappedValueRepresented.swift | 62 ++ .../Sources/Options/MappedValues.swift | 42 ++ .../OptionsMacros/Extensions/Array.swift | 42 ++ .../Extensions/ArrayExprSyntax.swift | 43 ++ .../Extensions/DeclModifierListSyntax.swift | 42 ++ .../Extensions/DeclModifierSyntax.swift | 39 ++ .../Extensions/DictionaryElementSyntax.swift | 47 ++ .../Extensions/DictionaryExprSyntax.swift | 41 ++ .../Extensions/EnumDeclSyntax.swift | 42 ++ .../Extensions/ExtensionDeclSyntax.swift | 57 ++ .../Extensions/InheritanceClauseSyntax.swift | 44 ++ .../OptionsMacros/Extensions/KeyValues.swift | 70 +++ .../Extensions/TypeAliasDeclSyntax.swift | 39 ++ .../Extensions/VariableDeclSyntax.swift | 81 +++ .../OptionsMacros/InvalidDeclError.swift | 35 ++ .../Sources/OptionsMacros/MacrosPlugin.swift | 39 ++ .../Sources/OptionsMacros/OptionsMacro.swift | 69 +++ .../Tests/OptionsTests/EnumSetTests.swift | 90 +++ .../Tests/OptionsTests/MappedEnumTests.swift | 69 +++ ...appedValueCollectionRepresentedTests.swift | 157 +++++ ...appedValueDictionaryRepresentedTests.swift | 77 +++ .../MappedValueRepresentableTests.swift | 40 ++ .../Mocks/MockCollectionEnum.swift | 61 ++ .../Mocks/MockDictionaryEnum.swift | 58 ++ .../Tests/OptionsTests/Mocks/MockError.swift | 34 ++ Macros/Options/codecov.yml | 2 + Macros/Options/logo.png | Bin 0 -> 89318 bytes Macros/Options/logo.svg | 551 ++++++++++++++++++ Macros/Options/project.yml | 13 + 56 files changed, 4058 insertions(+) create mode 100644 Macros/Options/.github/workflows/Options.yml create mode 100644 Macros/Options/.gitignore create mode 100644 Macros/Options/.gitrepo create mode 100644 Macros/Options/.hound.yml create mode 100644 Macros/Options/.periphery.yml create mode 100644 Macros/Options/.spi.yml create mode 100644 Macros/Options/.swift-version create mode 100644 Macros/Options/.swiftformat create mode 100644 Macros/Options/.swiftlint.yml create mode 100644 Macros/Options/LICENSE create mode 100644 Macros/Options/Mintfile create mode 100644 Macros/Options/Package.swift create mode 100644 Macros/Options/Package@swift-5.10.swift create mode 100644 Macros/Options/README.md create mode 100755 Macros/Options/Scripts/docc.sh create mode 100755 Macros/Options/Scripts/gh-md-toc create mode 100755 Macros/Options/Scripts/lint.sh create mode 100644 Macros/Options/Sources/Options/Array.swift create mode 100644 Macros/Options/Sources/Options/CodingOptions.swift create mode 100644 Macros/Options/Sources/Options/Dictionary.swift create mode 100644 Macros/Options/Sources/Options/Documentation.docc/Documentation.md create mode 100644 Macros/Options/Sources/Options/EnumSet.swift create mode 100644 Macros/Options/Sources/Options/Macro.swift create mode 100644 Macros/Options/Sources/Options/MappedEnum.swift create mode 100644 Macros/Options/Sources/Options/MappedValueRepresentable+Codable.swift create mode 100644 Macros/Options/Sources/Options/MappedValueRepresentable.swift create mode 100644 Macros/Options/Sources/Options/MappedValueRepresentableError.swift create mode 100644 Macros/Options/Sources/Options/MappedValueRepresented.swift create mode 100644 Macros/Options/Sources/Options/MappedValues.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/Array.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/KeyValues.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift create mode 100644 Macros/Options/Sources/OptionsMacros/InvalidDeclError.swift create mode 100644 Macros/Options/Sources/OptionsMacros/MacrosPlugin.swift create mode 100644 Macros/Options/Sources/OptionsMacros/OptionsMacro.swift create mode 100644 Macros/Options/Tests/OptionsTests/EnumSetTests.swift create mode 100644 Macros/Options/Tests/OptionsTests/MappedEnumTests.swift create mode 100644 Macros/Options/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift create mode 100644 Macros/Options/Tests/OptionsTests/MappedValueDictionaryRepresentedTests.swift create mode 100644 Macros/Options/Tests/OptionsTests/MappedValueRepresentableTests.swift create mode 100644 Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift create mode 100644 Macros/Options/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift create mode 100644 Macros/Options/Tests/OptionsTests/Mocks/MockError.swift create mode 100644 Macros/Options/codecov.yml create mode 100644 Macros/Options/logo.png create mode 100644 Macros/Options/logo.svg create mode 100644 Macros/Options/project.yml diff --git a/Macros/Options/.github/workflows/Options.yml b/Macros/Options/.github/workflows/Options.yml new file mode 100644 index 0000000..1f13ac5 --- /dev/null +++ b/Macros/Options/.github/workflows/Options.yml @@ -0,0 +1,242 @@ +name: macOS +on: + push: + branches-ignore: + - '*WIP' +env: + PACKAGE_NAME: Options +jobs: + build-ubuntu: + name: Build on Ubuntu + env: + PACKAGE_NAME: Options + SWIFT_VER: ${{ matrix.swift-version }} + runs-on: ${{ matrix.runs-on }} + if: "!contains(github.event.head_commit.message, 'ci skip')" + strategy: + matrix: + runs-on: [ubuntu-20.04, ubuntu-22.04] + swift-version: ["5.7.1", "5.8.1", "5.9", "5.9.2", "5.10"] + steps: + - uses: actions/checkout@v4 + - name: Set Ubuntu Release DOT + run: echo "RELEASE_DOT=$(lsb_release -sr)" >> $GITHUB_ENV + - name: Set Ubuntu Release NUM + run: echo "RELEASE_NUM=${RELEASE_DOT//[-._]/}" >> $GITHUB_ENV + - name: Set Ubuntu Codename + run: echo "RELEASE_NAME=$(lsb_release -sc)" >> $GITHUB_ENV + - name: Cache swift package modules + id: cache-spm-linux + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}-${{ matrix.swift-version }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}-${{ matrix.swift-version }}- + ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}- + - name: Cache swift + id: cache-swift-linux + uses: actions/cache@v4 + env: + cache-name: cache-swift + with: + path: swift-${{ env.SWIFT_VER }}-RELEASE-ubuntu${{ env.RELEASE_DOT }} + key: ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}-${{ env.RELEASE_DOT }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}- + - name: Download Swift + if: steps.cache-swift-linux.outputs.cache-hit != 'true' + run: curl -O https://download.swift.org/swift-${SWIFT_VER}-release/ubuntu${RELEASE_NUM}/swift-${SWIFT_VER}-RELEASE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz + - name: Extract Swift + if: steps.cache-swift-linux.outputs.cache-hit != 'true' + run: tar xzf swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz + - name: Add Path + run: echo "$GITHUB_WORKSPACE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}/usr/bin" >> $GITHUB_PATH + - name: Test + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift-version }},ubuntu-${{ matrix.RELEASE_DOT }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} + build-macos: + name: Build on macOS + runs-on: ${{ matrix.os }} + if: "!contains(github.event.head_commit.message, 'ci skip')" + env: + PACKAGE_NAME: Options + strategy: + matrix: + include: + - xcode: "/Applications/Xcode_14.1.app" + os: macos-12 + iOSVersion: "16.1" + watchOSVersion: "9.0" + watchName: "Apple Watch Series 5 - 40mm" + iPhoneName: "iPhone 12 mini" + - xcode: "/Applications/Xcode_14.2.app" + os: macos-12 + iOSVersion: "16.2" + watchOSVersion: "9.1" + watchName: "Apple Watch Ultra (49mm)" + iPhoneName: "iPhone 14" + - xcode: "/Applications/Xcode_15.0.1.app" + os: macos-13 + iOSVersion: "17.0.1" + watchOSVersion: "10.0" + watchName: "Apple Watch Series 9 (41mm)" + iPhoneName: "iPhone 15" + - xcode: "/Applications/Xcode_15.1.app" + os: macos-13 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Series 9 (45mm)" + iPhoneName: "iPhone 15 Plus" + - xcode: "/Applications/Xcode_15.2.app" + os: macos-14 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Ultra (49mm)" + iPhoneName: "iPhone 15 Pro" + - xcode: "/Applications/Xcode_15.3.app" + os: macos-14 + iOSVersion: "17.4" + watchOSVersion: "10.4" + watchName: "Apple Watch Ultra 2 (49mm)" + iPhoneName: "iPhone 15 Pro Max" + steps: + - uses: actions/checkout@v4 + - name: Cache swift package modules + id: cache-spm-macos + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}- + - name: Cache mint + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + id: cache-mint + uses: actions/cache@v4 + env: + cache-name: cache-mint + with: + path: .mint + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Mintfile') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Set Xcode Name + run: echo "XCODE_NAME=$(basename -- ${{ matrix.xcode }} | sed 's/\.[^.]*$//' | cut -d'_' -f2)" >> $GITHUB_ENV + - name: Setup Xcode + run: sudo xcode-select -s ${{ matrix.xcode }}/Contents/Developer + - name: Install mint + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + run: | + brew update + brew install mint + - name: Build + run: swift build + - name: Run Swift Package tests + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files-spm + with: + fail-on-empty-output: true + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + files: ${{ join(fromJSON(steps.coverage-files-spm.outputs.files), ',') }} + token: ${{ secrets.CODECOV_TOKEN }} + flags: macOS,${{ env.XCODE_NAME }},${{ matrix.runs-on }} + - name: Clean up spm build directory + run: rm -rf .build + - name: Lint + run: ./scripts/lint.sh + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + # - name: Run iOS target tests + # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "iphonesimulator" -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test + # - uses: sersoft-gmbh/swift-coverage-action@v4 + # id: coverage-files-iOS + # with: + # fail-on-empty-output: true + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # fail_ci_if_error: true + # verbose: true + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ${{ join(fromJSON(steps.coverage-files-iOS.outputs.files), ',') }} + # flags: iOS,iOS${{ matrix.iOSVersion }},macOS,${{ env.XCODE_NAME }} + # - name: Run watchOS target tests + # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "watchsimulator" -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test + # - uses: sersoft-gmbh/swift-coverage-action@v4 + # id: coverage-files-watchOS + # with: + # fail-on-empty-output: true + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # fail_ci_if_error: true + # verbose: true + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ${{ join(fromJSON(steps.coverage-files-watchOS.outputs.files), ',') }} + # flags: watchOS,watchOS${{ matrix.watchOSVersion }},macOS,${{ env.XCODE_NAME }} + build-self: + name: Build on Self-Hosting macOS + runs-on: [self-hosted, macOS] + if: github.event.repository.owner.login == github.event.organization.login && !contains(github.event.head_commit.message, 'ci skip') + steps: + - uses: actions/checkout@v4 + - name: Cache swift package modules + id: cache-spm-macos + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Cache mint + id: cache-mint + uses: actions/cache@v4 + env: + cache-name: cache-mint + with: + path: .mint + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Mintfile') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Build + run: swift build + - name: Run Swift Package tests + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + with: + fail-on-empty-output: true + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: macOS,${{ env.XCODE_NAME }} + - name: Clean up spm build directory + run: rm -rf .build + - name: Lint + run: ./scripts/lint.sh diff --git a/Macros/Options/.gitignore b/Macros/Options/.gitignore new file mode 100644 index 0000000..008465e --- /dev/null +++ b/Macros/Options/.gitignore @@ -0,0 +1,133 @@ +# Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,swiftpackagemanager,xcode,macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +*.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +.mint +Output + +# Due to support for 5.10 and below +Package.resolved \ No newline at end of file diff --git a/Macros/Options/.gitrepo b/Macros/Options/.gitrepo new file mode 100644 index 0000000..394e5c2 --- /dev/null +++ b/Macros/Options/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = git@github.com:brightdigit/Options.git + branch = syntaxkit-sample + commit = a2fd9e31d5fdf1a0e9d61fe76ab5a4461d10b08a + parent = 12b377f8e1df18994e1c9693f6c6399e7f9ddeb2 + method = merge + cmdver = 0.4.9 diff --git a/Macros/Options/.hound.yml b/Macros/Options/.hound.yml new file mode 100644 index 0000000..6941f63 --- /dev/null +++ b/Macros/Options/.hound.yml @@ -0,0 +1,2 @@ +swiftlint: + config_file: .swiftlint.yml diff --git a/Macros/Options/.periphery.yml b/Macros/Options/.periphery.yml new file mode 100644 index 0000000..85b884a --- /dev/null +++ b/Macros/Options/.periphery.yml @@ -0,0 +1 @@ +retain_public: true diff --git a/Macros/Options/.spi.yml b/Macros/Options/.spi.yml new file mode 100644 index 0000000..2c312f8 --- /dev/null +++ b/Macros/Options/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [Options] diff --git a/Macros/Options/.swift-version b/Macros/Options/.swift-version new file mode 100644 index 0000000..760606e --- /dev/null +++ b/Macros/Options/.swift-version @@ -0,0 +1 @@ +5.7 diff --git a/Macros/Options/.swiftformat b/Macros/Options/.swiftformat new file mode 100644 index 0000000..c510d49 --- /dev/null +++ b/Macros/Options/.swiftformat @@ -0,0 +1,7 @@ +--indent 2 +--header "\n .*?\.swift\n SimulatorServices\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" +--commas inline +--disable wrapMultilineStatementBraces, redundantInternal +--extensionacl on-declarations +--decimalgrouping 3,4 +--exclude .build, DerivedData, .swiftpm diff --git a/Macros/Options/.swiftlint.yml b/Macros/Options/.swiftlint.yml new file mode 100644 index 0000000..6be46e8 --- /dev/null +++ b/Macros/Options/.swiftlint.yml @@ -0,0 +1,118 @@ +opt_in_rules: + - array_init + - attributes + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + - fallthrough + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool + - trailing_closure + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - explicit_self + - unused_declaration + - unused_import +type_body_length: + - 100 + - 200 +file_length: + - 200 + - 300 +function_body_length: + - 18 + - 40 +function_parameter_count: 8 +line_length: + - 90 + - 90 +identifier_name: + excluded: + - id +excluded: + - Tests + - DerivedData + - .build + - .swiftpm +indentation_width: + indentation_width: 2 diff --git a/Macros/Options/LICENSE b/Macros/Options/LICENSE new file mode 100644 index 0000000..59f9701 --- /dev/null +++ b/Macros/Options/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 Bright Digit, LLC + +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. diff --git a/Macros/Options/Mintfile b/Macros/Options/Mintfile new file mode 100644 index 0000000..c1dc548 --- /dev/null +++ b/Macros/Options/Mintfile @@ -0,0 +1,2 @@ +nicklockwood/SwiftFormat@0.53.5 +realm/SwiftLint@0.54.0 \ No newline at end of file diff --git a/Macros/Options/Package.swift b/Macros/Options/Package.swift new file mode 100644 index 0000000..5646b68 --- /dev/null +++ b/Macros/Options/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.7.1 + +// swiftlint:disable explicit_top_level_acl +// swiftlint:disable prefixed_toplevel_constant +// swiftlint:disable explicit_acl + +import PackageDescription + +let package = Package( + name: "Options", + products: [ + .library( + name: "Options", + targets: ["Options"] + ) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + ], + targets: [ + .target( + name: "Options", + dependencies: [] + ), + .testTarget( + name: "OptionsTests", + dependencies: ["Options"] + ) + ] +) + +// swiftlint:enable explicit_top_level_acl +// swiftlint:enable prefixed_toplevel_constant +// swiftlint:enable explicit_acl diff --git a/Macros/Options/Package@swift-5.10.swift b/Macros/Options/Package@swift-5.10.swift new file mode 100644 index 0000000..da62733 --- /dev/null +++ b/Macros/Options/Package@swift-5.10.swift @@ -0,0 +1,65 @@ +// swift-tools-version: 5.10 + +// swiftlint:disable explicit_top_level_acl +// swiftlint:disable prefixed_toplevel_constant +// swiftlint:disable explicit_acl + +import CompilerPluginSupport +import PackageDescription + +let swiftSettings = [ + SwiftSetting.enableUpcomingFeature("BareSlashRegexLiterals"), + SwiftSetting.enableUpcomingFeature("ConciseMagicFile"), + SwiftSetting.enableUpcomingFeature("ExistentialAny"), + SwiftSetting.enableUpcomingFeature("ForwardTrailingClosures"), + SwiftSetting.enableUpcomingFeature("ImplicitOpenExistentials"), + SwiftSetting.enableUpcomingFeature("StrictConcurrency"), + SwiftSetting.enableUpcomingFeature("DisableOutwardActorInference"), + SwiftSetting.enableExperimentalFeature("StrictConcurrency") +] + +let package = Package( + name: "Options", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + .visionOS(.v1) + ], + products: [ + .library( + name: "Options", + targets: ["Options"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-syntax", from: "510.0.0") + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0") + ], + targets: [ + .target( + name: "Options", + dependencies: ["OptionsMacros"], + swiftSettings: swiftSettings + ), + .macro( + name: "OptionsMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "OptionsTests", + dependencies: ["Options"] + ) + ] +) + +// swiftlint:enable explicit_top_level_acl +// swiftlint:enable prefixed_toplevel_constant +// swiftlint:enable explicit_acl diff --git a/Macros/Options/README.md b/Macros/Options/README.md new file mode 100644 index 0000000..40ed2ed --- /dev/null +++ b/Macros/Options/README.md @@ -0,0 +1,182 @@ + +

+ Options +

+

Options

+ +More powerful options for `Enum` and `OptionSet` types. + +[![SwiftPM](https://img.shields.io/badge/SPM-Linux%20%7C%20iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-success?logo=swift)](https://swift.org) +[![Twitter](https://img.shields.io/badge/twitter-@brightdigit-blue.svg?style=flat)](http://twitter.com/brightdigit) +![GitHub](https://img.shields.io/github/license/brightdigit/Options) +![GitHub issues](https://img.shields.io/github/issues/brightdigit/Options) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/brightdigit/Options/Options.yml?label=actions&logo=github&?branch=main) + +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FOptions%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/brightdigit/Options) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FOptions%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/brightdigit/Options) + +[![Codecov](https://img.shields.io/codecov/c/github/brightdigit/Options)](https://codecov.io/gh/brightdigit/Options) +[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/brightdigit/Options)](https://www.codefactor.io/repository/github/brightdigit/Options) +[![codebeat badge](https://codebeat.co/badges/c47b7e58-867c-410b-80c5-57e10140ba0f)](https://codebeat.co/projects/github-com-brightdigit-mistkit-main) +[![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/brightdigit/Options)](https://codeclimate.com/github/brightdigit/Options) +[![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/brightdigit/Options?label=debt)](https://codeclimate.com/github/brightdigit/Options) +[![Code Climate issues](https://img.shields.io/codeclimate/issues/brightdigit/Options)](https://codeclimate.com/github/brightdigit/Options) +[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) + + +# Table of Contents + + * [Introduction](#introduction) + * [Requirements](#requirements) + * [Installation](#installation) + * [Usage](#usage) + * [Versatile Options with Enums and OptionSets](#versatile-options-with-enums-and-optionsets) + * [Multiple Value Types](#multiple-value-types) + * [Creating an OptionSet](#creating-an-optionset) + * [Further Code Documentation](#further-code-documentation) + * [License](#license) + +# Introduction + +**Options** provides a powerful set of features for `Enum` and `OptionSet` types: + +- Providing additional representations for `Enum` types besides the `RawType rawValue` +- Being able to interchange between `Enum` and `OptionSet` types +- Using an additional value type for a `Codable` `OptionSet` + +# Requirements + +**Apple Platforms** + +- Xcode 14.1 or later +- Swift 5.7.1 or later +- iOS 16 / watchOS 9 / tvOS 16 / macOS 12 or later deployment targets + +**Linux** + +- Ubuntu 20.04 or later +- Swift 5.7.1 or later + +# Installation + +Use the Swift Package Manager to install this library via the repository url: + +``` +https://github.com/brightdigit/Options.git +``` + +Use version up to `1.0`. + +# Usage + +## Versatile Options + +Let's say we are using an `Enum` for a list of popular social media networks: + +```swift +enum SocialNetwork : Int { + case digg + case aim + case bebo + case delicious + case eworld + case googleplus + case itunesping + case jaiku + case miiverse + case musically + case orkut + case posterous + case stumbleupon + case windowslive + case yahoo +} +``` + +We'll be using this as a way to define a particular social handle: + +```swift +struct SocialHandle { + let name : String + let network : SocialNetwork +} +``` + +However we also want to provide a way to have a unique set of social networks available: + +```swift +struct SocialNetworkSet : Int, OptionSet { +... +} + +let user : User +let networks : SocialNetworkSet = user.availableNetworks() +``` + +We can then simply use ``Options()`` macro to generate both these types: + +```swift +@Options +enum SocialNetwork : Int { + case digg + case aim + case bebo + case delicious + case eworld + case googleplus + case itunesping + case jaiku + case miiverse + case musically + case orkut + case posterous + case stumbleupon + case windowslive + case yahoo +} +``` + +Now we can use the newly create `SocialNetworkSet` type to store a set of values: + +```swift +let networks : SocialNetworkSet +networks = [.aim, .delicious, .googleplus, .windowslive] +``` + +## Multiple Value Types + +With the ``Options()`` macro, we add the ability to encode and decode values not only from their raw value but also from a another type such as a string. This is useful for when you want to store the values in JSON format. + +For instance, with a type like `SocialNetwork` we need need to store the value as an Integer: + +```json +5 +``` + +However by adding the ``Options()`` macro we can also decode from a String: + +``` +"googleplus" +``` + +## Creating an OptionSet + +We can also have a new `OptionSet` type created. ``Options()`` create a new `OptionSet` type with the suffix `-Set`. This new `OptionSet` will automatically work with your enum to create a distinct set of values. Additionally it will decode and encode your values as an Array of String. This means the value: + +```swift +[.aim, .delicious, .googleplus, .windowslive] +``` + +is encoded as: + +```json +["aim", "delicious", "googleplus", "windowslive"] +``` + +# Further Code Documentation + +[Documentation Here](https://swiftpackageindex.com/brightdigit/Options/main/documentation/options) + +# License + +This code is distributed under the MIT license. See the [LICENSE](LICENSE) file for more info. diff --git a/Macros/Options/Scripts/docc.sh b/Macros/Options/Scripts/docc.sh new file mode 100755 index 0000000..3e4c918 --- /dev/null +++ b/Macros/Options/Scripts/docc.sh @@ -0,0 +1,3 @@ +#!/bin/sh +xcodebuild docbuild -scheme SimulatorServices -derivedDataPath DerivedData -destination 'platform=macOS' +$(xcrun --find docc) process-archive transform-for-static-hosting DerivedData/Build/Products/Debug/SimulatorServices.doccarchive --output-path Output \ No newline at end of file diff --git a/Macros/Options/Scripts/gh-md-toc b/Macros/Options/Scripts/gh-md-toc new file mode 100755 index 0000000..03b5ddd --- /dev/null +++ b/Macros/Options/Scripts/gh-md-toc @@ -0,0 +1,421 @@ +#!/usr/bin/env bash + +# +# Steps: +# +# 1. Download corresponding html file for some README.md: +# curl -s $1 +# +# 2. Discard rows where no substring 'user-content-' (github's markup): +# awk '/user-content-/ { ... +# +# 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) +# +# 5. Find anchor and insert it inside "(...)": +# substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) +# + +gh_toc_version="0.10.0" + +gh_user_agent="gh-md-toc v$gh_toc_version" + +# +# Download rendered into html README.md by its url. +# +# +gh_toc_load() { + local gh_url=$1 + + if type curl &>/dev/null; then + curl --user-agent "$gh_user_agent" -s "$gh_url" + elif type wget &>/dev/null; then + wget --user-agent="$gh_user_agent" -qO- "$gh_url" + else + echo "Please, install 'curl' or 'wget' and try again." + exit 1 + fi +} + +# +# Converts local md file into html by GitHub +# +# -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown +#

Hello world github/linguist#1 cool, and #1!

'" +gh_toc_md2html() { + local gh_file_md=$1 + local skip_header=$2 + + URL=https://api.github.com/markdown/raw + + if [ -n "$GH_TOC_TOKEN" ]; then + TOKEN=$GH_TOC_TOKEN + else + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + if [ -f "$TOKEN_FILE" ]; then + TOKEN="$(cat "$TOKEN_FILE")" + fi + fi + if [ -n "${TOKEN}" ]; then + AUTHORIZATION="Authorization: token ${TOKEN}" + fi + + local gh_tmp_file_md=$gh_file_md + if [ "$skip_header" = "yes" ]; then + if grep -Fxq "" "$gh_src"; then + # cut everything before the toc + gh_tmp_file_md=$gh_file_md~~ + sed '1,//d' "$gh_file_md" > "$gh_tmp_file_md" + fi + fi + + # echo $URL 1>&2 + OUTPUT=$(curl -s \ + --user-agent "$gh_user_agent" \ + --data-binary @"$gh_tmp_file_md" \ + -H "Content-Type:text/plain" \ + -H "$AUTHORIZATION" \ + "$URL") + + rm -f "${gh_file_md}~~" + + if [ "$?" != "0" ]; then + echo "XXNetworkErrorXX" + fi + if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then + echo "XXRateLimitXX" + else + echo "${OUTPUT}" + fi +} + + +# +# Is passed string url +# +gh_is_url() { + case $1 in + https* | http*) + echo "yes";; + *) + echo "no";; + esac +} + +# +# TOC generator +# +gh_toc(){ + local gh_src=$1 + local gh_src_copy=$1 + local gh_ttl_docs=$2 + local need_replace=$3 + local no_backup=$4 + local no_footer=$5 + local indent=$6 + local skip_header=$7 + + if [ "$gh_src" = "" ]; then + echo "Please, enter URL or local path for a README.md" + exit 1 + fi + + + # Show "TOC" string only if working with one document + if [ "$gh_ttl_docs" = "1" ]; then + + echo "Table of Contents" + echo "=================" + echo "" + gh_src_copy="" + + fi + + if [ "$(gh_is_url "$gh_src")" == "yes" ]; then + gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent" + if [ "${PIPESTATUS[0]}" != "0" ]; then + echo "Could not load remote document." + echo "Please check your url or network connectivity" + exit 1 + fi + if [ "$need_replace" = "yes" ]; then + echo + echo "!! '$gh_src' is not a local file" + echo "!! Can't insert the TOC into it." + echo + fi + else + local rawhtml + rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header") + if [ "$rawhtml" == "XXNetworkErrorXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Please make sure curl is installed and check your network connectivity" + exit 1 + fi + if [ "$rawhtml" == "XXRateLimitXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting" + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + echo "or place GitHub auth token here: ${TOKEN_FILE}" + exit 1 + fi + local toc + toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"` + echo "$toc" + if [ "$need_replace" = "yes" ]; then + if grep -Fxq "" "$gh_src" && grep -Fxq "" "$gh_src"; then + echo "Found markers" + else + echo "You don't have or in your file...exiting" + exit 1 + fi + local ts="<\!--ts-->" + local te="<\!--te-->" + local dt + dt=$(date +'%F_%H%M%S') + local ext=".orig.${dt}" + local toc_path="${gh_src}.toc.${dt}" + local toc_createdby="" + local toc_footer + toc_footer="" + # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html + # clear old TOC + sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src" + # create toc file + echo "${toc}" > "${toc_path}" + if [ "${no_footer}" != "yes" ]; then + echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path" + fi + + # insert toc file + if ! sed --version > /dev/null 2>&1; then + sed -i "" "/${ts}/r ${toc_path}" "$gh_src" + else + sed -i "/${ts}/r ${toc_path}" "$gh_src" + fi + echo + if [ "${no_backup}" = "yes" ]; then + rm "$toc_path" "$gh_src$ext" + fi + echo "!! TOC was added into: '$gh_src'" + if [ -z "${no_backup}" ]; then + echo "!! Origin version of the file: '${gh_src}${ext}'" + echo "!! TOC added into a separate file: '${toc_path}'" + fi + echo + fi + fi +} + +# +# Grabber of the TOC from rendered html +# +# $1 - a source url of document. +# It's need if TOC is generated for multiple documents. +# $2 - number of spaces used to indent. +# +gh_toc_grab() { + + href_regex="/href=\"[^\"]+?\"/" + common_awk_script=' + modified_href = "" + split(href, chars, "") + for (i=1;i <= length(href); i++) { + c = chars[i] + res = "" + if (c == "+") { + res = " " + } else { + if (c == "%") { + res = "\\x" + } else { + res = c "" + } + } + modified_href = modified_href res + } + print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")" + ' + if [ "`uname -s`" == "OS/390" ]; then + grepcmd="pcregrep -o" + echoargs="" + awkscript='{ + level = substr($0, 3, 1) + text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) + '"$common_awk_script"' + }' + else + grepcmd="grep -Eo" + echoargs="-e" + awkscript='{ + level = substr($0, 3, 1) + text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) + '"$common_awk_script"' + }' + fi + + # if closed is on the new line, then move it on the prev line + # for example: + # was: The command foo1 + # + # became: The command foo1 + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | + + # Sometimes a line can start with . Fix that. + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' | sed 's/<\/code>//g' | + + # remove g-emoji + sed 's/]*[^<]*<\/g-emoji> //g' | + + # now all rows are like: + #

title

.. + # format result line + # * $0 - whole string + # * last element of each row: "/dev/null; then + $tool --version | head -n 1 + else + echo "not installed" + fi + done +} + +show_help() { + local app_name + app_name=$(basename "$0") + echo "GitHub TOC generator ($app_name): $gh_toc_version" + echo "" + echo "Usage:" + echo " $app_name [options] src [src] Create TOC for a README file (url or local path)" + echo " $app_name - Create TOC for markdown from STDIN" + echo " $app_name --help Show help" + echo " $app_name --version Show version" + echo "" + echo "Options:" + echo " --indent Set indent size. Default: 3." + echo " --insert Insert new TOC into original file. For local files only. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details." + echo " --no-backup Remove backup file. Set --insert as well. Default: false." + echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false." + echo " --skip-header Hide entry of the topmost headlines. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details." + echo "" +} + +# +# Options handlers +# +gh_toc_app() { + local need_replace="no" + local indent=3 + + if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then + show_help + return + fi + + if [ "$1" = '--version' ]; then + show_version + return + fi + + if [ "$1" = '--indent' ]; then + indent="$2" + shift 2 + fi + + if [ "$1" = "-" ]; then + if [ -z "$TMPDIR" ]; then + TMPDIR="/tmp" + elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then + mkdir -p "$TMPDIR" + fi + local gh_tmp_md + if [ "`uname -s`" == "OS/390" ]; then + local timestamp + timestamp=$(date +%m%d%Y%H%M%S) + gh_tmp_md="$TMPDIR/tmp.$timestamp" + else + gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX") + fi + while read -r input; do + echo "$input" >> "$gh_tmp_md" + done + gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent" + return + fi + + if [ "$1" = '--insert' ]; then + need_replace="yes" + shift + fi + + if [ "$1" = '--no-backup' ]; then + need_replace="yes" + no_backup="yes" + shift + fi + + if [ "$1" = '--hide-footer' ]; then + need_replace="yes" + no_footer="yes" + shift + fi + + if [ "$1" = '--skip-header' ]; then + skip_header="yes" + shift + fi + + + for md in "$@" + do + echo "" + gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header" + done + + echo "" + echo "" +} + +# +# Entry point +# +gh_toc_app "$@" \ No newline at end of file diff --git a/Macros/Options/Scripts/lint.sh b/Macros/Options/Scripts/lint.sh new file mode 100755 index 0000000..31c3fa9 --- /dev/null +++ b/Macros/Options/Scripts/lint.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +if [ -z "$GITHUB_ACTION" ]; then + MINT_CMD="/opt/homebrew/bin/mint" +else + MINT_CMD="mint" +fi + +export MINT_PATH="$PACKAGE_DIR/.mint" +MINT_ARGS="-n -m $PACKAGE_DIR/Mintfile --silent" +MINT_RUN="$MINT_CMD run $MINT_ARGS" + +pushd $PACKAGE_DIR + +$MINT_CMD bootstrap -m Mintfile + +if [ "$LINT_MODE" == "NONE" ]; then + exit +elif [ "$LINT_MODE" == "STRICT" ]; then + SWIFTFORMAT_OPTIONS="" + SWIFTLINT_OPTIONS="--strict" +else + SWIFTFORMAT_OPTIONS="" + SWIFTLINT_OPTIONS="" +fi + +pushd $PACKAGE_DIR + +if [ -z "$CI" ]; then + $MINT_RUN swiftformat . + $MINT_RUN swiftlint --fix +fi + +$MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . +$MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS + +popd diff --git a/Macros/Options/Sources/Options/Array.swift b/Macros/Options/Sources/Options/Array.swift new file mode 100644 index 0000000..0c4db74 --- /dev/null +++ b/Macros/Options/Sources/Options/Array.swift @@ -0,0 +1,58 @@ +// +// Array.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +// swiftlint:disable:next line_length +@available(*, deprecated, renamed: "MappedValueGenericRepresented", message: "Use MappedValueGenericRepresented instead.") +public protocol MappedValueCollectionRepresented: MappedValueRepresented + where MappedValueType: Sequence {} + +extension Array: MappedValues where Element: Equatable {} + +extension Collection where Element: Equatable, Self: MappedValues { + /// Get the index based on the value passed. + /// - Parameter value: Value to search. + /// - Returns: Index found. + public func key(value: Element) throws -> Self.Index { + guard let index = firstIndex(of: value) else { + throw MappedValueRepresentableError.valueNotFound + } + + return index + } + + /// Gets the value based on the index. + /// - Parameter key: The index. + /// - Returns: The value at index. + public func value(key: Self.Index) throws -> Element { + guard key < endIndex, key >= startIndex else { + throw MappedValueRepresentableError.valueNotFound + } + return self[key] + } +} diff --git a/Macros/Options/Sources/Options/CodingOptions.swift b/Macros/Options/Sources/Options/CodingOptions.swift new file mode 100644 index 0000000..e26ad68 --- /dev/null +++ b/Macros/Options/Sources/Options/CodingOptions.swift @@ -0,0 +1,49 @@ +// +// CodingOptions.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +/// Options for how a ``MappedValueRepresentable`` type is encoding and decoded. +public struct CodingOptions: OptionSet, Sendable { + /// Allow decoding from String + public static let allowMappedValueDecoding: CodingOptions = .init(rawValue: 1) + + /// Encode the value as a String. + public static let encodeAsMappedValue: CodingOptions = .init(rawValue: 2) + + /// Default options. + public static let `default`: CodingOptions = + [.allowMappedValueDecoding, encodeAsMappedValue] + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/Macros/Options/Sources/Options/Dictionary.swift b/Macros/Options/Sources/Options/Dictionary.swift new file mode 100644 index 0000000..a7a3b31 --- /dev/null +++ b/Macros/Options/Sources/Options/Dictionary.swift @@ -0,0 +1,51 @@ +// +// Dictionary.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +// swiftlint:disable:next line_length +@available(*, deprecated, renamed: "MappedValueGenericRepresented", message: "Use MappedValueGenericRepresented instead.") +public protocol MappedValueDictionaryRepresented: MappedValueRepresented + where MappedValueType == [Int: MappedType] {} + +extension Dictionary: MappedValues where Value: Equatable { + public func key(value: Value) throws -> Key { + let pair = first { $0.value == value } + guard let key = pair?.key else { + throw MappedValueRepresentableError.valueNotFound + } + + return key + } + + public func value(key: Key) throws -> Value { + guard let value = self[key] else { + throw MappedValueRepresentableError.valueNotFound + } + return value + } +} diff --git a/Macros/Options/Sources/Options/Documentation.docc/Documentation.md b/Macros/Options/Sources/Options/Documentation.docc/Documentation.md new file mode 100644 index 0000000..994798d --- /dev/null +++ b/Macros/Options/Sources/Options/Documentation.docc/Documentation.md @@ -0,0 +1,162 @@ +# ``Options`` + +More powerful options for `Enum` and `OptionSet` types. + +## Overview + +**Options** provides a powerful set of features for `Enum` and `OptionSet` types: + +- Providing additional representations for `Enum` types besides the `RawType rawValue` +- Being able to interchange between `Enum` and `OptionSet` types +- Using an additional value type for a `Codable` `OptionSet` + +### Requirements + +**Apple Platforms** + +- Xcode 14.1 or later +- Swift 5.7.1 or later +- iOS 16 / watchOS 9 / tvOS 16 / macOS 12 or later deployment targets + +**Linux** + +- Ubuntu 20.04 or later +- Swift 5.7.1 or later + +### Installation + +Use the Swift Package Manager to install this library via the repository url: + +``` +https://github.com/brightdigit/Options.git +``` + +Use version up to `1.0`. + +### Versatile Options + +Let's say we are using an `Enum` for a list of popular social media networks: + +```swift +enum SocialNetwork : Int { + case digg + case aim + case bebo + case delicious + case eworld + case googleplus + case itunesping + case jaiku + case miiverse + case musically + case orkut + case posterous + case stumbleupon + case windowslive + case yahoo +} +``` + +We'll be using this as a way to define a particular social handle: + +```swift +struct SocialHandle { + let name : String + let network : SocialNetwork +} +``` + +However we also want to provide a way to have a unique set of social networks available: + +```swift +struct SocialNetworkSet : Int, OptionSet { +... +} + +let user : User +let networks : SocialNetworkSet = user.availableNetworks() +``` + +We can then simply use ``Options()`` macro to generate both these types: + +```swift +@Options +enum SocialNetwork : Int { + case digg + case aim + case bebo + case delicious + case eworld + case googleplus + case itunesping + case jaiku + case miiverse + case musically + case orkut + case posterous + case stumbleupon + case windowslive + case yahoo +} +``` + +Now we can use the newly create `SocialNetworkSet` type to store a set of values: + +```swift +let networks : SocialNetworkSet +networks = [.aim, .delicious, .googleplus, .windowslive] +``` + +### Multiple Value Types + +With the ``Options()`` macro, we add the ability to encode and decode values not only from their raw value but also from a another type such as a string. This is useful for when you want to store the values in JSON format. + +For instance, with a type like `SocialNetwork` we need need to store the value as an Integer: + +```json +5 +``` + +However by adding the ``Options()`` macro we can also decode from a String: + +``` +"googleplus" +``` + +### Creating an OptionSet + +We can also have a new `OptionSet` type created. ``Options()`` create a new `OptionSet` type with the suffix `-Set`. This new `OptionSet` will automatically work with your enum to create a distinct set of values. Additionally it will decode and encode your values as an Array of String. This means the value: + +```swift +[.aim, .delicious, .googleplus, .windowslive] +``` + +is encoded as: + +```json +["aim", "delicious", "googleplus", "windowslive"] +``` + +## Topics + +### Options conformance + +- ``Options()`` +- ``MappedValueRepresentable`` +- ``MappedValueRepresented`` +- ``EnumSet`` + +### Advanced customization + +- ``CodingOptions`` +- ``MappedValues`` + +### Errors + +- ``MappedValueRepresentableError-2k4ki`` + +### Deprecated + +- ``MappedValueCollectionRepresented`` +- ``MappedValueDictionaryRepresented`` +- ``MappedEnum`` diff --git a/Macros/Options/Sources/Options/EnumSet.swift b/Macros/Options/Sources/Options/EnumSet.swift new file mode 100644 index 0000000..c2e447e --- /dev/null +++ b/Macros/Options/Sources/Options/EnumSet.swift @@ -0,0 +1,139 @@ +/// Generic struct for using Enums with `RawValue`. +/// +/// If you have an `enum` such as: +/// ```swift +/// @Options +/// enum SocialNetwork : Int { +/// case digg +/// case aim +/// case bebo +/// case delicious +/// case eworld +/// case googleplus +/// case itunesping +/// case jaiku +/// case miiverse +/// case musically +/// case orkut +/// case posterous +/// case stumbleupon +/// case windowslive +/// case yahoo +/// } +/// ``` +/// An ``EnumSet`` could be used to store multiple values as an `OptionSet`: +/// ```swift +/// let socialNetworks : EnumSet = +/// [.digg, .aim, .yahoo, .miiverse] +/// ``` +public struct EnumSet: + OptionSet, Sendable, ExpressibleByArrayLiteral + where EnumType.RawValue: FixedWidthInteger & Sendable { + public typealias RawValue = EnumType.RawValue + + /// Raw Value of the OptionSet + public let rawValue: RawValue + + /// Creates the EnumSet based on the `rawValue` + /// - Parameter rawValue: Integer raw value of the OptionSet + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public init(arrayLiteral elements: EnumType...) { + self.init(values: elements) + } + + /// Creates the EnumSet based on the values in the array. + /// - Parameter values: Array of enum values. + public init(values: [EnumType]) { + let set = Set(values.map(\.rawValue)) + rawValue = Self.cumulativeValue(basedOnRawValues: set) + } + + internal static func cumulativeValue( + basedOnRawValues rawValues: Set) -> RawValue { + rawValues.map { 1 << $0 }.reduce(0, |) + } +} + +extension FixedWidthInteger { + fileprivate static var one: Self { + 1 + } +} + +extension EnumSet where EnumType: CaseIterable { + internal static func enums(basedOnRawValue rawValue: RawValue) -> [EnumType] { + let cases = EnumType.allCases.sorted { $0.rawValue < $1.rawValue } + var values = [EnumType]() + var current = rawValue + for item in cases { + guard current > 0 else { + break + } + let rawValue = RawValue.one << item.rawValue + if current & rawValue != .zero { + values.append(item) + current -= rawValue + } + } + return values + } + + /// Returns an array of the enum values based on the OptionSet + /// - Returns: Array for each value represented by the enum. + public func array() -> [EnumType] { + Self.enums(basedOnRawValue: rawValue) + } +} + +#if swift(>=5.9) + extension EnumSet: Codable + where EnumType: MappedValueRepresentable, EnumType.MappedType: Codable { + /// Decodes the EnumSet based on an Array of MappedTypes. + /// - Parameter decoder: Decoder which contains info as an array of MappedTypes. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let values = try container.decode([EnumType.MappedType].self) + let rawValues = try values.map(EnumType.rawValue(basedOn:)) + let set = Set(rawValues) + rawValue = Self.cumulativeValue(basedOnRawValues: set) + } + + /// Encodes the EnumSet based on an Array of MappedTypes. + /// - Parameter encoder: Encoder which will contain info as an array of MappedTypes. + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + let values = Self.enums(basedOnRawValue: rawValue) + let mappedValues = try values + .map(\.rawValue) + .map(EnumType.mappedValue(basedOn:)) + try container.encode(mappedValues) + } + } +#else + extension EnumSet: Codable + where EnumType: MappedValueRepresentable, EnumType.MappedType: Codable { + /// Decodes the EnumSet based on an Array of MappedTypes. + /// - Parameter decoder: Decoder which contains info as an array of MappedTypes. + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let values = try container.decode([EnumType.MappedType].self) + let rawValues = try values.map(EnumType.rawValue(basedOn:)) + let set = Set(rawValues) + rawValue = Self.cumulativeValue(basedOnRawValues: set) + } + + /// Encodes the EnumSet based on an Array of MappedTypes. + /// - Parameter encoder: Encoder which will contain info as an array of MappedTypes. + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + let values = Self.enums(basedOnRawValue: rawValue) + let mappedValues = try values + .map(\.rawValue) + .map(EnumType.mappedValue(basedOn:)) + try container.encode(mappedValues) + } + } +#endif diff --git a/Macros/Options/Sources/Options/Macro.swift b/Macros/Options/Sources/Options/Macro.swift new file mode 100644 index 0000000..ab7ff7b --- /dev/null +++ b/Macros/Options/Sources/Options/Macro.swift @@ -0,0 +1,42 @@ +// +// Macro.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +#if swift(>=5.10) + /// Sets an enumeration up to implement + /// ``MappedValueRepresentable`` and ``MappedValueRepresented``. + @attached( + extension, + conformances: MappedValueRepresentable, MappedValueRepresented, + names: named(MappedType), named(mappedValues) + ) + @attached(peer, names: suffixed(Set)) + public macro Options() = #externalMacro(module: "OptionsMacros", type: "OptionsMacro") +#endif diff --git a/Macros/Options/Sources/Options/MappedEnum.swift b/Macros/Options/Sources/Options/MappedEnum.swift new file mode 100644 index 0000000..9298ada --- /dev/null +++ b/Macros/Options/Sources/Options/MappedEnum.swift @@ -0,0 +1,66 @@ +/// A generic struct for enumerations which allow for additional values attached. +@available( + *, + deprecated, + renamed: "MappedValueRepresentable", + message: "Use `MappedValueRepresentable` with `CodingOptions`." +) +public struct MappedEnum: Codable, Sendable + where EnumType.MappedType: Codable { + /// Base Enumeraion value. + public let value: EnumType + + /// Creates an instance based on the base enumeration value. + /// - Parameter value: Base Enumeration value. + public init(value: EnumType) { + self.value = value + } +} + +#if swift(>=5.9) + extension MappedEnum { + /// Decodes the value based on the mapped value. + /// - Parameter decoder: Decoder. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let label = try container.decode(EnumType.MappedType.self) + let rawValue = try EnumType.rawValue(basedOn: label) + guard let value = EnumType(rawValue: rawValue) else { + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) + } + self.value = value + } + + /// Encodes the value based on the mapped value. + /// - Parameter encoder: Encoder. + public func encode(to encoder: any Encoder) throws { + let string = try EnumType.mappedValue(basedOn: value.rawValue) + var container = encoder.singleValueContainer() + try container.encode(string) + } + } +#else + extension MappedEnum { + /// Decodes the value based on the mapped value. + /// - Parameter decoder: Decoder. + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let label = try container.decode(EnumType.MappedType.self) + let rawValue = try EnumType.rawValue(basedOn: label) + guard let value = EnumType(rawValue: rawValue) else { + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) + } + self.value = value + } + + /// Encodes the value based on the mapped value. + /// - Parameter encoder: Encoder. + public func encode(to encoder: Encoder) throws { + let string = try EnumType.mappedValue(basedOn: value.rawValue) + var container = encoder.singleValueContainer() + try container.encode(string) + } + } +#endif diff --git a/Macros/Options/Sources/Options/MappedValueRepresentable+Codable.swift b/Macros/Options/Sources/Options/MappedValueRepresentable+Codable.swift new file mode 100644 index 0000000..550f7c6 --- /dev/null +++ b/Macros/Options/Sources/Options/MappedValueRepresentable+Codable.swift @@ -0,0 +1,97 @@ +// +// MappedValueRepresentable+Codable.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +extension DecodingError { + internal static func invalidRawValue(_ rawValue: some Any) -> DecodingError { + .dataCorrupted( + .init(codingPath: [], debugDescription: "Raw Value \(rawValue) is invalid.") + ) + } +} + +extension SingleValueDecodingContainer { + fileprivate func decodeAsRawValue() throws -> T + where T.RawValue: Decodable { + let rawValue = try decode(T.RawValue.self) + guard let value = T(rawValue: rawValue) else { + throw DecodingError.invalidRawValue(rawValue) + } + return value + } + + fileprivate func decodeAsMappedType() throws -> T + where T.RawValue: Decodable, T.MappedType: Decodable { + let mappedValues: T.MappedType + do { + mappedValues = try decode(T.MappedType.self) + } catch { + return try decodeAsRawValue() + } + + let rawValue = try T.rawValue(basedOn: mappedValues) + + guard let value = T(rawValue: rawValue) else { + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) + } + + return value + } +} + +extension MappedValueRepresentable + where Self: Decodable, MappedType: Decodable, RawValue: Decodable { + /// Decodes the type. + /// - Parameter decoder: Decoder. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + if Self.codingOptions.contains(.allowMappedValueDecoding) { + self = try container.decodeAsMappedType() + } else { + self = try container.decodeAsRawValue() + } + } +} + +extension MappedValueRepresentable + where Self: Encodable, MappedType: Encodable, RawValue: Encodable { + /// Encoding the type. + /// - Parameter decoder: Encodes. + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + if Self.codingOptions.contains(.encodeAsMappedValue) { + try container.encode(mappedValue()) + } else { + try container.encode(rawValue) + } + } +} diff --git a/Macros/Options/Sources/Options/MappedValueRepresentable.swift b/Macros/Options/Sources/Options/MappedValueRepresentable.swift new file mode 100644 index 0000000..896f4a9 --- /dev/null +++ b/Macros/Options/Sources/Options/MappedValueRepresentable.swift @@ -0,0 +1,68 @@ +// +// MappedValueRepresentable.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +/// An enum which has an additional value attached. +/// - Note: ``Options()`` macro will automatically set this up for you. +public protocol MappedValueRepresentable: RawRepresentable, CaseIterable, Sendable { + /// The additional value type. + associatedtype MappedType = String + + /// Options for how the enum should be decoded or encoded. + static var codingOptions: CodingOptions { + get + } + + /// Gets the raw value based on the MappedType. + /// - Parameter value: MappedType value. + /// - Returns: The raw value of the enumeration based on the `MappedType `value. + static func rawValue(basedOn string: MappedType) throws -> RawValue + + /// Gets the `MappedType` value based on the `rawValue`. + /// - Parameter rawValue: The raw value of the enumeration. + /// - Returns: The Mapped Type value based on the `rawValue`. + static func mappedValue(basedOn rawValue: RawValue) throws -> MappedType +} + +extension MappedValueRepresentable { + /// Options regarding how the type can be decoded or encoded. + public static var codingOptions: CodingOptions { + .default + } + + /// Gets the mapped value of the enumeration. + /// - Parameter rawValue: The raw value of the enumeration + /// which pretains to its index in the `mappedValues` Array. + /// - Throws: `MappedValueCollectionRepresentedError.valueNotFound` + /// if the raw value (i.e. index) is outside the range of the `mappedValues` array. + /// - Returns: + /// The Mapped Type value based on the value in the array at the raw value index. + public func mappedValue() throws -> MappedType { + try Self.mappedValue(basedOn: rawValue) + } +} diff --git a/Macros/Options/Sources/Options/MappedValueRepresentableError.swift b/Macros/Options/Sources/Options/MappedValueRepresentableError.swift new file mode 100644 index 0000000..d303174 --- /dev/null +++ b/Macros/Options/Sources/Options/MappedValueRepresentableError.swift @@ -0,0 +1,47 @@ +// +// MappedValueRepresentableError.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +// swiftlint:disable file_types_order +#if swift(>=5.10) + /// An Error thrown when the `MappedType` value or `RawType` value + /// are invalid for an `Enum`. + public enum MappedValueRepresentableError: Error, Sendable { + /// Whenever a value or key cannot be found. + case valueNotFound + } +#else + /// An Error thrown when the `MappedType` value or `RawType` value + /// are invalid for an `Enum`. + public enum MappedValueRepresentableError: Error { + case valueNotFound + } +#endif +// swiftlint:enable file_types_order diff --git a/Macros/Options/Sources/Options/MappedValueRepresented.swift b/Macros/Options/Sources/Options/MappedValueRepresented.swift new file mode 100644 index 0000000..26ef4e5 --- /dev/null +++ b/Macros/Options/Sources/Options/MappedValueRepresented.swift @@ -0,0 +1,62 @@ +// +// MappedValueRepresented.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +/// Protocol which simplifies ``MappedValueRepresentable``by using a ``MappedValues``. +public protocol MappedValueRepresented: MappedValueRepresentable + where MappedType: Equatable { + /// A object to lookup values and keys for mapped values. + associatedtype MappedValueType: MappedValues + /// An array of the mapped values which lines up with each case. + static var mappedValues: MappedValueType { get } +} + +extension MappedValueRepresented { + /// Gets the raw value based on the MappedType by finding the index of the mapped value. + /// - Parameter value: MappedType value. + /// - Throws: `MappedValueCollectionRepresentedError.valueNotFound` + /// If the value was not found in the array + /// - Returns: + /// The raw value of the enumeration + /// based on the index the MappedType value was found at. + public static func rawValue(basedOn value: MappedType) throws -> RawValue { + try mappedValues.key(value: value) + } + + /// Gets the mapped value based on the rawValue + /// by access the array at the raw value subscript. + /// - Parameter rawValue: The raw value of the enumeration + /// which pretains to its index in the `mappedValues` Array. + /// - Throws: `MappedValueCollectionRepresentedError.valueNotFound` + /// if the raw value (i.e. index) is outside the range of the `mappedValues` array. + /// - Returns: + /// The Mapped Type value based on the value in the array at the raw value index. + public static func mappedValue(basedOn rawValue: RawValue) throws -> MappedType { + try mappedValues.value(key: rawValue) + } +} diff --git a/Macros/Options/Sources/Options/MappedValues.swift b/Macros/Options/Sources/Options/MappedValues.swift new file mode 100644 index 0000000..67d0a45 --- /dev/null +++ b/Macros/Options/Sources/Options/MappedValues.swift @@ -0,0 +1,42 @@ +// +// MappedValues.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +/// Protocol which provides a method for ``MappedValueRepresented`` to pull values. +public protocol MappedValues { + /// Raw Value Type + associatedtype Value: Equatable + /// Key Value Type + associatedtype Key: Equatable + /// get the key vased on the value. + func key(value: Value) throws -> Key + /// get the value based on the key/index. + func value(key: Key) throws -> Value +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/Array.swift b/Macros/Options/Sources/OptionsMacros/Extensions/Array.swift new file mode 100644 index 0000000..78275fe --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/Array.swift @@ -0,0 +1,42 @@ +// +// Array.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +extension Array { + internal init?(keyValues: KeyValues) where Element == String { + self.init() + for key in 0 ..< keyValues.count { + guard let value = keyValues.get(key) else { + return nil + } + append(value) + } + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift new file mode 100644 index 0000000..2cab374 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift @@ -0,0 +1,43 @@ +// +// ArrayExprSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension ArrayExprSyntax { + internal init( + from items: some Collection, + _ closure: @escaping @Sendable (T) -> some ExprSyntaxProtocol + ) { + let values = items.map(closure).map { ArrayElementSyntax(expression: $0) } + let arrayElement = ArrayElementListSyntax { + .init(values) + } + self.init(elements: arrayElement) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift new file mode 100644 index 0000000..c155633 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift @@ -0,0 +1,42 @@ +// +// DeclModifierListSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension DeclModifierListSyntax { + internal init(keywordModifier: Keyword?) { + if let keywordModifier { + self.init { + DeclModifierSyntax(name: .keyword(keywordModifier)) + } + } else { + self.init([]) + } + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift new file mode 100644 index 0000000..f4981a4 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift @@ -0,0 +1,39 @@ +// +// DeclModifierSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension DeclModifierSyntax { + internal var isNeededAccessLevelModifier: Bool { + switch name.tokenKind { + case .keyword(.public): return true + default: return false + } + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift new file mode 100644 index 0000000..b719a24 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift @@ -0,0 +1,47 @@ +// +// DictionaryElementSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension DictionaryElementSyntax { + internal init(pair: (key: Int, value: String)) { + self.init(key: pair.key, value: pair.value) + } + + internal init(key: Int, value: String) { + self.init( + key: IntegerLiteralExprSyntax(integerLiteral: key), + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: .init([.stringSegment(.init(content: .stringSegment(value)))]), + closingQuote: .stringQuoteToken() + ) + ) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift new file mode 100644 index 0000000..d21549b --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift @@ -0,0 +1,41 @@ +// +// DictionaryExprSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension DictionaryExprSyntax { + internal init(keyValues: KeyValues) { + let dictionaryElements = keyValues.dictionary.map(DictionaryElementSyntax.init(pair:)) + + let list = DictionaryElementListSyntax { + .init(dictionaryElements) + } + self.init(content: .elements(list)) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift new file mode 100644 index 0000000..07035df --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift @@ -0,0 +1,42 @@ +// +// EnumDeclSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension EnumDeclSyntax { + internal var caseElements: [EnumCaseElementSyntax] { + memberBlock.members.flatMap { member in + guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { + return [EnumCaseElementSyntax]() + } + + return Array(caseDecl.elements) + } + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift new file mode 100644 index 0000000..cf1eb6e --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift @@ -0,0 +1,57 @@ +// +// ExtensionDeclSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension ExtensionDeclSyntax { + internal init( + enumDecl: EnumDeclSyntax, + conformingTo protocols: [SwiftSyntax.TypeSyntax] + ) throws { + let typeName = enumDecl.name + + let access = enumDecl.modifiers.first(where: \.isNeededAccessLevelModifier) + + let mappedValues = try VariableDeclSyntax.mappedValuesDeclarationForCases( + enumDecl.caseElements + ) + + self.init( + modifiers: DeclModifierListSyntax([access].compactMap { $0 }), + extendedType: IdentifierTypeSyntax(name: typeName), + inheritanceClause: InheritanceClauseSyntax(protocols: protocols), + memberBlock: MemberBlockSyntax( + members: MemberBlockItemListSyntax { + TypeAliasDeclSyntax(name: "MappedType", for: "String") + mappedValues + } + ) + ) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift new file mode 100644 index 0000000..be7dc57 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift @@ -0,0 +1,44 @@ +// +// InheritanceClauseSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension InheritanceClauseSyntax { + internal init(protocols: [SwiftSyntax.TypeSyntax]) { + self.init( + inheritedTypes: .init { + .init( + protocols.map { typeSyntax in + InheritedTypeSyntax(type: typeSyntax) + } + ) + } + ) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/KeyValues.swift b/Macros/Options/Sources/OptionsMacros/Extensions/KeyValues.swift new file mode 100644 index 0000000..7800a73 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/KeyValues.swift @@ -0,0 +1,70 @@ +// +// KeyValues.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +internal struct KeyValues { + internal private(set) var lastKey: Int? + internal private(set) var dictionary = [Int: String]() + + internal var count: Int { + dictionary.count + } + + internal var nextKey: Int { + (lastKey ?? -1) + 1 + } + + internal mutating func append(value: String, withKey key: Int? = nil) throws { + let key = key ?? nextKey + guard dictionary[key] == nil else { + throw InvalidDeclError.rawValue(key) + } + lastKey = key + dictionary[key] = value + } + + internal func get(_ key: Int) -> String? { + dictionary[key] + } +} + +extension KeyValues { + internal init(caseElements: [EnumCaseElementSyntax]) throws { + self.init() + for caseElement in caseElements { + let intText = caseElement.rawValue?.value + .as(IntegerLiteralExprSyntax.self)?.literal.text + let key = intText.flatMap { Int($0) } + let value = + caseElement.name.trimmed.text + try append(value: value, withKey: key) + } + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift new file mode 100644 index 0000000..b682f69 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift @@ -0,0 +1,39 @@ +// +// TypeAliasDeclSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension TypeAliasDeclSyntax { + internal init(name: TokenSyntax, for initializerTypeName: TokenSyntax) { + self.init( + name: name, + initializer: .init(value: IdentifierTypeSyntax(name: initializerTypeName)) + ) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift new file mode 100644 index 0000000..48dd371 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift @@ -0,0 +1,81 @@ +// +// VariableDeclSyntax.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +extension VariableDeclSyntax { + internal init( + keywordModifier: Keyword?, + bindingKeyword: Keyword, + variableName: String, + initializerExpression: (some ExprSyntaxProtocol)? + ) { + let modifiers = DeclModifierListSyntax(keywordModifier: keywordModifier) + + let initializer: InitializerClauseSyntax? = + initializerExpression.map { .init(value: $0) } + + self.init( + modifiers: modifiers, + bindingSpecifier: .keyword(bindingKeyword), + bindings: .init { + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: .identifier(variableName)), + initializer: initializer + ) + } + ) + } + + internal static func initializerExpression( + from caseElements: [EnumCaseElementSyntax] + ) throws -> any ExprSyntaxProtocol { + let keyValues = try KeyValues(caseElements: caseElements) + if let array = Array(keyValues: keyValues) { + return ArrayExprSyntax(from: array) { value in + StringLiteralExprSyntax(content: value) + } + } else { + return DictionaryExprSyntax(keyValues: keyValues) + } + } + + internal static func mappedValuesDeclarationForCases( + _ caseElements: [EnumCaseElementSyntax] + ) throws -> VariableDeclSyntax { + let arrayExpression = try Self.initializerExpression(from: caseElements) + + return VariableDeclSyntax( + keywordModifier: .static, + bindingKeyword: .let, + variableName: "mappedValues", + initializerExpression: arrayExpression + ) + } +} diff --git a/Macros/Options/Sources/OptionsMacros/InvalidDeclError.swift b/Macros/Options/Sources/OptionsMacros/InvalidDeclError.swift new file mode 100644 index 0000000..ea31645 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/InvalidDeclError.swift @@ -0,0 +1,35 @@ +// +// InvalidDeclError.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@preconcurrency import SwiftSyntax + +internal enum InvalidDeclError: Error, Sendable { + case kind(SyntaxKind) + case rawValue(Int) +} diff --git a/Macros/Options/Sources/OptionsMacros/MacrosPlugin.swift b/Macros/Options/Sources/OptionsMacros/MacrosPlugin.swift new file mode 100644 index 0000000..1bc8833 --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/MacrosPlugin.swift @@ -0,0 +1,39 @@ +// +// MacrosPlugin.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxMacros + +@main +internal struct MacrosPlugin: CompilerPlugin { + internal let providingMacros: [any Macro.Type] = [ + OptionsMacro.self + ] +} diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift new file mode 100644 index 0000000..8f93f5d --- /dev/null +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -0,0 +1,69 @@ +// +// OptionsMacro.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import SwiftSyntax +import SwiftSyntaxMacros + +public struct OptionsMacro: ExtensionMacro, PeerMacro { + public static func expansion( + of _: SwiftSyntax.AttributeSyntax, + providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in _: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw InvalidDeclError.kind(declaration.kind) + } + let typeName = enumDecl.name + + let aliasName: TokenSyntax = "\(typeName.trimmed)Set" + + let initializerName: TokenSyntax = "EnumSet<\(typeName)>" + + return [ + .init(TypeAliasDeclSyntax(name: aliasName, for: initializerName)) + ] + } + + public static func expansion( + of _: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingExtensionsOf _: some SwiftSyntax.TypeSyntaxProtocol, + conformingTo protocols: [SwiftSyntax.TypeSyntax], + in _: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw InvalidDeclError.kind(declaration.kind) + } + + let extensionDecl = try ExtensionDeclSyntax( + enumDecl: enumDecl, conformingTo: protocols + ) + return [extensionDecl] + } +} diff --git a/Macros/Options/Tests/OptionsTests/EnumSetTests.swift b/Macros/Options/Tests/OptionsTests/EnumSetTests.swift new file mode 100644 index 0000000..5e55439 --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/EnumSetTests.swift @@ -0,0 +1,90 @@ +// +// EnumSetTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@testable import Options +import XCTest + +internal final class EnumSetTests: XCTestCase { + private static let text = "[\"a\",\"b\",\"c\"]" + + internal func testDecoder() { + // swiftlint:disable:next force_unwrapping + let data = Self.text.data(using: .utf8)! + let decoder = JSONDecoder() + let actual: EnumSet + do { + actual = try decoder.decode(EnumSet.self, from: data) + } catch { + XCTAssertNil(error) + return + } + XCTAssertEqual(actual.rawValue, 7) + } + + internal func testEncoder() { + let enumSet = EnumSet(values: [.a, .b, .c]) + let encoder = JSONEncoder() + let data: Data + do { + data = try encoder.encode(enumSet) + } catch { + XCTAssertNil(error) + return + } + + let dataText = String(bytes: data, encoding: .utf8) + + guard let text = dataText else { + XCTAssertNotNil(dataText) + return + } + + XCTAssertEqual(text, Self.text) + } + + internal func testInitValue() { + let set = EnumSet(rawValue: 7) + XCTAssertEqual(set.rawValue, 7) + } + + internal func testInitValues() { + let values: [MockCollectionEnum] = [.a, .b, .c] + let setA = EnumSet(values: values) + XCTAssertEqual(setA.rawValue, 7) + let setB: MockCollectionEnumSet = [.a, .b, .c] + XCTAssertEqual(setB.rawValue, 7) + } + + internal func testArray() { + let expected: [MockCollectionEnum] = [.b, .d] + let enumSet = EnumSet(values: expected) + let actual = enumSet.array() + XCTAssertEqual(actual, expected) + } +} diff --git a/Macros/Options/Tests/OptionsTests/MappedEnumTests.swift b/Macros/Options/Tests/OptionsTests/MappedEnumTests.swift new file mode 100644 index 0000000..1e19650 --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/MappedEnumTests.swift @@ -0,0 +1,69 @@ +// +// MappedEnumTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@testable import Options +import XCTest + +internal final class MappedEnumTests: XCTestCase { + private static let text = "\"a\"" + internal func testDecoder() throws { + // swiftlint:disable:next force_unwrapping + let data = Self.text.data(using: .utf8)! + let decoder = JSONDecoder() + let actual: MappedEnum + do { + actual = try decoder.decode(MappedEnum.self, from: data) + } catch { + XCTAssertNil(error) + return + } + XCTAssertEqual(actual.value, .a) + } + + internal func testEncoder() throws { + let encoder = JSONEncoder() + let describedEnum: MappedEnum = .init(value: .a) + let data: Data + do { + data = try encoder.encode(describedEnum) + } catch { + XCTAssertNil(error) + return + } + + let dataText = String(bytes: data, encoding: .utf8) + + guard let text = dataText else { + XCTAssertNotNil(dataText) + return + } + + XCTAssertEqual(text, Self.text) + } +} diff --git a/Macros/Options/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift b/Macros/Options/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift new file mode 100644 index 0000000..98565ea --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift @@ -0,0 +1,157 @@ +// +// MappedValueCollectionRepresentedTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@testable import Options +import XCTest + +internal final class MappedValueCollectionRepresentedTests: XCTestCase { + internal func testRawValue() { + try XCTAssertEqual(MockCollectionEnum.rawValue(basedOn: "a"), 0) + try XCTAssertEqual(MockCollectionEnum.rawValue(basedOn: "b"), 1) + try XCTAssertEqual(MockCollectionEnum.rawValue(basedOn: "c"), 2) + try XCTAssertEqual(MockCollectionEnum.rawValue(basedOn: "d"), 3) + } + + internal func testString() { + try XCTAssertEqual(MockCollectionEnum.mappedValue(basedOn: 0), "a") + try XCTAssertEqual(MockCollectionEnum.mappedValue(basedOn: 1), "b") + try XCTAssertEqual(MockCollectionEnum.mappedValue(basedOn: 2), "c") + try XCTAssertEqual(MockCollectionEnum.mappedValue(basedOn: 3), "d") + } + + internal func testRawValueFailure() { + let caughtError: MappedValueRepresentableError? + do { + _ = try MockCollectionEnum.rawValue(basedOn: "e") + caughtError = nil + } catch let error as MappedValueRepresentableError { + caughtError = error + } catch { + XCTAssertNil(error) + caughtError = nil + } + + XCTAssertEqual(caughtError, .valueNotFound) + } + + internal func testStringFailure() { + let caughtError: MappedValueRepresentableError? + do { + _ = try MockCollectionEnum.mappedValue(basedOn: .max) + caughtError = nil + } catch let error as MappedValueRepresentableError { + caughtError = error + } catch { + XCTAssertNil(error) + caughtError = nil + } + + XCTAssertEqual(caughtError, .valueNotFound) + } + + internal func testCodingOptions() { + XCTAssertEqual(MockDictionaryEnum.codingOptions, .default) + } + + internal func testInvalidRaw() throws { + let rawValue = Int.random(in: 5 ... 1_000) + + let rawValueJSON = "\(rawValue)" + + let rawValueJSONData = rawValueJSON.data(using: .utf8)! + + let decodingError: DecodingError + do { + let value = try Self.decoder.decode(MockCollectionEnum.self, from: rawValueJSONData) + XCTAssertNil(value) + return + } catch let error as DecodingError { + decodingError = error + } + + XCTAssertNotNil(decodingError) + } + + internal func testCodable() throws { + let argumentSets = MockCollectionEnum.allCases.flatMap { + [($0, true), ($0, false)] + }.flatMap { + [($0.0, $0.1, true), ($0.0, $0.1, false)] + } + + for arguments in argumentSets { + try codableTest(value: arguments.0, allowMappedValue: arguments.1, encodeAsMappedValue: arguments.2) + } + } + + static let encoder = JSONEncoder() + static let decoder = JSONDecoder() + + private func codableTest(value: MockCollectionEnum, allowMappedValue: Bool, encodeAsMappedValue: Bool) throws { + let mappedValue = try value.mappedValue() + let rawValue = value.rawValue + + let mappedValueJSON = "\"\(mappedValue)\"" + let rawValueJSON = "\(rawValue)" + + let mappedValueJSONData = mappedValueJSON.data(using: .utf8)! + let rawValueJSONData = rawValueJSON.data(using: .utf8)! + + let oldOptions = MockCollectionEnum.codingOptions + MockCollectionEnum.codingOptions = .init([ + allowMappedValue ? CodingOptions.allowMappedValueDecoding : nil, + encodeAsMappedValue ? CodingOptions.encodeAsMappedValue : nil + ].compactMap { $0 }) + + defer { + MockCollectionEnum.codingOptions = oldOptions + } + + let mappedDecodeResult = Result { + try Self.decoder.decode(MockCollectionEnum.self, from: mappedValueJSONData) + } + + let actualRawValueDecoded = try Self.decoder.decode(MockCollectionEnum.self, from: rawValueJSONData) + + let actualEncodedJSON = try Self.encoder.encode(value) + + switch (allowMappedValue, mappedDecodeResult) { + case (true, let .success(actualMappedDecodedValue)): + XCTAssertEqual(actualMappedDecodedValue, value) + case (false, let .failure(error)): + XCTAssert(error is DecodingError) + default: + XCTFail("Unmatched situation \(allowMappedValue): \(mappedDecodeResult)") + } + + XCTAssertEqual(actualRawValueDecoded, value) + + XCTAssertEqual(actualEncodedJSON, encodeAsMappedValue ? mappedValueJSONData : rawValueJSONData) + } +} diff --git a/Macros/Options/Tests/OptionsTests/MappedValueDictionaryRepresentedTests.swift b/Macros/Options/Tests/OptionsTests/MappedValueDictionaryRepresentedTests.swift new file mode 100644 index 0000000..8aca268 --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/MappedValueDictionaryRepresentedTests.swift @@ -0,0 +1,77 @@ +// +// MappedValueDictionaryRepresentedTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@testable import Options +import XCTest + +internal final class MappedValueDictionaryRepresentedTests: XCTestCase { + internal func testRawValue() { + try XCTAssertEqual(MockDictionaryEnum.rawValue(basedOn: "a"), 2) + try XCTAssertEqual(MockDictionaryEnum.rawValue(basedOn: "b"), 5) + try XCTAssertEqual(MockDictionaryEnum.rawValue(basedOn: "c"), 6) + try XCTAssertEqual(MockDictionaryEnum.rawValue(basedOn: "d"), 12) + } + + internal func testString() { + try XCTAssertEqual(MockDictionaryEnum.mappedValue(basedOn: 2), "a") + try XCTAssertEqual(MockDictionaryEnum.mappedValue(basedOn: 5), "b") + try XCTAssertEqual(MockDictionaryEnum.mappedValue(basedOn: 6), "c") + try XCTAssertEqual(MockDictionaryEnum.mappedValue(basedOn: 12), "d") + } + + internal func testRawValueFailure() { + let caughtError: MappedValueRepresentableError? + do { + _ = try MockDictionaryEnum.rawValue(basedOn: "e") + caughtError = nil + } catch let error as MappedValueRepresentableError { + caughtError = error + } catch { + XCTAssertNil(error) + caughtError = nil + } + + XCTAssertEqual(caughtError, .valueNotFound) + } + + internal func testStringFailure() { + let caughtError: MappedValueRepresentableError? + do { + _ = try MockDictionaryEnum.mappedValue(basedOn: 0) + caughtError = nil + } catch let error as MappedValueRepresentableError { + caughtError = error + } catch { + XCTAssertNil(error) + caughtError = nil + } + + XCTAssertEqual(caughtError, .valueNotFound) + } +} diff --git a/Macros/Options/Tests/OptionsTests/MappedValueRepresentableTests.swift b/Macros/Options/Tests/OptionsTests/MappedValueRepresentableTests.swift new file mode 100644 index 0000000..e400539 --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/MappedValueRepresentableTests.swift @@ -0,0 +1,40 @@ +// +// MappedValueRepresentableTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +@testable import Options +import XCTest + +internal final class MappedValueRepresentableTests: XCTestCase { + internal func testStringValue() { + try XCTAssertEqual(MockCollectionEnum.a.mappedValue(), "a") + try XCTAssertEqual(MockCollectionEnum.b.mappedValue(), "b") + try XCTAssertEqual(MockCollectionEnum.c.mappedValue(), "c") + try XCTAssertEqual(MockCollectionEnum.d.mappedValue(), "d") + } +} diff --git a/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift b/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift new file mode 100644 index 0000000..45a530a --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift @@ -0,0 +1,61 @@ +// +// MockCollectionEnum.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Options + +#if swift(>=5.10) + // swiftlint:disable identifier_name + @Options + internal enum MockCollectionEnum: Int, Sendable, Codable { + case a + case b + case c + case d + + static var codingOptions: CodingOptions = .default + } +#else + // swiftlint:disable identifier_name + internal enum MockCollectionEnum: Int, MappedValueCollectionRepresented, Codable { + case a + case b + case c + case d + internal typealias MappedType = String + internal static let mappedValues = [ + "a", + "b", + "c", + "d" + ] + static var codingOptions: CodingOptions = .default + } + + typealias MockCollectionEnumSet = EnumSet +#endif diff --git a/Macros/Options/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift b/Macros/Options/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift new file mode 100644 index 0000000..0cb80da --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift @@ -0,0 +1,58 @@ +// +// MockDictionaryEnum.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Options + +#if swift(>=5.10) + // swiftlint:disable identifier_name + @Options + internal enum MockDictionaryEnum: Int, Sendable { + case a = 2 + case b = 5 + case c = 6 + case d = 12 + } +#else + // swiftlint:disable identifier_name + internal enum MockDictionaryEnum: Int, MappedValueDictionaryRepresented, Codable { + case a = 2 + case b = 5 + case c = 6 + case d = 12 + internal typealias MappedType = String + internal static var mappedValues = [ + 2: "a", + 5: "b", + 6: "c", + 12: "d" + ] + } + + typealias MockDictionaryEnumSet = EnumSet +#endif diff --git a/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift b/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift new file mode 100644 index 0000000..1a7037b --- /dev/null +++ b/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift @@ -0,0 +1,34 @@ +// +// MockError.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// 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. +// + +import Foundation + +internal struct MockError: Error { + internal let value: T +} diff --git a/Macros/Options/codecov.yml b/Macros/Options/codecov.yml new file mode 100644 index 0000000..951b97b --- /dev/null +++ b/Macros/Options/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "Tests" diff --git a/Macros/Options/logo.png b/Macros/Options/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..02c0f98c823e47bcc7b047da909f16217a917f74 GIT binary patch literal 89318 zcmeFZWmsInvM7qXOVHpFAUF(e0RjXG3GNWw26uu>fZ&$klHl%xy99TKfx!oN=T7$C z=bU}dJ@@^3zuxy2-_EfA!Nmc9)gi?p{R0OPtXz`=|6l#*Ap8eD1W*p*f0PlcVC~>|xOljQxp;+nxoLQ~ zgn0yo`S{_Y14MgZRp^fLIxcWtktz$wUw2GO&#nxOw1g;DVb-q{$Ij#S#Gfxx8e**o382=oj=-&|t z>wGqMajQ9xm9$$^G|J^dAHK->Uy>djAB~{(lDj_v-%+`d^S5KxtGW1J0bu9iW#{Eqhe-k-uP{HCt|;gK(d@sW zkajkQ0skKe{v|>GLhx^m{wHt;XLSb$JMsTXn*TX~f588%)xQvka{lGzf8*_c3eG>? z!X!r=0|xmYQYenG$krtZ2PX-qAS0#j34iz%HGr}&+1>4Ct4@cYo6giU0h^L3!phD$ z1&M$uttS8-FhKTj$ZI*>zmqL+h&b+-cgt;ObyjE8E6MtHYZyhe;_|BE7b;=x!{&wY^f_a9mxANJK`# z1|Ny#4VwHr`BSt!8k@8V`d_8;a*-9bH3h{A25P8$8qiN>ck~8BZ8PUmCdFL-?MoqC z?^4NRjH@0FG!4{oF3_@qT@{vkf_qgAh?7-6+;k~#Exh>tu)LLn1k~E0j zBMdvT%ZHf?2mG={{~I9j7H7I$_10S>Rr z0g^a0s+jO#Z}t$d&x)7Wcc;uO{+;TpF zycl*>df*N;pS%pBi{&VL(81|@>^8ltbf<@-T#pW&)motdMPwU#We5;Gkr=)4WdPXF zYa(h^`Wj4E+sCaYC^EiGocFAcmuun7*is>(_zPM(un;|_NRSFoQ((Q0T(xO^du_-C-#(QU=yvnlF%{pi5mf%LK;S3QopOYovFcSkMYHEYU{Rfl1R#M>}A!q zOpg*Oo$X{7Q1oIim{ZsPao{0i3USFCH;{dmcUiA(Qjfclvx^gmYKkp;T~}5X&pP}B zay`HE1w5t49HSbQ>Jwz~i6C@dSy%=K?OEj% z+fY#;b6QRX&%3%w)MUn}$8}R6AWDGF%Z%SRmr4)_Y2anL)aFJ|7%n~$xjY00p+4!& zXLfv?O2V$Wm4#~>J6QBobMq39Wk`8+}a^1e|r0xbCX)699}KkHLeAre_GbuO>e z{*1kdxUH|ImN;$Z6_zY@r^^}7U}40GUBmnK-r)jS8g*&0A=BPEGGUALoPc253Zt?8z(2!?JqC%G~f@7kc0HvMR*s)>o2NR%eGx9vEjq7Y_fiBT`W%r zi)l5kU4dOjr=dIh#cQia3Z4zV5;IfIY|pHdvv0+iC&^ElMl5;uFOLjXam(ye`~+Mi z0->F5jTh8{V}OC5yv{?*syllzpid(r9h%sk6pr8%#!x zW^80f>vdEAf@fphB7J_xe38rll|7<5T@i5k}8=KTMiQYoq`uJw1%K(jw;S zaVd9$AxdPR1-M)5rZ%X`VG6i5Ce2r2&S}Q)vDl-Jx3XLmvN>P~-B9$IX!zD?e50`& zzP#jjgpG8=w!{Cxc_#M}=kn>KmTkS&W$tmd*kYiE{Ry?=Bc%fPUPj4t3_j)~rI@DB zSV+9c#MkXhZOi#Dlq2n?bgpsIc-pnz^$lWK+UNDCDTZj@PJ?0S%^#5R779hlMbP(? zaA`MC!%EWH8=W!A8y(AMD_cl=UKRMqb^ zfHj<(t?%^ymu;-9F&rs94oj$(%O?k(Xd_f4b=9QQvyx+$ep_jU7QDTuNr84esFhIw zWQEE`E4{T*ZL6UGdZ*stcyf1<^7QRSoIm)v)9oT3OJ5-NXCi+9WM|Sc0un?26eVTRhA|+bIt`%8Y9*3dB z{SF%?o|%GBILAz!gibeML0wv8K^0UDO+n!B2jIc({1ea0nipyl^91d9WHQ^NDnLT& zhZfO;8nVd=@AIXdcenIl6;j~s3Y(?OGV?m8x2TCCxfM@ta#QpwiQzFCQ^&Q(O&gm4 z8N-MxNt24ln*gZm*)LjbK^4TlRUU0{(>5Du;l&+2=IcYOG5DD3_I+i6@-ZvhySciU z94)9nIM1|0y8+JZvn-9%jlhZBe%d`3Y15@~d9QtkoG?g&Q0HlHg#J%rb`}OrF&0b> z^5pRSK-PD_ll(Iq2uXtP+l2M;`#tb^zOf9Wp)OOAx_KvMLEIYS-0KfDO1MYi!;Nih z`^#lU;usK^RCe_Be0TnZV`=uvh4P@=)LwCvv5ma($#6XIb#4Q0%82-J&d$5O#WjTa z@~)1rQ(KW2f*al#thw+etcP`(5J3>5<~C9 z_!ir)WDPX9`SN$K{Fi$R=d{uX&I-POo83-hKcK|KzLR$}pEQuxl^~oB_cClg9`kwn|0@0d5QDJyAmdS-LSN+W0B&NKxy+cV(otX|bG_+H%(8Z!wojch} z40DEO5GzvQ`?}wPn?u2xc-WMcaNlJYRh%=@;?p0l%1?Zr;2l3zePh_}ZSZhS8qSI< z+|+K)+yDoKbKL)NBBXlvUKuIj<^1LbS#P;hHkax1B?jp>;CeDzR=VqV?3kzxr+2wP zB?Gwm$rM+=k;Dbu)aIEum$4;COrIKpei7aszJzPw(n$KPS9Rfp9|>XYJCllSY+LJI z9%LY!$)w2r;uW2V`6*K^G>rt_(i<4Iyf9(!fZc2ObtkfiR?`Ww2gb;7ttcf3^AvF7 zW*Xooh0Zs`9tjZazvhaO2WJR+es73yuWKJ&nC%9!!O5?fQ{f)sv{vIsX07q>uC8m? zpNKMOgUEmnqML>YHa@dt%6AULO~;3%F#a8&76gj*&+0lxLa@m|5g@HhYWtJcHr)a6 z&w8}F#)dQ-$xrozQ?~6GVL6`?m!+v!oTcpo<3%(F2E2$5#}X#O@OC_$X?ow7r;z-8pOYf%FXAc~i5|9PGjMJ9i6&Y&$uz&ZEbq1DsKtD+NVnro5}wXrx*Q z@vS1r*L?vEfa~!q6haX0zz+X~Ti(M#up>jLPZ}BG+`S=ZqgX=I4Z(O!xHc4*?Y=1p zTGEY?p?(RInLsD?Z)}3%OB5dZf?ZD2?}5-Qu}<7n?)K2U$!7j;OU09it&Dva=qqqu zWV^Nv>L&4f?f+~e=K5as(@r7_YOwi&|G%eqP5ka zy*FG%5UeE4_UUWSZHPH>TEm_%Sx0MR{FG6{OMD+}+r_=eJ@2?mBKd}x`YyJK z{SD7M2Z`{BKUzw3-I{0eInQ2{P2xN$6&r$jGrtO=S1&*&^a4wZccdYh`GBz*5#DRD_dgotL$E1k4AElF{0`4F!)|ENcCjD z2!DxRkH)Y|i0WM_<;0TFRk2Grc3vEP4~iB!ig=IkGHkxf!g?fJA$KYzSdHPIvgYO2 z8y!gJ=l(6P`^xdcdDV@wG1wK*C>07}+PO%3&w<`U2s~-(v7pQmN<_Q;dgZ^v3iu86 zzO78nzFhC~X?)py#(36-R<5pnTzilUUY4rdRgB`y^kY2Ho+<6H1wC)8u}V+XBIo|b zbOb|{wt2pboqhT-5Zu1A)od#?Mk+_4F>rj>S`eQu)@;g6c|WmNrE8Skza?MSdnQ|1 zYp4g%?KFDR9*+BUAspLYGU@}GD#X3h0qZnes*tcQj@YuS!uK7e!O@^(i(|bh{COqeD-03c<77_du8KwyQP3? zW>g9KdWwIy-%etaYQs?Suc0IDg>_d>fO6LrWUTn9k|Di6eTqu0F?c-C_qIYW&^)+v7 z{aSZa`)CETGSElX={#YP`O&o2cu8IVWlEtxEV*~~>j|T&xjhH6z5uSLn$wD?=*TRL zuZW{p?1~^#c6=JXW0cN0cnNRFKu8~2KMm9aStELS1RxqKDmhpZXcOBFC(DnQt=!7# zk#B^q>``Ze2psTC?lOoMtT4El7L{m)lO*7gqGZ? z6N>^q^W)RVT}-JM#=6qj|5O|#>MUDs~Wbfl=<$y)xQc{qns0jcJOWQ(}J z@vwitH#>kgRaXsaFhHD-mcphK2qTR8C)Rlq4C)@bH#utdNP~aaj(!!T)miPFN&I4; zNB8=%Od>MLQMV|Fpy{Yb&^IQ>Av%}*NhSdL!-ZigNT)HBPGcJ(rk#tLmLRli2T5N5 zMbL4DROM|-O(F#HedVu)CSYV4{3mvG%VdjcwblJBTjuIx4w(( zxA-kVHB1a?#&@koZ4tACRh--BUit89sp);Y@tzXocP@h>S`jj|DH^xcTF-Y8ow;Ua z{k}e4+T_{TZ%0my9 zS;?CW;g9p*WU6*-nuln%>E_1w*9JoCxshV#`@d}yxV!ky3}T~vanV6RaV?Dr_^#Db zrsNk}SDAcXo^-@nREzZKQhO0)umK0Y6a+K03?MZy6TeeB7B{!8Lwh54=&=2W{upzs zm+w!p@tNo`3T(_QiDMQ{?&G`p?>W|r<@Yt_EqZf6CP>ag!{f#`oGsTOZ(<#N9=mEi zXDD2$q&jVYsI61tA6K1I*i<}`DzxsjYF{=4g=^G*2SmvAel^)gI5@`iirRG&aC|KY z3@XHWRS%;)zq6Pj^-X#dDdopy^*=g?6*(KPJEVoGmm6?C*$qN)DWyJ9eybsajXF9w z!SlR@fMj8XT`>)@Vm{zX{*SFt@7IkdbAEFEXVNA^3O2B({da8+6M=#7X#LIDD2d(8 z6QMXyS(^05Ip$O_!Kv5y1~&C($2K<7^^UT`goglvyW=u_gyjh^ew+>59Ox9)uyG4Y z#3thV^vh#;a08#{ogl(|DFlbIG6om-$C>nZF`=^>kxklUMKRPc6yUD%zo&)$zKu4rl!9HUt!af*APO(T;CX>9W-n`DM zh#%ia;bUTDgU4-Kgv9N(yb-0eLMJv{_Mr2+0~9n0If zpw(dqngB`4b^!oq2}`}gw>D@^K!kXu#!ae;s=9T5}1vX%`i~r71f0 z*52#zKuz~f$|A4e?@-l8en!4*-31d<3tSpMJ^a2H+H!$WGUNDGB;Yxh|HC zO92uT=WtgK>SaSHHk*1i8!`No!LuncmCf51bqN87?+ZGDyAuT)=Qo{g+Z5Msrrb`q zL`l7k232)hVrnNN8Xy)5p<^3BwexI=v|7I#JtmeEl(%P2MmkY0j3B+QbcH~a{UO0q zRMsga>iqr@kH6PBsob_x1xWu(?-F}i*th=!N=2+}Pp2DNcF>%P7va)^#ZXCSX)Ntb zhp8Q~&-Sm=`JNhXKrR=24nrUHw3-S@%s~oN?3LU%LE79evzPxC)Kq96&A*^p^AM>J z#GrGjTqCue(@cwWxCO{U!R7iz6dW1=b;J3S`mv%mFT#e+sLP~3Wxfn1Q#kmJt-|7> zqHaNuym@H2PFWjA%lO8e<0~oBfbw+1bZSI z3l6zQLh{~t2@7_i&AgS8oadj(u5p+qvo@Tr+Zc^xEUEHhlTTbw$Eqs{g+q_96lgqK zLjtycR_erU5{D;HBdd0Qj5IZkqutDJIiEEl&ipX|f)Qec=(i5*;g+w8rt{e=HF;6G zxWg0anv%tm=9rHmRrOsc6i|2>W%px!U8RHOn27)u_suaa9PKi#%0BMbdX99}_&os+q4WGZ*4x_} z&8eX~eHnz!{(*%0K$p+d=Mkj>@eKyZly~^$dqw}I0FGdJf&1km zg|J=OT4dQ-wazD^&cW9rjv4wQL-xvNAwq%Ht!bTCXP$h{0!J5uzkY~O2LgU4JEi?8 zrfn^GIBOCu1vWuoRAFV+W}V-MQDw^qouLBnjoEY&r};XI;!a)=O7Ealag4MVwW z;pXZI%P}{{wBv07jDvp>l}bLP$zqdYI1zp#ToF$^28lGwIgOufo%oo@4iD>3dw zl{(LTg|%lJnq55K00;eWjaSM>h9)QYK}n~`6>131Wj|=d;bP17bmJP#k|wb&>w9rIasp)dS>+ zIH6(g`2E0fhzo&XZ(;zh93Id%4DA zVBk&|9~Df8RQ3_LXVvm$o6>6`O9#9Ls?Wk!P|YMKPowW|&{jy{J?T!$G2V!pp#-iS zjk!=zP>&t$*f}x;K(P_Di-jX2Td-_I>ptVzUVw^<4wyCFNzJCEjCbeQ-!P{? z^vV`)pM7?e1Ofpr){(7Ft=EnNbO>dRdei*C4jcG+c8hC6)KAs{b^!X^9&+Wa5I%OJ zmlVXqg2BI}j;`s?x-T2urTdJbc(gBE_n8yn1MPS(E61eKv@6iJNki&3$8aP!OAM{5 z*Ir|`X+c#7Xchq7R?Yq4(V0sAN^`>X(y1m;V}$_wvz~al$azr7#a|vlhbL_{cM>NF zN8uUg{zhEHhY~oN4xs(nz5GHN?`HHTtv@>3wxxknTo~=$)~wh9K;0~2IkLpQCU-c6 z4_?0!Y3}S@hvUqxPohU}XEF=*DHo-eN$K4nrl>Q_8cT4*A+m&FFMRSAU$cV`3qbVc znlZDm!n1$v2;ex@mc^{;kM@b0SS>Gd?pkytGVKb6I-ynd+sn>NP!`h233R$qpxBNh z;&o4&A1I*f@Dhj>GL;M`zQs$H$paJFFb; z?OXhQJ-&5;o4Ql&Te;K4nc8mjz zHxW8dqg+9W6mAJqz0q8qYdmG+yYy_JyTjn?ZiI3T>44S<|0X}g&u2jWl!TIlmC($t zcFXbF5R+PHH*2H0MqfJ7ol2)$dVgg-CaqD9g((Bm@jA+dd|!uq;Y>ljyaMwuV3-%i zrsm-@)k;)57qC6A>v4ZyN)bQJm;MdX@1yglJJ`@4_epw`ff;W*v|4wz8zBS!`sG8; z86;mP>4nX5vbgfm&?411AcFHUVGGm!cfH{yYP2BOWTR@_^=EZ#o zrD8dh54c;kZ%(0TDzS1?m5#t>E!exzkMBVF*GD-Pt@TVBmy2HUWqqR9&9ybeK8+8x zx+;YJ5W38DWl&d!0M^P%&WW$fhnO~<0zwUL#s`2P8`IVBr7IfAm0XF(ThpWw zjWv9|Sp77&&q98&{=kQ`V79YdJi^RrD+HvPKXj6t%r#^7hqlkDHGf=wpzAk!P4wUH z5L9$TX*ni(GN$&)Jhv)Jw?TDgM!&0cI`LXfvJXT4x}OAZlHQq67|2Ve;UmrYpQ9fr zT6`yR`-LU6#r$&)=<>_yL-Z@2{p}=~KvF`qvKKS<>6N+o3eStp6J^(kyF8evhkv`> z@qX&TZkJ%l&k}8c$^_Bml^s4YQ%qKuv4ES*Cp3viU|o$3j{Z)-)i|OR{T0(WhYkHH z+yL`PkQshiO0@g>fGk_Lwt2155-l%`&pMAX%KSXLAj880x;12+x})&(*0k2DW4QzQ z^@0iA1z5`_y=~9N2b-2!pv|aPjw>_*EUzGL;?$N~@7F2r@k0pm2P4Vz-0jxB%^e+- ze)@Aa@U!)?`9hbkKniMrD~giaOWre~WN!#_Yy=Ff3XDXPkSmr&K8T!ZHPu_YYdOkI zo_ZS)@=^+l3zszLpNeM3@;Zm!U1$iw4stPVl-C9;%F`#8jt)E26Z3ruLuUlMs5w?} z#j=$<27M$vC$dRk?#FOV){6iY7k@sz<0_VJbTYfM7?-t;Y0%pWWqsl})-^LH-i8R& zm}L}xR_B%WS5W)n$G=2k%yTJ5lF=z5F%7;N)SHyk^B{kF-p6Xi9io3b5Bg~PB{OrS(0$r2Gs77AvyHd<{cazOHddbxffo)~r~|fy9j~Y~9it=}Yd{jjm0mCQZO~Xw zu;)a&=Lu@*{YC$gE5{3U*_Xv4*mli({4&E1%K4|G#8NHi(_&U+zg^>t6h3T9*uwHD zcUw^WKCUb3!^IsEed=D;M`qAXmzh15{EsR(TZx@+)W&bGh2S<@9kIg^FZrc5DK~yqc`s;< zS}p@eu2P1*7-5?G@cM;OW$|7avW>`-)b|ty3=8onl!YR%fEnY%6Z+;UDc7mB^^ z`HZQ-GY>X#?NQgV31@ox&1l`=;31bkVh|w*-J8ciCFeb-tcT9X<5p+A>8lk<4mV~t zLEYb|j+s<7??F@OW5{6yUhGkgZ#C|e8Z^jW(>CF@2?{kwG#c=uZ6zdsg($c+=#kRg!lSf zr}eRI9_CnQGId_{SMv|MedL<*%uizQ4Bo&d5xwD_=N_|_f~TwuRXy6Z5Hi%u`^soD zx(nMNo@=4Whvy}cXa1-fKe*=}wU zRj)eg-w9R=qr`gv6A8zhL%UzQxn>Ttc3$0wN4KKR^Twjy7@P8^hG459uM=Oq#wU=GA3loty>v9x6sCbnsqiF?4t!5h(73h?PK&)D6=@+o-MBC(Eir zGK1))-RHX}yDxiW&7TceM;TZ89Wq1BG}|YC=c`xibzVhIDEUf8K)Z%~RFdpXadi5U z8OLP-fPE{qPA9@swauo8SLG}=j(CW6 z{3Kvdvlm_9`7mzAv))$QKK~d^DUmyE7(k6vzUbYf9qFwUn zJv-|LrkuFY%~0%Gy}W4Rmq18;cqr3=ZKH)fdj5r%GJWdpjzt3cd}u-*e24B}K6ZAQ ztb_k8I{BvORl_&rh@y#9D&bc|^mcXCg-7|jXB_f{*Y>BHNJz3?-F3vzJAIzKKdDbkgn0=M zDuYmb-@u%B)(Q^kjQSz3vC?^a8AR)w5dG1iKcyt1=l2~g-lrdgJyfyU=(e2&WFHMB z94X`m+*uk?c|k^_KVIh47<<;$8;!p0!?u0tRRAeROU=k%19s1(Fn63LA+n*l@aM5M zN~Lo~-t^TSS$FfX`X1ds@wbe{sbdL7cG1XqoOdf%xn0kse6p%a{T*NIoY7>1{I<^S z2J#n?JZ`T@uk6<&HT|2Zj=nR;M}E~?NnntAn{hora^s=s|NR=mNqsbuS^HvUQJwmr+%Ap zHPXzPvLi4o=ICqoOHt^UNw4qFH>Qz{?Bj5Sh**A+cU;JiG$PD=Yc-~PN4;<-_>7bp zb>yq#g&4riWxkTsP8~_|EY+G_%Gj#&Wb1ZEx&EKkk6CS-Tm0g&yHTC?9?`r2qnW_m zL+jMH-bch9S7py@aPvo*1BBUrsxQVrixdFq5%JfyV_r5_PfR7Cnm3LshB3>sdEPrN zv1Hrco5c>Uz1^5Yi~TKOpL<^FLmP6f6I)cFzlOd&(z~c7)RNV??BdkbOT?Tm2d6g! zDmWtxDjh5kQv)VbhYSFW++yOhr##`G~O1R-8~-xPQpq1dhJfV;FdFX2nX$-TFlA@fPKCNyh~_qmSsXvdFy ze>q`s+MgKv)`629N(F@eSZZw45nYaO0m65Noxev+n6E$TeweI_ zoUG83hUY`4Xnp-U6D^#FJ1@M}yoxq`??ko@F}R31-ubHJNIPd})6AdsE2J;B{Y=92 zBK_|EC!31l{*5P2WeB`u3jvSawke|Fv4WXOTb)Ap#%!>-V;j}XL22Sz4OOw1?cv9B zWQ}hP22RI`wltrWhECxI6yY{58{+R)l3L#3XLs2h*Uja;U{k8qm&H?#Z{gkH+Mh^G z2H?8&(-pPVV~3vk2MvZPB8qPz^gTSBcA+ZR?U|?bSFy*?-i@;T(K80e$Rg#Im1RS( z_H0kHqiY;2w?V6spw%&^#>5}X9PVIwdCIoINUW)A7)$(8YIQDk)%F@P+*tdIM6vnpPnS)9xu0`qZN4$L7jDa}8VWiUv@EStu5T zxqL4Whv2U7C4Sbl#yX6&f?WK)R=-r+86yKCTAbRbJL8nrxc=!qB1)uyTfAkZ=(d|( zk!}MW+Dd^UrznY;Pr^_lr{3lDvMXbYQJTH}z;nLrT5RUPkkcN9d#Td{p9)*X4~7rT zR1u?{ayJ~#5+eJT6M&fD@^|K)R-?OacUv(>T@AT`Z8zTGBqF2S& zS>)1+{wZ5=(H3WR*Nw(ta*Aa8^vPni!eZM;Dr+NC{*hz^V0usKduwy9#5NqP74kP4 zo^}-TpIpWvS&mpN`!b=QxKhMgC*Kl0+zG+p5f5uWH#kw+5vxHy=W^BGwQd*~*Y0$P z)Ujs?kI_eriuq`evZU11B5#8(s15zt6osqHhWQm!cieGyKKAxoNep`j0p+RX&36oW zUarF7qV8c4#q;6mi@IGE#TKPWNhER1OXmxVAN?+S5>OrW5aGd|*Z~V(N^ar~3xkYJ z`VD*09gMt-!=icM>q_OriOye4H6`u-JHed(t7i2DoPM)}l@7yO7)E{6MC5vY*4_-| zh&1s-s~%^M^Zqsln4`30O~5BXOhKILJ-J7_d?;dU)cz+v%^46DnlYj}ryMBaXS z$m6Oi$Ib4=F1v$OaLz@Wu^b|}`N|Z3FXOWo{j=3!FmJZ5$OcV8a|pi^BUrE{*rCr? zWpt3SD)VKq=AkP0;iG3TDQWZfQ#1;T^!EEsMO2CNxN+5JDrj%J7faEdz{waQe*;4ds$V3CPOS%UZrHw6Zt|wKx z<5KdTLkiOEHT-POmEM7Ul7lQg5AaTM_*Mqaa({_>Nb<)3S>XQwfk!PN#UqR4Q)(8_ zcV5kCSRH2)HHQSvpQUvF1_I)v+xoTPLumhejbA{u6nz@nEuZ}5^&4Sek<5o}*X7_n zN8r(8xY1Lr48S9q1duP(Pq!J2$<}naG0>jPiEH6pxIuhQdp{+W2an`>SbHU+-NhfE z9xVHh4aVQzIkvH^AERQCwxSwZ-FBv{g7joUt~nhu0C@%5I^x-SU$zo}Sr-#E686xK z87z?qtMrCSrVIMS@?eO*loyq+V>!i*;!fN3b&T2lUQmgBV+C=Xek9y8-!H6!S_ydS zmCt5nEvOZG!R$g?@|X8)7|bT!Re>#}zK(3~WRBn+Gsu$)kQVu~1$;?m$_?4u5BIDM z;^$&d1Mzcp%{RPE(c+Z-8E`m4&~35g^6RNHz0?(LX_~RJmpqClg;guWk>rgX2~bJ8 zLuSK`^6LYj(R6~_>xuwXH-r!j=UU0)z5UeUV%78=?{ z@OmZSBTG7$q8{Q6F(l&$zKrn2{PE*BGCxjod-Y6da=#53a>Lbo!H{4N}_5oBD zP-fjy6*YJV6T;_|;pTn~j{xXpcIxEh+Qvi=O{=R4ug;PWJeIm(7Q%Zn6QtN_K**sE@hVKTFU; zOX=cR1|5bKu3+Y5yQoe*jc6SuhdYneMTvGF{%$wwon}}B zpYa=hP{35Wgsfmt_b`nA&sea(1(hV>2i#>a<&2H)Hc)_&8Z0Yry%%6B2|?VcP#PC` zGGk%b>9Pxfb>w%1*NGrL_iEbTm%n%#O$zT`c$OUASc)N>==&#mShDg-Y+hu=4(g#X zOrwDZX+-XMekxjWdO|g(lXHXY(TG4)R}W)7>pO3kd+qeXDXoeEKv~p>ZID*RQflWM zFRL091lgD#Uqi$q*4WIf@z+oEkG+S4{;T8|Ds zhu_)^LdmzOMvgAXuoIzmDR<3$^F((rG7i!B24h`k&P9 z9`nk0ZliTBv`7R$wd~I#RiqcfIj)LYsK8<<(&e&=V?#+V>R>%&ff+?`@-~s3Q+A?k0U+ah@KOuVjy`IBn!tK849XNfw@=iMkEz__aw~ zS+=qqy?c})e<3>Vr&Co;8kJsr)Zis)t#-8&*S`pa^?54@KOb${_gMklbT(-Wi{_^Q z$u7Jy@?L3N>e(=YC|h@Z$(rpLx}x3)UEwzIb+YST@H6W5+4>o1PKrQ4{+51tc>HmC zm{~U(eXuaLvo$JKESGVsgA;s3sbnO)PT=(Br#fBg%Tee>%1gjWR=_n9Of{Lqf^^tS zd!`A&p4*dO;vsfAE~G7%?M~;}j!0pPo~CKrY(}v2h^>2E=I`282jWFp7k$iVHeA6A zOAEDM6di=U&dcr`?5$KO)jdc9JM`VVu_psMC_3tt+_i}Z(g%#}x6D-hSbxq`f*yy= zgJal`FjQe#H|-QE;CtZP6<1`~xq8}Lmkc5LMzrPW)a*w}fcwXu=faeG=${Cwq@zVm zJNL`^`KcHL12wsmqgI-Sgtz@ii~r^!quDZT5y z;qW^w_iI@Wzg~EkGwAm6&0p9tvwQ>I{tb=l7R;$3>ik6mJ6>vBm>=K}dxn;Gf&K49 zH`^B+QV$q(ynDY?E1y%W?qbTHdJb6dyDC50JAGi{y$c`zYPQ%&PJohZ5ZIZg*m3UF zv3G)V*Jin?@dPjw`3;%x%P6ZD+`4%28hR8s-mRz3X>^0p;6WTBeEn%R4XQJ8_11A! zxWPg^~;Q-5g(<1K>b}}wet5C9F=>@^M2id z?Vli8jL5tnUH*^j$HNwt$k{Tyd;D69Ua6EW=+_Dz99tK!!@Z`vy-y-4?F}{OzNCf> zpb*@@fR2C8qiWBSU78}oQ-QL2^v4nA7isWNbx#MK8b@}qDZTFoox%w|-k#24AuZS{u$~P~jPtbu1UIiP;pNHvV)u#(@u^cd>cZu9I8fYZqlWJdX{%&*iS)O8| z%>=_i^~Z`4;I_%@t3YcbCRBuI}x$v9b|PLHqZSbvVbEPoGmcuU3(M-m_zj21^< zJ$-kHmCcZ`aV9V4DQnFt&cHC2v#>PlrvApt#zAUw?v?u8Y(}1_u;s>bsjXHBL4f2} zG{HVMY!?Igig^Jm)nOt86(>smN#ElS^o^9mhOGx7(io8;Jpcu{s64Jdwg~P+%vYV5 z?FnH9udCoi$Iw*l^`rRAABXa8)d37&dL6|Wyjq+6PY@rfWtX!#i=Z_%JTD1%m8(V- z_JfE>WinvS+Ss3Ewrjjx0h~1JdqbwgRJcV{t~MXi;Amk*t)u2ar?8On$Ck}P;^xf#*#}+B z>OIn0%YD*ZxiuqG)CX;~x~d{tq4E#bTP(kl1NXpxt@AW|lD*mom?_@kpp+{I!H0{9 z9Y8#WgO`NEgJt0V|BwH#lVF5~<_F7RO3nW}iO>hH>~g4m>A4oZx|MRUhI=86OEohb zZAg77q`8$hYXQHI0#tgiH*1aqC(UiJ*aFK1PsmGIb zCJox{Ivj&!fEX<4@h}?QN`?gH4?;Yfmj;zx$B&6xyY$2YJ6WhIU*PO!8cZOwvC-Pi zPyKU&C4(%o9Cp(fcOBcXBh5y)^^Ur;4S4hbNiU?!W2m%Kl7zvz!hizR`UX|@i{!{j zal!y0hf$AjMTK=F({Ni&#(ECX7JrGpCkb)Qc>S~8n()O`4hoGbH7tGY-Z5DBkp`BI z0xa7j_nF(bmOroh1dScJ3Vmm3obS$RnAwL?&Z{t>*s&lnXdmDa)dij7o|A7iQy^~(*0Uz?BM+}9Vmm^TQ~ z%>?l0s1pAzd)hGdRB%!U+)P;1(xD7)`f57T7N35*pFH3tJ?VcI+Aa4bAMl0Rbw_uK z|Mht4B0EU^9G6|<@k!sMKG&6(F0Ot1l3p~;H?}ZN$WW=zRGtk&>~f&=X!6dLi0XIT za>}?_3Q5gRiSFTyeCaNGNU%gkZ6gte-!L^+=00{9P9KMv;VThDeM=|x+X-~R;}Jn8 z>#hEl-NW73y0u+Qn6VpXKS;KB;E-H){s*+uZ};Vtq+H?|^7TqFC=Do0<1#&8sPOK% zan1LhkCr%=X7iOH+t+QZ5W4dE4~yse$(8znH2?9x0Jfj?h3|aD)1Q^)Zh|Wpr{3^& zGa~3UTQ)R$=QnP8JrcCaJ8>O{)H>KQeS+D%NM-0zhy$6_t`)FEj@9``E7vIlAv zCn{lKa;uWGd9Ajk^sBJ3Z_fjmwtMo(EUmP0pzRsGC+BM%Zf-8@;z+Nn8wsr3wusD5 zm2S3{>@UGL|CwrClTT)~$?$(9WwYc*z&mC#r9BQK#MJVJ{rA-C5MpCg;m9<+{GIJk z`Pe!O=Zyx3Fu#j#dZORKZ{FiiR7(6wzma{OPF3)emLCYlE>+NU87kp0RZvTE1#s6% zs{B5-X?e(1{$7lFun-U+?oyB>DC#XuL*6z@g7w-;Ixz+oJK}-l&P0wJ!^@3Dm}ERR zxO%?&v+KGzGdA=Kd!cEeH09jzHp^P@#JVB*@h_^#H=**o<$*7rXB`G>(1OD(zhRx{ zpseRw0U^)aIK}DhhHrhDJi!Y33Uw&0=odeggunV|-5}cjR3}KviwyYRQn?Z4Wik8>sv~IIG{FgO!QI^*4(`E1aCdk2;O-XO9S-gm+#$HThJ(Aa=lx`B z_y1IN*Yr%!^wals_2RF6+nM7@y!Hb-S{K)me#7cR-7f(Xg_F`PLw--+#00owPRqj! zLw(zBv--Asu68V`bseK2a*>9_w&#SsliwRF2wF?ZvmJ@WK9sGPF&_sZMybNn4mhsY z*E>j7M?-$(L#shw^+5f9xVI|ybRz2xzjUzK54D{qc*AO!Y&r@Yr6^(KR*%idO$=Y^ z=cC-cTH#3X+p^(%X_Qtf{j&)ox1;?kVWLL}`!-RC*NqPoQXyUq8JaUho5yDg^cTWT zgRn?pZ|D5WfIPkDqnyU0T4E({d#P_&Op;*5L*?y&on7w(Y|v{ez-Gy3r=wGfm>1wA zsCCGTDRm+8m^yEgqZjSO`WhGcm&<5+0uk-UNO-ij4{5yf$LLFP?vv*ywM57|bD4wC zeYptWwix%)&77+WJAe#u!&!Rm<8WB{v=sQXc`V%UreBH;b680Zo=;Z+fwWt~=&?B= zuH$eQKkYK|!vDDW4_9WF{<(H8kD1Y_cV65{3w#m+$H>5ou2mlg?{7*`gO^KAdc&xx z!5);vr$1~@;$)#{vcn z_x|>wONCK!{SDvJ>m)w>f9P=AV&W7dxWEFRp67iTn~@yBFxMtb$(@cI%XvG7*PMBh zJHL<7=wJJbPG+(DaqrheFJssi5*-hk&>F#lC6x((@un(XSG&0{yp`)%>2DCy-wTX zo7aT@&kDwa#$b_4@D4zzrt~W$;qimAdy}yanDJZRSFPM^dw%=7kCxe_VTIntBKnIe zPQduU+}~@Us4q?Z24^|(L4G)RbKR{9ZS8Ki++S8pT;8X#=#eZ>2}i7czPH2lKDsEw zahDcS+@-dMsS?MZtlED7u&u)(1URjNZ+6YBFp^)jC6C+EZ~TTG-!sZ^D)8^;Zgw<@ z4{|tB;2=3<2*cnp5x-cIofKIbHnur#dBWDk(eL0d z8i_uKK zNcG|5?$T%2w!mKZ1A}p~WCLETt(*xX_V!1#b;bWfst*Z1xe*5<=z8c=U2uYR4nn5a z1ZAy~Ze}ICD;#z~rMtLdBiUSJ1nk2Uf4+{`2}rTAJ&uEaIv>_xVJ|$%7rI^r&VXmz zez3zIiPzE{3{NY|V&osqB^E?&>li>ftgnA1@_S!zf)VWs7K>n+(G|IU>hZrgfN0>m zTSBOzf0`;}v0G&m%Ay$))sJ>jHI{u^Z$Py|iv{BZRbw<`-#GjDq{4HKy?X)?pB~E^rC)7^2-&e)Co$jouH-BwPD%q^wI!;um-z z4e39%zBuQ#Cw_^VvMB&=cS(4f6#fDz)Ia#Nhl94NL>pD!A73EmzlsmB4JUvf)+c82j04l zgg*EtM#qhtRNb?HPUw-IcWG8f0@KY%oVc$m!h3k##6F4RheLM{AAwZI#IEBjzHKeA zfrFddU5zC+rvqllx^tAP2lt1`HCEYSKX2yl_5?#{T=D11M}KqUg$ptyzY+gC`1o#m zU&IklYmon)XtXd~L;*N&Q)xvBrepD~&jMZ@^8+T7kn!3i?pt$>s-Tdp2Ovk7xSVf$ zd(718O3rm*>yMSFP*I@nGx|8GNXhavKO`G}?Ss6N#aX(2cXwN_mu3Aq6+hlywk10| z{No-K(*pKs-Ajnz{#d$DOW3P~X7<(!hADr>We%Ou{{0^|-}U6t8|4hX*{{y@!3l1) z0I`WJ=$8=?%CHo?kjM&}nCri3Rq~x*?mT2K-mP_zQl;j*2>DU8m|3CA6xX^AE*_X4 z9DTmG-1jIS)S2TePrS+@l~%lxp$C|M=+-sZsr~s><_PZoe3qxGN^I}Km$3sb=5FqgRa3(I$nEORc3$Ni^F67Bb|ufeq}U3Z!f z?$c_2GmdTNb&uN{xj`tpt$0^or^a}4t_sQ=)-~t_fw}LyI!|6~)Zgz5r=FU=jR z5PVgF@3lMtbbC9V%Te#Sp|heU7Yg?${j;`QRcf_InLjLEHFkYFbKw@ghY};HZWWNo zPMamaHD{T2IvFN7WRwTUPfUmF#lK zT=UCDvLlWAAgr&|NdSaF2y|mlt+~RVXe&92M!(q~x0)!iY=p{~RZN5X$#@fZL9)YH zJrQ{q7FmQj$~?F>LvKK0b4{$llN8H?3Yc_4pBPO3j_#?jwhMPh+6Po{4Mvl9be=2vT4U6Nmy^Md{pm#RUqQG*+m*5jHj=Dc z02u~9f&+VJoVAitPe>)Ne5jp4_`9pq9ZZ%gDTcVw=Too}eZ?>2&4(qCvqcLaudi|S zo?I$v03>9Zqn{WcE$=tA~4tI{WTSN%{BXB}oi0RR#DmSs*hj>qkj%OlOuJcrSt=;nYhAEwMPus!@l{-SvYO?mkVh zv{)vA?kl9QE3|`m^NV9Ck1Vh$%46Pb$vC+6jPo>}jzrC>fHJe&;s2$J2`=U;MBvjW zbROQ#Lj?<5iXC9dRn2efH+8@;9|{azcf@W;Jf2OA{3|kB%2?=OKoZr|B&2ms$)R0N=ng6T(LMxNKPl5WaId$b z=tb|noO|Yo>mHKPq74qVTp4nM=%fcP^(wD8?^yl!@3;*rFJ%X4GPoA-frOWmwCVOg zqd=$s-4nwRVv-#-W?v*6N*XXWOkHZ+R&wHRz*#Lvmdblla*NtM;TTFy|2tYH+BeW} zzrZZUa5y=@zFv4x$z#Zd1WeHl;VF=ll`MXHD0@RtNH`>_iuK%x&v^Ger4 z9=d88X4hXjtNspRb;&55(`Mk@9aQliVY-oy>Y!JUzFS#-9+UL*waf2ewpxv850R&=Y#0rU5rIAS-$s*!Fe;3);Q~~#3{J*42x1J@A zkF8E4`9WbL zdXo{C*aS%#(4Wsf%TmG?;q~)42Nci#&2c%zFgKdL&n@>$lZ*$xZh-3%S#}TIJPg<8 zdGZH&DeP)gmI9Kl@ri#&ki7+868EH&Q&)|#*n7v3%RmjXyMfyeQhb4_5HaNiDd;QU z4ML6N!?PfYzlu);;!{4n9qDG%w(6cs?F!Gr(sefi1b^Z+w$El@g*#h3#k|%|nNHj~ z_}Pmjh>c2}zc4DUMctu4~hP?Oz$SS<3p);nffub3ii?*>A1sj107CdnL6paJdi7as%`qO)>&ttuY z2l4z_d{&tMs8snYgcC=`h~?M&ysfmM0^Yj9rgS*{ra=qnYxZ+_$=E@gj{3%W?_0+@_gw>xXD zT>hlpVX39`XNf#>Gkmr%ads=FU`u`K`(0hy_IBO#=D5!U!dJ7q7`~JIQU=Z@~0nCHP;)(i2 zIvCVJ+9KrfLDtJTTg?y!_AC?%bNrn$vltazZ48xPoT5LkGOof+H8Tg)S{<0jUuYtQ z18_sP+MSyt#A9^F3`<>Y>O4vx}nisMbNzx6@*9_Se z3B|+uKH@TgTAU|cHtl{buae;FK1vwN%oO*imVMn&XSnOWvmnbUzmiQ3w)eZj7rSIS z6qz0VS$s_{&B@B2r@`gi>g!<+F30dy)iaCQ37}oQ*nF;|D6NxRir@ZJ3m=^J`#77a zt<@fdLA}=Lg&Xnd$@vFta=$*$(}8Y1Da*p8J^KB^)o(`uNu{#lvKzDCYf)UCHSXBw z#`9%M#K6*VGq~vd;D+yV-h$*#8LHAzO|AEkv6qLBHJPDgf=-|fu|VVcfd+>LpOEGX zH)o~_zdgst{T-%uufIZ+5DleP27ivdV_URM_x^8SsH?Mqct0<5%9Y(S&6$P%dicOU z0LG_o>)E64F<%IPjRY`4+Zv0c#&~A&Iy5Xp^F3`h3`G5UIK0Q%#=KLs6|M@H>YcT4 zOhEKLG9pWN53eD|VT}A?f+jZ+J-g$nA`4DzNm6KB=Ki~tHa1}T8f?&ljwYmUi>St< zA1xF?%Y|~w0d#p7n*e97)(rPHY$&<$&+JI-jjB$oN zG$wDJcNO0}-hRmEq3eNo<@OLvy?iLvciZm;_t~zf)>cuv&%4rd@aK7h)>dM6Z<$4p z<40x{J13vLDWKN!z+9XN)wrl)&jqM#hhat)3{G-TG%SBnQyDAS=*-OG74+rMA3Cr8 z3L%erjfnK}mdI$e@SFJ`v{)@8JJkSEs|^B*x8F8UZJ24MeZNEJmqR`YXFKqKodaJv zG`bST;J{;)sj96cqL-6|OpR{p@FN$Toc&MgS=09_xN|F}txWP9C8DrCRhL}$4n{fO zL{73`pr`$q_)csHl2G`M|Iu6Z9!B;U0$YwM`qx&+81Kl_C_xPVa6(ce(u`eqCZ{LY zKixRG&S%xyhZ|Sf0>I8Mcd)M_!5~O4wqd%wiI-f!>rN;B&&YqLTi>0z=f84SZh$2K za|==>tZI3%Bhf@)Z$UcOf5eQ-v3gE6m!|YBkiFkYNi#LasJ_0g@jDHMj+b%n6Lpla z&$30yQ6A}g0l_A(yh0ft3H0Ts84@EU9rqToILNK`3iD|5c0GAR-jh%N8ul*X<1|ng z)OP*{XWIorh;%dBVz$luUyj!78&olVV%+m;9hNlaKSJ|*^8)yUB#2VrTvc@PStLJK31ZjkbvIVQ%<7kO19_L@78%f6{sESv^@c5zN_#K$ao34|N1h5**Rk^` zHnYIxbl08B)G>SLzoi+A<5wRsI>1{L-{6<|oemm8qQ%05so@XrtzG6D5Bzidn~CSk ziyoKf#pw8s(qK+rz_%D{FeLygJ(hMsxqEs$yW}TX7jGk$%HMkITKRz6f5Frv8l|&L z%6<>9599$OF9L6gCu18A;b)oD3?1Mp5|dy!Gh~{~zr%Chr2QvL>%`a#qtXpr@df=% zyZ@lja=TP7+o6@?Cg#R_sNzUgIwjX-2j*wPHo(rosoY^^`0r8m!&MZs1%J9qI_!x2 zpnCQU4Q~7FQp>9x{QwgUyrrqMc}~TT!BAc9PM?-)Rc?mGkn%?XT@qW2K0x~*)ll!N zDka0pkJ;d-f6PM!OwJhyd-%H#&_;P?JFTBC3_|-tc|O2f00i=hQZPs|Z=(?38sMUN zy~fmAnEzI`?yF`KhkQL%;uvK*<<7_~61-nRALg6uB6mCb4ov~pbnr|?Y8*oB@}ZJ! zt|q*IH!!o6dw*#x6h21}UPoVz9WU(nMbrZ01q4U#RjO8t4SQyYj^O z%+a=vAiSPQ{kO;Cq1t)I6P&mMb)kwtz3BAaixrSE@x2-X^qT1n|4o%DRa}j3t{z~G zp{BWPN;B{>e>K)Ud7&a!0>szMzI{E=+e3iLjIjT0Y+!gtE+a2!FGG#sWg&6tSzWOd^*>~A7XZeez zvO`tdg>-{W+>640KJV9I9O?dZ?BM6H1|A)UMKQUOtA$jfV*T*w*6BYI7rV8_O@_c> zdBAn&kHwph81t7Mn{yEiz&B@GdaN6YYs+DCXLlyK9)h~@{ikUy9H|AW&e9?~ih4|0 zOE#V|&+WR`pg1Ln!oG87|=c3tsuPHDR7^~LfuJk;`jlm%5+reh5c;5 zuv3WQlEYs0*GHb=!ibe&eEMd9u(s~8P(`TU{}Q2uefT}~?m5_inF-?2mo)$Ab^$Ub zZ zBsyVMo4@uew%8v;6 z*7?P?O(^rWO{B0sg|eB{g~XY`Yhals@?Tpt4^;}1cC$`{)}vp~n~Rlm7bNHY5x5gP zxDit|%3m7zL_~%eAe!c721C)sHO6q-VjFh;OFoj#yFU**$?RpCnqw>I(x`NS%jlB# z>@Wt7uWUSB|E6(e4%V-=(fy`7;>IsN8H)vO9t&Rato6r^zd_Kl>Qk9q0W6Js8-lZU zeSp1lLROsdqqMvmQw|u<)9oU7tV(M^>SvN9O&<_oW|Eop>t$tBQ&DTWi5h5+$veA- zn`wZ+w~$O4AR7%(NMIkoQ25HP-!9|(@@0;V#H!?%#@ca(&b0FoLOk$*zb5cV1ZB!= zgCI`fgw5RsamOng#L|+a&FdpJk}0&GbKE9M>X2CT13KUJzx5^CgLd~r+E){TkbL78 zUx!eVmGlefe8`Y}-cSY+gq{htcRURA5Rd zI1ozO1lFiF#g1=2UavgFZVyjab&5+~v1Kh))-_Fl*V8<7Rn&VTTMzA;E)tEIe4*Yb5Gw>T7RS1dN)%qF{ro<9>R_@s@WGtx1-Dz2YNXvyM=(iH_wetCOTvVysBOAj*W z&KcKnRfBAKZ7j6hPd!>&L!04lnp-L2$Vga|;a@525hIl}C~mAKb?4%_HF9g_^z%r_ z59+umOPlJ=iCKiD%cI6iqW40cH?aB@e*T_?BCp={EO8>rm>K%Gm_W79EWb&u+YuDb zonBjlQbBucs;P%1!0-5%0+m3Yo_A>Ag;ZeYR`bMFj{TD-_|LDg)Q!!C`(0$K(*78P z{x6-NTesA+9^)iZS+l~=+r@bErKL=-uC6N7IF4>q2ThHf881-G;!DyHA*_Z=&8{a5 zt8mtgWbN{0kH|Cj%h|9kU%Dh70XG9y{jJYn+8!6Eml#1!c;{f>1Ks=9YN=C2OL(3k zHC3|yi85Y1ykufum#)B&;I_qWdjwAok=A60!3SCd>fL%Jr~cTXc{BV7 za+M#v1b^V)IT-kBbJirD`z}9*r*(SP1Xjq|#ws7B6G zLYg_Yt-Op;C+R-_-f5;?GJVZ%7c)gaeGHb$w7uH0wNOm?*6dT@zp6gi@l$BpWO8?D zCBki7)9|@4xG?BTMW}?oBY2kW!JD1=^=0G-hvHFE>a&!jZq!?swgSc5bI(m0hjkG* zd`obP?nX;U6*3dx@N`K<+7x|=XU^8tn=hmKX9V@nBK|#hY1U^1grB|7=^$7$@ak98 zY(7)m@@b95o==A`QgbZFBdzr z;29W_=hejvrqGUuI;P$_O@jBkl{EicYv+LP!{F$l$9Xns*U8xGay*os%U^U# zc=MuVL^hr@@x>Ztzb497@&po1*eOzyPDO126yHAYhTo<&T@6UH5WKI;Wbi-Qev30w z-&omLEXAo=6Pnu&gv*d+p*oRo_`h^x>*7cAV{8Nr-(JzSgFgSQc%(fD=7QGE;ET;V zU-(>{tQ-M6AyJ5kiNCM3#sS;Zh7CdcQkg8^hv64$80(KO<8@%ajRg*Uj8iOLK?X8LEEs zFDSn#L&rOU%M#&uP-cBCsg~C9_)zG$?=(73dFAPjH4vMSCki>^e}p(v=qBYGae!mO zaqNsN+-TP}Jy!!<&K5Ca1civ%RzD%J!CpD82kA)+AzQQ#I=g^RtSCiJ;i7uqF9bzyb+(IX>nn!T(j|7|-}B^z^7?VPy|=)QEyi38sziiUBI zm%>yH2SXYjtHEj^y^`?j*eP&_#rDb3_5qWcThG3?6{|q4c5U0qc)|V^x`63PnDBNn z>A+~;#^akq@!6EXfmEQz`1?OzFgz-Kl20M}<5!fHDWyU{ z-tD{_f99U>hC3K-wl763)J}-3g8Uw5u56NjHcccz=*Ke)O?2sNEW9g%c9#x4M!*)? zW4LThe~q(wM<$jf+`?^&d%d27?_yNi2j37?rRvYaR)hoi+1Hk8C{+jE?eEgTz)*tM zWQzw?I^kapyU8E9+A{5=8ouT)G6Yn5@O3r0Ym_)$KtlP$K1=TnYC%0L`DhI z^X*o$suKy7Ep252S|Ayg`THH{5P16o8C$lHJ=$QZ}Nbwofi*!W&vbw!k#P{_Ar? zjO*HEJ+0MGgYLX5Hi%c9R$((Fm`ifPRlpLi+v*MA2_#OJOn&I;)#-CeZUbfYbdm4Q z{+!yh2+ouUJ=~+TT}ji2Gv1H;5}FNjUE{IM^`T(VbGgWx>3Pt> zbla{1;l(8VO}U>b&Bg)j6s3(bcmvynWCo;|%kqvV@0O^{d|K@!GlCi)N@(F9d}^2Z z{5xpZ_6=lX7MN_%GaFFFf=#(#YLARtF(i;Qi80w6-=AV|ja}DMTELE=`scfzIbdYc z=)YgP9`t%VjWHT)Zf&jTV7t(_DX;~XfVka$-PfK1$JQZA;)L%Cej=+mW}ljK1d(PS z;EzNXlE~@y`tmppc%Cz7|MII+uzK!`FLZr3!GXKI1T8L}R_Y;K>CLBZM4-*Q z6sK)|K{K6&!4^FD(3a~?VNocGR>8#8@n&n{R~qz6m?)3b4v1*f~mS?WN(AZK4oQO())BOLYHTZun+3%t~MKl z`*@QT1^%k7lvp@lu0$e%eY$Z8-OccvV4DT%m#QNtJ~L+y_HbPfQ-DRhSge%XZ^ z^(S z#W47BG5+A%LNU9FgHNJ?9Fu>Z(h@`s=}b~D61ut6p0gb1`{Wbwad(X>{kO2<$id$^xthVxi61E~L(V!F z&SmN;57+F)qcP{&M7R2+ryGPf;6%*@BcBMY;z%{xs;Yvrv=xAK2k!sq&z9wmZP=>2 z-*6whng-vAHm#JOY7=~;jqgGkL*@kHeirB}?Iv(Zn|1S{t7%-Z`0Kq8H@#^tH_q-I zDDhJ6ZbdcWmsVZmo$b57?NTsdG^o+(bRAC^rKJ}VFf^NOMs{1jp`kQDa{}oaT!@yx zhp`r`ED`kNc1k6kQVvR$84H+gFfK<`o|D?G)c5AZ#$FmaaU$lc-gCTnTYAL%aQ~*% zDjMzzZ4W4jfZQEk(<*&IiHZ{5Ng34?MnSMK4=&6~Xbu}0B7*)lk<``mlXw!WOr)2O zMeY`(a-K%B`{cL32`3M2xQM#nz;mkiNYVGvRXa;}a;c;3=m~5ZmDKDP^*QHRV3j6W z7=}+sFv@DVw6%0^+>0EI*xMg6v+r4kBFkog~Sz_OS30aVVRB@H}vy}9-u(hOuM_T%|lB!*TsVfz6on{yf9eK?t!Yy=IDe&mw#=WL`LWv)H%#h}bX zjObYm6Mu`bMWcULGAJ#0h%JU@W}9`J>25(RoDs1-Ct7<=jNlb*rPU?8YRM_nY*P)? z?niFoFf$l4y*FUT9!K#I!84`T99Ea1>Z~R^*e<^7hzTji9@ui>k^amuK?-Uj4F9vl zd$EmzY-am=vLN=b{QXONEV^mKH(7g?~BVBrW!6f?z5#-* z_{F>)c?|@MrU z)idSl9A--!M+pJa@)IZ=Q*H*BCZJ0%otmOf2BrD(tW+b;KR0J04fZ3k^m%3wP=BVI zcGAq#`N!}?f^rLQQ6-e(Ae0=C^3R^;H@0x%hzd&WUK5@NUXSg*`fBd?3?7`$_=Zy~ zI^VzVEm5-7;UjC69Q&pC)V5#dS5O}9Y)f~Bq1I`wF~0$IE~M5b=O_`yOfd3)Q!NbK z=je~9VLB;wlY)L$9}}lJ^k@E-Yg5-y;p{#xE!*Bm$_x5JrSZMwGl3e}kU~)9aL5eHK^JbCAc(Be9> zD~`T@a{nX_LGd}(R=S2hxj zB7_MaksND!;G!96@@5nwTsL>iYk811K9$;QK{3)g*2eDJlRSYUt&`^Q;5Xmh=#gWk@8&J%C2*um5NdF#iFC0(*_VDG#b2qFhODsB`^ zP?ErI(m2bvorR$+C(2MFJdbeo2RN|`8=UU_iX0`j?O&`;no%0%(v zz%1#0P__Ni_b417y?Lfn;Ng~_R=JTe+1l_h1m*)UJ-)4OoefOUkO0(;J+_fN0S^0ys?mv6t zmTg+d>FO!7RG~kEtdQ18&!s&aTNtSN4A9f{NJyHP%&i$OH1spx-&5HoCR*vnwA%uG zAX7H{qAp%Iw#;KUKl-i^+hkvohPsy`mE9$ytH(p4sozpok7?Hb{*~FwFZ$=hgnd}! zx3wz^b)3e3>DE?=c9UcxGyE4!T4nx1JT&HDWnpy8e~ePhA3)q{Q8{KW+F}eevjSYI z=*zL%!r0D7osBAm&409Peh;TP-1irjt%dQQsYOuQLby&hrX`Vj`C%-#z;^ zV-q7k-U^g2pK!R(IA%jKD}PYmT3APvq$sym@5cn1vg@a~oi4k4J=kJmw}-B*F7AqT z!+u+Hxnh$Ti~U>XJ$rRVmCg+_6O-0j9J5XEgchh9SX#F&|nd;-`F|gDymR%qDo8)3gDd63F>>k*Zly%lg zw9fPQdb%F}k=au*h^E${ck5mSE2^OX`e+F!eb%1^)F*&`{1|7O43C67nIeTDR6Q6; z+>#zqD1xOIwyR6WZcBRh!RYBmzDz^o50+B3Oxx!_lR={Z_d&X?aV~x10`33lLCvwE z?;1Vjl~YxWMlcl<*fh0BS8>oX?Q2_=g58p81ARD*PD)3-+|>#syT*)FY=HefmD5MQ*N4f=)up=gm{BWJ9dkfsuNo&lI4K(QDSr1yP_^DVF1vslMr7s_g^usHqiJ z&D{M)Pn^b!FIRB2kFyX9Q`AyQ94<#ON$bQDS`gw_$B22!yqwO`+I-8uW?EFO_gewX z>XftZY(Ut0FbPb=Kqvt4B&3&=89XK?cd10(2@+re?%?4H~}N5Y>m96OYd+ zCb8pQwJ@*uj6=Ou;o1F}He>LCrX*;xBwy{{+>4Ov534{5kjLg0y7e5K4CmP=!iT3( zABdUjjpGTg_VE^~6}7M8rjx-_>WoRQGYX>gtHPKuOVypDU4-gvPyNGij}&ewZnNcw zV|RJR_S?4pCrd2TJg41fO4;^RSg<-Cty#c{`^@>#k=Kx%*#&&qEmc~!s^_vh9=DPw z35_?a&jI1=(|Aj;`D=9vfF$lLv(9nzMtf~;je33iQ(uTStfp&3o?3H^26`6_b4SY7LV0zz~nr-f17L&#Zd{hMtw1Hq)?xWGwGn0&RdXxEwDz#`hx- z+Gp1d;e1!zYd@UF@eLt&Q8mwycW&N}_B5qWMWK(BhAhIK3(usVi4PZTC4KuKH&60+ zPv(@LT!t}%8dT_Z>kW7{YjrU9s$D)=$EgxOPvjmAe)m4UWUM%w?NbPn$kFq}gI_M5 z+E*Yi7BGvq=ozQ%s|eI#pJ5YRf*vM_iOq82@js3HGk=fIrCq zoVe7>y(^J%wZaWw?W>`{sjLqTQVA(Tt%ASt(4~-&m*k{hrO=zMTv9K(IXdP+T}@az zDmFvaAaO~*DB*;=Dc0S$IKOo!t?G?$F8B6%9Cc^19xcf<|aavy1t_SU5gX$-eS$B|Iv2xMft07%z_CQ^0>BTfN>*6>n z2sY9D7X*hxuzh-qcFCg1Z>HGMdYnlS1aNiUh%fA^WZ^wI24D?cm;8Y8OC1H1($xDF+qE6}YEtpGd*U>*m9-cYl=iAKilmI0SF`;m{^;-nyW zu0wDAJ5iR_+>tn87@cUmwROhRS)taXO8efk-I5W!lF_;(d4Ao8=NVHhHPl2jRf>Id zPWL-nEGq(YS4L}))svdTaZk6Ug*4B&%#qN<<}~l>21pjs!+^u)nPgM3O7D)TtJTNTcGY718O^VBVd& z*@qeZcEe3^Ze&rt)@b{7OQFl#97qtao2o|Q&Lps=V>;XcPD{F+ZrO#AXwac>&YM_x z_b!V4;30fvU-tapj6S>BG^iH8#H~)=JXM{@uOcgah(amZ00HhrwY|6HikQ=V0e*qs z8OtB@w1$Y4FH7|G@lw3A^EIJ(sgENaD`SYcVmz|M4I1j1rh}#W#|1XG}YsB zT^yFq4UDiLW0dHKw)R6Es#i1~gQG z$Gpw_S~FDRIok|VF6<0KW>S4reiTG0RdQpjH@uiETVvE;O0%`f) zbBHV*G(4EGSyWECed`#DOiSXHCP=(!&dmU^S1nKD$p+)p-Ym>O0#iKKpDW?Rd%~Ft zcoB>q5xgo0TkYq6@Frv|>)u55jS`EIWBGAm;~kv78s?&oJ9~P92ED4z*XixC4Bg@c zF2imEh!A0WR5oHgmYj%c_u>9-DR#w`v1_)VOn9@+=(nZ+v~w_M*>C&TxV(C6lPH#u z=_SO%qqj>DC)y)ZyX^7SAGsTi#*YjkDHwPmx4g;2+wn@;eQ2qG&K$w2VjO^aTF z_3epI@rp4dVhNAw9IuCD@t|bKF<7Q7AeqYFJ_C8NsWU2$VhX*4>=`d_FH`K7PGaB4 z+?j{UYl{c+IDTJDl5;kI4JYakuylayS46S&6pUKaSyzVsWUChVj_;U+xUjq#ib+CQ zUzl!%Kg2B@e!xgSE~o5>h~8Jr4Y_;kSJ)<{QqN!=S%eclPabedg%)K(l}_7vP&*WF zC#+{YCxWHbpt??0S&sRsrfQ40O4(UKc@qfayVSHgv~Li`FyJS~+UTz!a?VM;uX+Rc z34QP>JJS^2@oz;fU%Pt%_gCJA=a1mU&94ka1Ek(4XxmEqtRC*WaIupT!iS59@mQWE z?V&@wmh(PGJZZ16IamUD-_}~Guq2ogS*;@_RVcqn1K`Ax&@rLOGDIp&G49Le_+SJn zsd$*cU=*bFz7{9aXR`@`ejx-L zA-t@V{y?eYuS-C7QvqD~+i4=FuY;s($lIJDA=H4OTKVXXt)Mp`P%_jzl^CW!F?YgX z<2A~V6F$Dj| z8-SZQx`K&@W9b`p-}f`Mw@PF9cH8t&y8H?{99t(Er>mj1lb(~vbMTcH3*oB0*OB!X zq+q<~O*lQKXSI;(o3KbO0Kygao|Qop|9eKI?5P) z?vQ7gw-JA*NEyzi()5y1dWWy%rUUglQbvSNBam5CJc|I-Wp_!MU4z5v*|8UINjaJr zlLFB>L!$<;4E1=v)T^TrK~)*b!}6-yJqs1A2BBC98rIDhyiZ>GW6pLaK)t)^zWjnv zq5(Hq=%Y=~zRy9(w;U=Ge8z)oexg2y^HFUSP@CxA(`sslwZQMau6qX-=`^6T@!KgQ zAz{Ix9!I3mc)=sb!06iQgzJtJ-Jq$P8G|THaw{hHT?u#O2=+h59B^^j*CZ+$V<_KL zbeppwf8|(;gmZDi;0nKsko&DSm8lEbmWyI)XtRHWTzuzWwa0-reN4A~ND?MrKKMR} z8+}(GoSiK|vyA-B9)k;}7uo5iT(bR0Q*pWIx`Nm3l90ygzF#xiJVqxFTW#GP+K8xC zUGC~pzl9>B>*R&6N?mKc(ZeVBUQoCdDJsuV`(wD-qRO<3I%`Ah=fmA2p^HRy4b+H5 zBULPH%d6xdDj^r`pbA-SlYxoQ*L$=4-UqImNiw*P-v!Ten%l$5HEEmZOn61-*Dcd_F{WJoquGce%k*2CoFt! zng}TaT{MkQ&s!B0v8W0HG_`Oi9bI_8#h%6v={jznt~)!NiP3TPHd5>esX_H&2Er}R zheo7T2ssqIulv6rMgg^+U(}^Kk$;BM$BK(w%9>qdvHJ{}#8L!KC;n$39h@63uVP<; zcU!JU((2$|$T9aFzwkM{31p1v$IpNKxW?@BbFmhd1k2b2KJsdH%|B(f@QvoW?m^%K z7$g1jNKCW7pZ@^@nON+@zQ%tMpags~@EUrHT}H7(qB2KoyKl$#VGp{qdUO9Wf)Y3o z3s1@3Zvf2c-jlX3j)l!M6cXl@7dr+lE8@hR-lW^)>N@4AL2DNtU>E~EZC7ox@gEe8 zZ>_MM{>F2`msdW3m%A)A6QY0d!INdzJS*_ExRiG0I)tKEHHcGHzy9c7eI zxcs*|MwEH|TT!t#1D~f@QYg>TbKvm8cf;b)BZgabdkiP=+~YQ5{0!a~qLyD(Gptsm z;Mj-SB_J@goTd*((znkroXcsTmRv%L{XMj(SE=^X=-VZ3YIB>) zz?i~4CWY(sxAd8Mnm;k%*ghq*EO`sib_AED3^r3K8oLvB&{OO^r3pV-zI9LF*grdd z5Kk|WJ5H%OFn*_bsV}T5t3Jo`=7<)thkJ}PQ=M!Hen~~Qe7M&=b5}}{1Q*|(snR1B zi$@GPg%h#u?}ItC50x@vl_HUu&vvv?ugq?jgbw0c74Bc(cVg5b901~(IpfSuz7%Y( z&zckC*N8@Ntq}e?%rt=aCCKL9WA8Wa^6j)Q`SaHBCo8azUsn2RU{%j&vb=E@A$qE101<=yhtTt??;*8*^0cbf(6xxiuW}M*7BUymqSf<5rOkt&z8ONyWYj%KT z_Iv2&^UgmM1OGrg#EMzWNUS!?&r%un`v&gl*7dn5ax>>Nn)f*F{7Fqp5NbO&`GfwK z@3cz*!ymcw4(y79CeVF7Zu45c9qzH6nP^+&ZX3c7cO}<}Z(j~@K8vsfNoBK=+k!;? z^2yTXC-b&}0W3TtPp^KfA~mW`s$>P}9K6I*mEYL|n*QOf`s1>NaIhnNp{z#th}fssSh-uOV~rk%v06SonOy*BKr^hV9nJri$F} zF{|8j*0=F3aLqM(kfxU?eqrl2b7Zs=Vd;YEnU?p55SPwB@7g)E#p=^);4N5!i_pFh z`CMr?R>}XkXRmMhm%sCE4|K+rct7L2_mb#50(`t4jopNFPIHr%PUbon(V!MU!P1pB znn7NQ0kvnlZefgB;d$I;dG*EO00@w8FlG7md8@@C%EA@S{!QmP4>V3ZYA-X~fsYD# z##HqGu0&)1R~J8X2#IBBd8o5vWkF(ujWsxG$4S<$; zR!@%le)rj@IO()8+@k)6Tt(OY^&Y+`jq*rr`Wl9=9#sa<;vT|;v!o1IKg|2dT z`!*e>PT<%QN(*f37YoyXRwoDE5=V$oymlp}h<0VuUpe;jsAsf4+@!;TMUlwM%!N0n8}2w)rXnJDIIqD6^%m#q0)I?4WyL9+s~c`)~JF z$00u|fR7oZi)XXygbJo*qzUT?L5q zEr0MTRZ*p@_5`-;NcVf)y~U;@ojwzA5m4Zb1q1tMsY{XM=Xtn0W&$+6+Sa7efgqius)gyzh_LV+o~ z{QI6`>RP{8Lv7{xsBxiS(d~AhRQFk6BJOzv4`q<+uA?STSJhN881A--#SEwaW5SpW zdHv)CQdk;AteegF;{^sqFq}>Ym9(+eEIH*PK{5e~=xU(0e^5HAu}P3_i|4$8GgYJf zOPpR5v<<^Dou^rI8LLB*q&07+BW9}pn0S1SZns9KiN8)t=(HkNF zg{|jyB43^ECxp&|_EO%OW`bNMZ9#q=s`+Qzsiccs_)>vX$c#v`|MxdcL=ld5vR>zv zVl@%e&xpMs|GFcz4V92UoMk}Onad{iUahjrEz3v$Bo2QN`S*=LqWMgJPO#UMui43F znwC6Xvv;d>tq06OtU0jsxt?ozzwB<1l`(}|uIX3=&^Id!XT&kEyPLA6@SrOvzSdu@ z#Z4g&u3ruc&Pb+I&C?$|A(dof?n93if2?L5GM(CEcXv&dr7hhGy-JCF*zj-A@71ch z+>$bN9v2R2t#(1J=W*n8F-B$vdNiZBKA6{Lef){iyh-m}C4i;RcAC@EAJr=vowTfz zJp*m>X07`Oav4t>?I~O7%6|(9Arut_AGcDC{xXt`ATw!>R2@(hsmdf9$VBv~Ie2?x z#g-t2Jiy1m~T+eL{Jus?7+pkWT`>qvx4bO74<`O$1`O{*6Bv|O6b8<;qmecW z1k6VZ!rpW z@rnXWP21{`c1oD9*w*RQ8;6&rLt4m@f46*DZIyWHZ4L6@uF#s}zfVP$ggcRrzR^=p z?)E!prR$InhD74~0j52z-I*(!TmzIibFof#wn^UTyb}a$wx#_F6A3u1c;~_td>~U(@ADpyx5q?suLuu^24^*ec_`!)(zxxpb$5d zg*>+{{6o)^Z#r7FYg;b2>G0TNkjI&tY_MmjHZh0XLS zQrrA~TPHa!$gnv+$Unq2a~`ndQ@RJKZ{QlKzp$kZZrne=;{#FLtp&DCr0070y{=2U zav@1@T`+%?mF5Asr(Ia<-!Iz0r#P`M$=b-!;oJi+Ffi|Pswo81K&-mUCcyF?H#{QZ zAs*C5N3Q9wQ-iPePDmi~UGMF@f-O+&PG3K@Zb9u?OFmUJs?cbx zNwXcWI{Mc6^R3$<=e|xw>){b2PxsS=zf(w8C?6ey;@w9_PVs>I&?PVKH9hYjb`Z{a zH)T!hMou!ZxFRR9&^}e|nO#^pxAXc~{gnQgfCn539yRd36AUqnk-uU;wg03xL`l>2>u3i@@b#;;t(+FQp)u2f9XmZjc z`jJY+5oyE;D%1(08Pq0eliy`j(}sfU3dH%2LcOpMpY1!<%I$|e+2%7Dzdx=!<9O$$ zod>S_35O6oBdU*|+IJuE4O-0!1Nf@G@<(%0c$4M&ILr05_@yinO!N-=y2KUFxmYGX)zSV$RjJ2sNQBQ$NM}x$A&SD9X!D_e1ryN+Q z26?^3Ft@2U#wwb&y2CQtuM|^UVPS73leaE>^7D+hv(n;4Q;~o`A#zmD*B0lRj(Hh9 ziMyJ7oCH-(QhRwxmuT&X^dF-!Z<(SV6{TQs(~6`;oiX@0>MbZVE_N0|Hmp& z!Ur#PWln|wsXqqRb<=AsJ>T%NS#eo#jMNXY%9cF7^cQP8ws89edBE>%QN*IJVd}0S zn7$0hi0fs{cAO$-0)xy1VuSS2zYL$gumVaY$dQGb9?{A*m)NITWpUjX08gxqjQ*G8 zB;OH(1(BUVA2=`#6^wFOwh6A24GF@}kf$f+@mX-%`lQS+)0c%jiX4v_LoCW{(=yXM zVJaMp0rhOz?~Nnnz77-bJt6fqt$UVD2(k!Cve#W((1R@XHotJprHnp$9aGw%ZhX#$ z!W^gd!*dA+2?k_MP+&hY(dsV{8Z6NBzkO)K$ z(WCrH0Y6~+AA9b2AJITOW1=5-n|aT;{^ZxHdCA;q^7lgtr96r(K+&T@8mtj@3gfu+ zG5WN|jDY58Uo+vM@heo2IQ&?UIHWuXQZQ7mpS`si^J}{A4Rtp=N0j|P-eb9t z;cO)=YW36(&`54%y8Nf{%AbI4gJ0lk|HzahXjU?mtw2f6oW9q7*584j^s%>bW?GC> zE_S%QyH9ArT8Qv{&~!LI6_7(*k(hcbOHYGpD{17_y{~MmCmTr_tQO<^2zi0jvD1Wz zM>J60{#<)eN?b3CLTuj5sUt$-hkCU~ht|tvH9_dH4DrdmX(pc?<>ZFQz(WaAz~hi1 z(&(yVLu77)l-u##7z`RuK;uqiKX1pSY3QTzShzpuE-wld5~v|MPzW08#H|)eayNKf z4?5!5e}NS+Jlp5=WZ5lrrAIOyXNJoW!9NCb`BS@(Y&KL~URnK@AzJ95jmi_5ANaNfVDRrVVx11y_-!S|wt$lh^xuZrlQIM5{P&cKaWcj#;}Hv_25v z@3E}!WF+qxdD2^dieE(*zPoLSg+Le&?At^oPEoz&C8}2Vtoz@rAP*oFn5dE)bpR-Z zTcwF2`F|zJCa)>6y=Yc{&mMB=E@yzR0Zk{>Rus}lbqtz)GVOi7&wR4ZVpe+Ng8LoH z8TPlHcI>34*1bISMr3N2NY7Hr?TTyI<28sPi#Z^_`;RqdK8J-Zkmhgui3`om$Gs$V zX|jrB7KL4jh{Ort@s_+x8cQ$Q>dzv@{aLc4_%PnL3?1mcIVxS~LtXg`CcTZFZzHxg z`$g|T7M%bwb>}+o%-%P-HX$|Ke=Q|kB7F4TCkS?St{v|f^Sz?=3hrxF*d7bMja?9w zt2-bVv;0lg(Bcn}PuBmsDyaD9&acS+5M%xJz{x=XN^eKc8p=J68(p3mYa|qMhN*S=GCsc!EHy7{11OvW#4fj$0HS!x(R9P9kb96c1b z6{+|e-Za_Wz+%dU0^xx&MN$SX`n6OwQz;L=$N13GLS6cXdD!>@?h6xrPIpN$Ee-5K z4*Kd&sX4iFj*mc)`?k@}6Es{al>b?l*YRzQ=Zu$r1n*I=BISnO{m4Mra%GBGT704d z>TDZB8eZ>j*I%-Xh{dl!*0C)s)gUKCcbV6C?y~0l)1`tbGyI+S zWD{DtcbmCtb3rSaGrs=jZ`pKoli~@WXOoErl$_p4?s_o}cWn1xiwFkY>2H5r{KWaR-KrHxpb$HcrT^Y^eY4p*B-~M0>(df}SiL9DPYz#|vw}7!4 zWDb5^Q98oN%YEH~fedxpp@q7-I1l!Gd}t|M>tbMsq{^9Vj1*|LdAE4GcfUf#y03AS zX}?*sGziz4E4gyB&H!f9P3-vY{DOUcJILLohq!RBbxldkdOpaB`=YDkXjHE+qAiwn z^&?%H#Pi{vb1Fl?=b(kkEDt{QKEopj~%qxP5x|^Vdfi z`S9s~LlPWI>-*R5c_qoLuRZbL{?ne1ow4xm0Ey%gwdqdnZAY8eiMqbx9o2lRqTh@B z%FJl+?ru|A4qdeKC+3GtbPHx0a+^mLpoA`YY#+rj2c4Mvg*nyy`Rk~#Ffw=}^+WNT zEtBWfP#grsg-E{7p?sOdy2Kw~aDl15=<35gti9hd`#$d1VuCEX&e%z8c$CBrN?cp7 z=ecJBCK~$i(_zy1uG2(Wh>Tq5+@IM-(8XU^ODH>dXM0)^x0-r+J8Wc70|c+H&9|F_ zFpmbjp)!Zt8d2L-r$wyp8bT?t>NXGcR-Gs{UpALHUPO12XbZofzz|}4l9XLsDsDNS zC(#i@Y2n8SZTqp74zEe_55D*&46CY&_31{V$=qRLwZA@qT6FHmPEg6n8&-ZOnGHGv zrwp7|N{yu$s6np$nLf^^N4>{3N&*@Co6mrP0K@U?MkFjW-D_5@b_e#o<JK7d`-{p#?pH!(_1oX?KdK!wt$V^f{E8?BmRW+z6@R+kwk0v<0#4fOGB5ZP$sMVfK=+gwRv&nlG zofm(J64DqX=snDi(5J9`d^~7)6?ePoQ!ANycsFwwD=CK)0VkSKslQ(Y!m0}*gk+HnZYav&yR~)-&KU+#croCz_@2$z9@z|P61g0pf>mT%q&2x7* ze05(;81mXYFMMQcNQk`YLj&JZ5Vr2z&Y#sPER{5%f55{-xUUF1zP;L0(1>p9#T%&0 zUye$xMpFrNo0z63|80~Xxr4%i`$9Lcr+Mx)(Q0^?rA zWM!QuR8^-kRYX19*q!CZ89>|;h^iMh)Q3T>#-_scpT+GIVD zyumnA$@5Qnl0JG2y&31$Vt+}DBNra2xco#)({zDDXrynFEB(yy(#&QFMo%p#PYRJ# zx!|+xsPiE~hC-0sgzfD4Qm`C$_=LyrA~sZe9oMZvHTwuL>Mfp5?4*fY_M09c z^sLRrdf&!g@3OYW*t#X>Q{mI!RxwQvSeMilrKrOs8*&TSJ5UT3hTB)Oh{V>2I1{_L?^CVKIR^f{2ORm7K7- z&e{Zz^x8b&K+j3CtUX7KFlC3R_hQ1TxhSb{N@bcvGZ_hX%=r;C6K)(QXWEv$`Nod`y4Itpk<$ zcZ5&3p$ZZ@|1Xb+|ID&a#&B8y_SlM;2f)DNtQMMP2_(yFMj_B>si7r|O%I=R6`_Z`_a|P_HAOh3z%fEGji{H#6?Jee2v3bPb3Z@y1fy_&o$wwtcIx#q220j(APF+ zBZl&*DX%%kt0~~Oxq4uFPIOUQN@u!FM%6xyj3977y{*tB5a89SjPWkKI0p-QEK+3a znw=R%ASmlrC-VqavWR;0x??|II1u(3l1UA}h)e5#;Uey#HoD@#Xe^5jy_JTF+`Z0l zh;aeBOQ*|rqstd3HW(rN7WQlUG9ewq<*o zgyz)08aUw>i4lQn4{$xTZrCCJh@BtDUv7ry%|GzoP+L~vp*2E&k)v8{^z08`?nwQb zhEBEdo-qyO^SB-Ti#|=|$gff2#WnfO2vX+Um!u*wP>oqN;aCpetcy(O3vm#j1U#O; z+(b;%7>|R42V4IN)FI)FY;21no1$k@2nJvK3q!4n0K(u2eyY8!5dQv0T{#+&&-ZPWqUr@sro%7|-(BmW(Mj0RHPH|5rOd+>!yl1{| zrL6$hI-C0kpfT1|%(gIauA`;!><7O(9E5fqyz(miUp;MXd|hJPS#{h&vGu|oaX?uP zAS^{pt8|}sWeiacvpPHT#82Hti`uT{@3JAo&djBy{PV(M=z)_yKKR%1$(`VWL%6*C zJv5$ezcwrAauk zZo*5G)A(GPnIqL(3s(|uSawAOaiOBJdD-rVv+PRRjggcxjBpskKa#Yi1JCIZCH5$Z zuw`PT?Ofw*>@#lv+*^|9o^U4HCU-I^v-{}LzlOulKJN1FjlrlptF*+RV zmYdDVQZT$LhNp8KQ_>|^cljSd!3y=`-dI?!&Xf6}cbH2b0)d~Cme ztRb}eQm6j)q^2^25wK|w4gOhI7*4?S zUo*2KvEU3bKd!zGztneHoXTpNm`tLF*Y@|&G05r(zcqqt^zgGF+-|*G>ML_2RKylOfX!aQ~X>8I;} zxy`MavQAV|QI116jHkDRsMy~JNA?t(vYE_;HSgTftc8zE_w+;qHXK2hCB{23XK+0~ zS1CNZ8{_Gbhy_kR2!uQRH0~YXhYhS==fnY7z_*sZ7f$ub6WY${#|EH7=ih4{nmT?w zvFSf}k|UC_ojnE+`F^1Jd!2Pu`fMjk_rjpx@=I)$O@S-T0v|%kLzP! zg!)p?>p~aeRIQgM{VedOI97?{J!!ctXMRIa4;eDC1HH#(r)NdY@qJrI{Jr1a4|d-y zhrKlw<)_r{aZoi%ud*ljR7$ATf~P)aU0bSw&8&JM_(%Jy>U4KU>h+Di&DNMb5xY0J z2z$Wm{zy0=2lj8yNM+k}ba?N?SNNN5MaJLP{f=0Iw!^O=Z;YF9g9Fk14gT9_ISM)3 zp=c;2Qb+90AnGsTM2eTW4B{tY57^$jWr26;?#tdmo$CS)r_5(}7YI@tn~ z#y=30t=Rg|s$+<8VFAy7ngN0~iMu~e*!_H9d)jQ-kxAe?$RWRTJQT8<2+%i#iAMhl z4es$hjg34c;7rw@kVFYF{j~CC4OJ=6y@LypVB2D%(Fh-dFhoqL1g$GYS$h{AMk}}4ols=Q- zcrnHgDw7Nu4Tc1_H=DK$^Bg%oF3on;x6=jnl~k#}0o(0+ay@t#h_(`bg4xbQB=C7w zrGWBNLWmWFhQmGzbyJDwXM1)JA!E_brUQi5X8&N3IReQWZq2Xz9tMi=TJ=e?LKtFw zJx={&>|HlDgh%&r)u1#^Z{AskMu6f>@FuJl)u{+zoF{|CskT{ECbFZQU6Q{Yex$MJ zv8+FUQ#8_;l2hO$0^|T7y5)$Is)Q%XQV-$H>g08ObYAkxy6s8GUUhU&KYP_HvhhOW z=*6JM9_{QKk6mcw8ZmFWj^`Fz5?hA2$}13ft^7Shgdjb=-$R^Axi{r%4u6p!zVTbC zs*0~SQ8e&d%6R^$%s~RBJ`8=Gv0GHi{i^Emv93BDp!)k>+*jD%qHML;-iq4c3i2U$ zOYkCFm#}=|AUMv+uM25l8^ThNWv#A^0nj(oQKG37+tC<`ehX3ER!5>Ebq`Nw(-int z(D-QMW9{Mz+~y5!W;vrMT$*clG>z2l5Cydc`p_Ob(c-}YtmD>;j((QjTqOfTGk)89pW)G0M>j=cyH1#o{f zHn@2rB45I1k0F%xokm6?18w0uA*z=(9Hlt9 zudHJPoxPH**G3i9mRXWZATMZGF*0a@@ALiq~-*D92>nu!McQLa|35EA%*ZJ#3 z1p8M>%dMGsgYEAg-|N6INtD0a_f{uOP6%Y`b}XDqeBAiP)u&qw!JBLROqF>G(rCl7 zFfCG7FmRBNtA9cc0-sh~jdQ51N@~&Lpeh|SlHd8xUtXHy1u+H2s0Q;vQMHku|J&gT zzOWiwjMx3cboH&pHKdjCQ!!UNmAqbURYeM8lv}d@~+0HtR4ptQIQr^cY zN9y18O|x+khMhPec*cSbjkRHbG9D9#%x)SdwEn3??4T~?55;TT&`0x>4K&FZ@0xlt zy$%v^Izzy-)&t@rz%EvV>hM!B0^J0eRrQy>nk&0%2yRt%6yEfhXD%RB`6GmlJpjQn z3KdM7lT-Vzo;5NP=R%8$CfxILo!V|ib>f61d9HtoCLE?tUIVr_y;S8mU%-4QzyytO19t2C|PdiC7^X*5rj2Y`YbD1sv!58ng*90|{GTcyERXWrj zy4Ws5f%)XX{UJl!R^Uk&`+y8%53BVIA9xjO4`>6m0S6%2b0rt19X#88D#%`*Mrc*e@N; zK4*a7&D}3xs@_Dn@3F`GqkNv3Hg^Bi0zFcUt<}8+j<(Z!(M}IXEcvO#q3F)_()|~H zJds{9am~g7scTfo6|*1ue9vs6!b3>cXF)RA>D=qkW?*par142^RX8`Iu2O}qW<=R^ zwy55>^4;C zmV?S#NR56J(phuRM?(LpOgzg_(jDVvA{Z-uHGw|sqZ9{uGz59{PPJ^Y>hJ6|ksHZn z1IYT&mxrlrb%dZx3_&@o+mPN2MyS!Lszn%u`{=_>r;lKb$5XQaaOs?>`s(R1X5(rI zs1r3;=&TgIVySO9O0xGQ+u!_1cH)(tW+K5x+pqsQYkOL4HKj!V_UnPX-!Er$QC>vF z`w#C_Wkj1?Fi$ zC)Q1Ml!?rAArJ$k1mdtXn>*0;E=?S8-?P z-(`^C=oz{WLgr2Qk_A>*} zVl!Upc;a#8C>uuZVS+~x8ACh5zB-M$m4+*+)ZAcjh&^0$C!>RCkJEFf!VZgN$?I=0W{|efk zf^oQA;4Dm_nh4d}_qUchL>Aii>vGbp--el>o=4pj2tQ(OU3K0;!xBmC3}VG2HX49Q zWXL+z(nBIr4!NJw;)bmw6yPekkS81&!jtr}>%)#dEhpr_j5O4^KR9Y`S8v1YB%eXF z=K3S89icOABu{d*g&?UX^xxO-g|+;w)Ia;e6BxGkPxWC0T$=a;V>Q3dmefW3KS`59 zL1V}$;nS{k)IyqWP*B3<1{JaRm8A_#+sqE0MEY#S6?wpEtI^)?R&G#;)a_z@l%Hn; znA*l^@-Hh1?s2A;pVeeJW|p5btz95*casXZBQkEC2Kol{+43;8@||w1A~BooGBeXe z9e*$AzWizVlB~A`ELv+Dh5c<%OTJ=Po4@;AAu6z37P0T;38Rbea^Y4(eD=JF6=RU} z%9Y~#91sRe*?nM~%uP^)CU`}|g(%6bT56vD6l6nHxX9-lHk8X2`q0s>ihS|u*_IS# z`-f-Flel9Cx&y21ux8>u_AYQmU$+7ZY;ifHBJxlOjq7REY%w9QFgJ;%-G0)lE5mTp zb8*y*Lo$n47(MnEG8PcPpn+-EA#-qPYK)BD;A3~#a2HfPak-Nwn<$LMgbWD&BE&tK zQHD}Mrfx$t*^OmU2O5oKwEi^XVpOx_oY{bb8mDgOk3HYt-SDsUuEwZdW0v?#8_5Y& zTU~1wb4P{uOBDw*>!N3aMComF0fPrJKINB~mO#C7?!OW88u@3by7=|Yl+hnR@BlEu z^;MO1lI=+_ENKd8Wqyp;8^zgnsA+KtB^1)_&KO%~neJn&OrNaP_xohl`Vqdc$=^ZL z1fz|PweJdHCWsEq(SkI^t>r-27OmS#f9Q%e=C2P;*9N)cghJcqFSz2uO~&wmm?%%> zv`_k)1CW||!*Rg#Xy@0{+NK*|{u^P+U;Xf)m4DjR_B`Gx5}HZ`X+P~G$dkkRa;@z>35v)5xJQ&Sidr?)-#Z!UoqwOmA%qD)$>)Vj$& zniWXxnogV|JPI*inMFo~6xE99{VLun>ZCEIuD)o=PN#4`V)cSsHXP(!EeU3}iCg-g z4MWy;Xr1A7ibfwA<4?Wzfr^Ny!q`wts?mh`DjUzXasbcXO1^*U`)||WMPAf_nCeyp zt8HAUJLh?&vfzi`A+qAL$R9_Jh!buxQHFwXY^8ro-y&Un8L;6Y6VB_k9Q-RNt;?)4Zc1^jIMsww)9=ev~dxL#n-VlnHh*l z+{~eNeXB?p1^Ne^wjXX7X-wzT{?gv0{CBMCKaFJn6lLS_nxO^>IW7*t{CRMTv$5cD z3=;I{ph&vV_(2+A>Src;DnUkypq=d88_zpU-r`m;+#G6r9Q7q(1a}J7&Gm8LC?d}Z z-|KoGmf|UzkEu_jx2kSWx@Lg9X-zb*b_{zs;Gs!BwctEd^}kQ0})@ zo**nTLaIVZQ_lUpD=!bJ24{I>eLi`Z2PPrzA9N^JdC{E6J0ZhslpP}daLi3}n06X= z=h=#1--y$Xs5(hY;a?l4)^dSH!vrJEV?11%bMN5Cr^hbI;CXQq8BPsn1qhOo;eGU? z`DKE}+J+K5DsDNMn(gUIr&~?9;4mjzK|+Q=aEglHo~Ovd38#lsXKn4Wxxvr^`p!Jc zpxqw|$VR$)W%5`z^|MqU6=mdc^Gx%1clV(Izu7;Y&tr!h^3d0ZTOWeYQ#Mf6T)#c4=Eh@KR2jAD6evzMd zR)`S0t^GToZ&`tVR|`3<_G3clLD|~(G+C%2vQgNB{6Q^VwZ;yFG2|~3-xgGhchOB? z9AR`21n(>$AyhbG+EosSXo^O%BYrClh9#i5N2(*wwoKWd;RY~PyjZQlLbK~zUaxJ^ z@T7qi;^LnC6BS)H^YI$U&d4>Guwo6$U}q458q~i(^guQT&}O=XbOK_F{_Gi=y*jt+ zNsd%|ec$|8pk<%`f+YL3K481MGE$z*G*T4LHrSd}Djj9- zVSX{|u-aS7A#t(kY#*LYc$aQGB}2u>ApcwX9qltyW}U+%&KQnXP78rn%YkfnOxDPk;;2g`K7cdF5$`_Lrwd+U``D${+3^IH$d zdE_`;B;CIF&$OA6{REAn6|w!;#B2T3#1Xw_DZ`ZU-*caCp(saW{#{`|2 zfZ_AT39$nSJ80(EK$vG_c9swi7hYRC_(?*Ee8A;JW>O~V>&DTFo=aAH`EY>+J`h?B z=iq6Pbwb5jR`1)o8pn2uva*DAb-6R*519B#}K@OSNV^`)6|M`vGCdotMwl*5TQ*g2wd)G(kWpTFF(z>Y>J$Z z5Jk%}#22-%HK5-H2<#Quw@6YY{>30K0thin>W!I#;(xI4Uy~BMy=}AR6IxMjSu2_?4@=Yu0S!=IM{s z7Z@*1*x=&mr&~h|#SuGP$+D1oT2@#1t?bxqeMSKa8m6x98A*G6TA<>A6IqV@*zn~p z`ndCh(96GNG>M?k&k(fZtwWA;MEepOS1VngS*p_Eqr8l$w2~fzR5ipsBBLSwJ+_X~ zH!mvi`FzdU(*wbOP@DTxL#FV%339igkW%7C9guZp!<|F%rDe$dsM~Yhk z31)905KP&LAf>-GUC(YnJ_i&onqSke`5H=^h3XJR*6hP(eUWChBtxQW(-|L+Ens>@ zJ=#$10K}@RHDLP1v9MawR?=s9thU0_AC-|oWPBJjcZ_r+m4GDfT+74KPt?{))kl-C zJThS_lOj4E=&*aJAD^1B3*jRtBx4xH?pa9^K*QiGUPtiTt|9TJH7O&qBHrtJFnbp4B{JSYce| zZ{VC<*a`RSeud|a4)NJ~?D+qLO#Z2dcC+0WbBnGmJfX&cjvC_C*JTKVY_G=yugV6T zP$d<}j-{9zi*E9X!laVdI|7lDM|n)|O~hu+wUOm>dyKmee)6moj9>E-5h91NMD%K$ z*)1`(jr3%#)Xe|02y3Ok)s@3AwkKl@?NOz+ZCuaQ^fJ*lyHNPtVS$TN-1lg;s^$YV z<2OC0dpJ6}F@f=N`86=vNXB~dvj=bfmHX`r*;a%wjRx@PUs6k)D4<(GyeQ)<<$uR> zMxO9v?q+dSd1Y!7uVo2^_exhhxH%NKZZ^Os1RJPVHXf!sdr!&U2tR2j6vzdl>$M|Q ztjup5lb0Q(!Z}PmzfIX}a(w?sin2SxZm&iIN5^>F?lKWI>p~x+FZs9XQ!EyDEd{zu zkwIdpD_erU_r*RP1WN;Z9BlgQXb4u%vrVzp#FdYwpqwKbj33`jbEqc#ES^JN9!To} z>oBnyG|9VxAvmU5+wqbh?h`=jt2uPvYZ|@qk9?o4Xw|U1#eIjtS%AY@FdvH6G&EzbGbeKo}6161436`mUOt&|FR4!3`N127<`0P)0!`5Nx!v_}-tZDGns&!9KZW{z^Qr2X_N$Pk? zpu2D@rpx~0WAi>g&|qH+);bp9*=i_3w1T}{12fPlrSx>!o+9wyqE4#!IEl6UYTBcU zsp}4vM`_yjzgSkp--bFl5JEUOPtk-5DN^0usvNK-M`qj!TIbKpvR!PoEG{*7X&6zC zo`ivhtR9mUgZ2@6v$*L(2E<7IgLSg@2nc-6EC6g)nX$?YgW7YngL#|+@t5<$BR>EvLn90g}&Z4-A zwX!Tw;S6URFhB3;FviN9zaKR=`2H-y#Tyr>n*+F!sz;P9;djXOC&Ux^2V}Z=bZSBQ z0ombEIO$`(@_o)+8z;b$1U=?BBZFI2mAvM!+!@&0`t`+myZe^Uz2c0^l37zsnuwsVHY ziP?bI%0VU+4TvqLCRu?EF4BJ-i`@RpiLEUObAM$tC}8{k;#M11sKf_tEuE{XclgUk z8)Yk!OHivoo2`Ljc0eZksP7yEby_utG@dr^g9MK6YPTHut67(^>bRz;#0~~0>z&o- z|7BwMu3ax>K<`jM~7!kTH`5&%3<%Ad_=AP#qq znV}t?<2S>x6ra=><%W5pUmW5NIy}92AQh01WQ1BZ&*0aT3K;%|Hb+($4DV|jr;ShW zH8TreWWsOnU?0{D_1&-JJ!@2j;BYKi z=wLzwF;_XfuBjBmV4#8HE=>lC-0r!HEdvH^+O zV*qzQatnb@yh{-gt_FOH>|*DQ>%P~coEqMQY8&>unH4IEfecdVZ5w*5sTksy_e67@ z(+K{ln4_G8t60Icz`gC-Gqm`h@>o{9QY+t6b{q?09E$Eh<$mR2#HDBy0<{DV{gQ0* zYgEcKCmMwQ7U7M4`AyjP@5asCKqt=x!p~{V+VU(f9QXM1%TKDa!^|w)uPH+R4OjCggb<9>Hz?0{fBT9^jH0MY_XZfhf3Gx%#wiTr)+dH=;9@w>E6KvCQtOwhyNgz zW*AzvkFy)4f7caaHeF4YxUvTSv6P<57-c=BPkXlo zk?-Ux#^e+?{Y0@P-|do*K`ysR*82A1&aqa-uc>QtISl) zg|qWtyTU%aCRudDk|3T^GS;ognf>-ah1b}HeV$m4AT98AXZxZ%y1F+kOaN4D`p`Zx z3d(h(V6572*}R<*eTo(?C+R*s{oxKIMCYic%eWECy|tO+e5`5hdA{hlq?+^NU##R9 z0+4mQdYlWG|(}rFr*pkBqRaR$10~{&vSavX&N+K0mwoyg!M#EQ%pmB4-Jh^h7;q0ZcPCpPaK> z&F4Z&sT^<5j!zdAm-E*8-Q0hUZ*!hzge0B-2LOhraTQ&K*a-rBCemi#=vpWhCJ<_ZBH17 z3hSAK70sBf!EXM~RhGk}9<{3FKhu$bsZx7hKI4wj205FPq&mS(yW7?OgA2kiPkZ|n z`k3EPB$2qCJcO39RM*F! zZT;t`FkE*9zTp0X)X2Wy-+`vr0+@`7J#FUb|iTn=>fCru6itg-AwBMsg!6mgc6h8+Op$VtM4=Iz!CQK! z&rN1a&n?*6`YI^ngQqxZM|nl5zP_Gz&gJlO=7uU<1Y16&nfs>F((|}mXo9(q`(N~; zp7Ofg=ct~LX2U(Q$GHH7l4Xj1JKmYt)6*3Dwh?v(2)Y$uSh$i*7X7!G=#ll9ysX!N|IHC9YG5Z6BX8*EwOert8MsTX zI?Q$9tN1?tG)l?7zrbX z(kOXGnykm zYgnH9Yy@SeK{O=OdS%F@YIA*`N27z5c|c3Ff9m4&>-uh~A=aTmz|M;2Xe)7mz1pIY zsW358oTRdkEnQS|%U-d{MW7;@Gj;I=<3f^D;f2|-s^H*H|A*JPe#bh0m*;qWucGs; zf~`d9vH&reyIN(;l9NZFjp@T={lcr3zqaU->qy}t9_2hx?5(=%ksfwkcCkvxjsi>V zpr%}ToHU;;yMzA=ho*Q>5UB%Vi$wA6ei&N@Ju}?vCPq`Yy^Lb2Fh#t`OHqmIwU7JWlF{onF%BXJ*`&} z`yeGl@U<$+bDtg>UmuA+4fNsV*w{_{)DaP0K!+MinIIVH=mfVrG

7{;e7ndk)9G5S(-1eN}7EC=%j4cTP2X{Q;REHGRNjM|Akq z%ShrhYd%sEVn@|a`Xy#q@Q*_8!(q+SWXLkgu3j6#<%WN}ic))&i){T>s42DYKJ!9v zkl=}pV!#7Bt3P(BMya@F?u!TWEPz1U?q=kOJWlLe_6Nxdwipk-v^`lIorZPXW^X%>wRR|aCbw!tvY)K^m%8K#Cyq+ODLeLGiR+SwoU*FJi8RPq44 z&Wgl7%iZsyn0oQ|15dAzorwo)!+IS@Ik5vJPU5KPf;-LEI+d1RyR8C9qaJ|lzo=V2lLeld+CC^}{Q7ZL|Bn(-5^7O)q{n8u0bgCd)-GwdyBD+<;3^#CG3=SyM zdEq_YhJLw$o|Iz}&O)xLkW?X36FUfkszGe*I!m6cq9fDOlL#d+j}&m&mBCJNONdoB zxJ6_@fVQoJeeDU-W68*hr`O-kedHsB_uwQRdXXba`;1V@7ixz~G)@=g?nijNNnlChf+QCXY&RBdFke_I> z+=5Go&O`5g2RAfS+c`oY$j*ztT)fZNb@Pyg=e@l*>1%6}e_Xy&jUmKzuR@?6F`+uEQ*@>Zl}y z-&9;}!cgR&%#-dWk4|hoo7zC>a)!Si4*6f)ee-*yLDO!Mjcs#cd*kfJPByk}TN~Tj zcw=oQJK5N_ZQC}^yx(`;^GBSY=enk=tE#K+>aORmZyPUTIJZ0owr$F{3&c)9H56V= z6IG%etz)0V5e}b>Wo=Z{Z%`4aMZ31uq*6Ossd`Wca#)IMnX)tk8aGY|bqV4@#*}b> z9e=r^HjH8v)w%mSFVj{LhZ@NqqQF;C(y3_~r zCVfe_$nfyggqo+4K`K zZe79I+o& z%syEzw%7FOTs-Vn`%y+Pm?^uQN~Ui7L;3v8N8CFhty`{21}1E`C;U8O#5y@GY1TD8 zEYrhy_xX4>W%gS2 z`xAumTUH2ui}w!+o?w%invnnZ)n0f({{25#QsO;w-tj^=PV{=qQ4kwnlwW3+$qaAz zPQTT;5+C`pK_LNo&jmGP+nJ)Ecq>uE2O0!0X~xLnCP_MtX1ND$RGT#Cl{>GhWy|O&5CV$oCcid3~w|QI0 z*|Iet3n=An^s({w@Pj4+H8p=;+%Ms$NeTD%5z68;($3Q5X%v)vsAR8cqey>Qy;-8e zEp{M)qXQSZ&X_O*;jHW3FEq-9Ngj(UhN6{iX}gMKxg$fDeVHooN4K;i{ZrLFl!$SJ zA-fR4!Wt;FMU&3uf3Z<*Au?n$Vq)N-W0?>B+=`crofiKm&yfk{em36u^=$FX!O5OU zkm88BD~@HBRQ{8k0oDl;TinBJ(_6Q5KwLK4RzuA*VB5Gq*|meP{r^}`TTF@l(-$5gE0XV znNr{Ho>z9l1E0?3S0{dHGS1Br1!l3naz*)G_mOY>e-^yiPT`RYs zZm`5~LZ@k4K)qb>o}ttA@W|VS>*m!A^UY1}oM|VJ@x^wd)wKU2s$>CX5l?ZeI1=1(Mqa~GvmyDbm7e1gNhAj>(csC|s zD&uQG9E~PSD=wz)2W6YhK)H-w-T8Tw%F7(?vUE=jCHoK3kDky3J!kdAI8te1J$Nh) z;%}TaIzEd}zBYbfCfW3JTZ{HIJv>zQ3JGd!Yf*b&qm4om7wS_TnU;1w(>|pif-j4; zREKNtwaIrZ5*ccD(1u0{J#fI>>zBc6m6KrGaf&HW6NhqU45POp8@HAfYW0i_$dv-L z4<2D@EGX1p?{WAbE}pDljTXC}$AiCI*+`xKH2miq&~WOOX)iUT3(;?rh$qTK(PBFkypTyxo1c%e6 z?o`^6lS6y;$EU8@e@>{zVeQmRh(Y}!?BC$J)~@^e2=f&sqxzu9eUH-Jw3EsU%tqK{ zME?Th;N$`;=F?d1d5~^gU zWg(^ zbCPP{_C71bBaGqHifaU>UEWOn5@DbUkTI8_G7)t7yt%h;HwsZjrMg;(RsO7?1tiGH zRGe6{sUVH|eS9Ijk^j}=vdRwUVW}Ry25PDe{whLon)QZ<$|x3??nnVTmt-ku4LIQb zvZpl*Dv$LfW_M;hwBhi8Im??ORCu!+=(z`X8<8L>q<=RWZoB>lbZ*V-VUMkvFw9{3 z8Yz1tU`+qK_b&I9gbtEY?*syhvCN=jOnRWK%H{s~v@}+?=JMBA>%o|pDO{hIN7ws` zejiH=<2aW_3y;~ZuV%jFUZw5d>V6kiDwdd}?9x_`m3=s@qz4MwAYI6^n#l**Sbe2W zk#bD2ge25nib_ChLVvhm!%4`39Wr^o1C$w7)c)i8O`}87C*eaG@Toco6q6i^CjpEa z#N5n%TsaVOdqkDCq=DQye0n$tldCiH%Tj}(7}SavYg_cyG6{YDr*Zc3FO#VNc!PeV zrKe4SW*bD2K^flYk>Q|&i?$Bk^2?4SRaJUU16oWn|wFQ7bEOfz=rsvg%I8- ze?2>~RIr8ys~1Px<19pfgu442!8-Wq8~9|wE`k`EyajMIX>vQ7X8go52EzL0@SBe( z2U?CZxce!$JCZFf${kt)H2uE-gy|Kf&5Q{)nwdNR#4Xkvg$la4WL~;Am7(t^6lY zwF1A!heDvaa&Wev9goZu}GkmfwX*XQg%oEMy8*$ zAc2yX>^d{EzTukmVhTg3b)sK7x{#ra@ImR&1h?LAr-R}_}V%1~N9 zKWy<>m^NGb&t_*MGj5D8le1};Jw~8@t#n-Yd(LpY`4TrpK&8;jIM_r_`HFN~=YH!B(bs zWxGnvyJEfs70}JPDG(GgMoX!y6DJL3X?JiP%}H02Ge~dgB5N!a*PaD}&+6go@$D5N zbPLu|ZPQE;#o!yBez=y%8Bk;4QBg-)zCLL+^J>L9gCxTw36jIZ^sh-b)ZqiUQfH9) zvR3fZ+Ro*9I_LTJuqX3!Q}e$KCuC?BjHlW5MIOQyC%BHdfvwt%NI{+Gwunc)`B0VS3{19l)vynOrr!$gEq*6!%zPBpfzg}c3-n_ zT|wLD35j$@C=@j&$l9jl-WWz|@c!712XAgC-FHNH3<965+K?8v1=1xX8($eOY>xqF|QtsX;`=I1AyCP zY3JBnU!z`(R(JKX<*|NTm;dz3;CC=<5hOVT^w00+7+~|M6nHvQA3gv!f6EO$x%M+r zdDJDXKAT=`jBRJ9dY$R0D{I$kp}fYr3G2@|(+9Nn;RKm#@fFauCPYv^P6oQ)J|u?* z#RP#lilF}IpQSLRS%GF(JF;Qm^h$NW!kVM6mw7iYxLIlYttvIF565)Rqhob5-*uk5 zGS%zLpwU?kdJ!H`8zH-oT%GSQs?A0CN964jKhL6bPIPpa5vz1~lDUG=3Zc@+YEIbY zr==7m@BdxuKbwG{NpyEMuNaU;Z9zsQp-~&&8Hwg{;k>_ua|QZIrxaV_@GVE;%{svv z{QjQPGhEJ!Fr4jzK4}b%MVT(nOzQxb*U}Nb2ZMd2TY{5|&NHL6)$v{F%p2awciOT?^v%Q5-H<p(Yo8{!>rzojkQX~{vv>e)Vv zo`3JchRP-jTx0Ch_w?2SW(Fkqw#i|$vX$&$GAgSx6DssIQkh$uOGmZ)?T&uBHuW)V zyZm>%rRWfpi`cP05YXB3yDp^2Q0OWjl{;4jShwyC2LJxC3SgvJuws+clRZNl+^vzo z0v*;EKApDHETF3Y*E#7TA@G}Fwu5`Vclm(hjMvG;&%t-MxXC~)xmM$Bl!%~MD~=8U z%$RqU^k5Hr+PYJ{xDSEet$tDib zdygWG)xS{Skd4CjhP#E^i$n@V@mger{<;r((G~8CVZArm`)Lc-$L|cV2zL)C(i#%b z4T5jyI@903RU$oImi6OwGrf6<=n7)TR7cZL1_qvA=6}xp@hQkVAxHPQDdkVXBi(g7 z|7xHa&Y3)ykhJ4aJaX_J0Ak`0uc>TZJBMW@ozpTd>cjukyX7H(W5)vffEDl{$5|`HLB19Y)fE_*Xv7MC>*j)Iw^Anm2lU%Yo z8!uqU`Ud%HCVmf1kAtA7E~&DJAf?pk7-F3dDk=_->!QL zEOG|~(r|yEl7ds7>ASr`C}#Y?*D#YL$M19;SOxsWl~0R~WMk~ZhVUwOm{=&!Ahc>R zs&9;?+c!s9H5LgY%^%&jF`kAVsaNDo=;fGDIOcmjT$*WX^{8qoXIu@t+m0XQjtc9B zh=OEpzY!Or?8wjWl6Ef}8yW)cOFi+8v07GAHMelguEfh;Il-&=S7KE+Pw9(G`B1LHNOSqsBi^Svnmwpp*>2SpO>oq zH2cM;3W=HMbz;(oIO@^PU45)wdY!x- zbrRZ~kRF)}=+l;)#Kc~|)Bf1;;^cS(9$71N9HzU+q_57fahVW6?jF;#EJwCeiLYFk zC4=mAWpx(x-J2Fw5?5k^O^e7%qi zFQrUBj7?1NJ#T3=?D*GG!K>PSCjCibT}vW8XQM5`0R!Il1}X z^lI&8m(Jt_4 z^>eNhcp)&4&s{LrAxq33wj(vs%5@^*d27wh9?+AYu|=ybtw{dU-{EtH#MU=FDr^!> zoL9sN$()OqRHi!|O(V}Cll^sm-HQ@YdR`Jjk8yN%caE^xi|QxZtyJJvb;UCcYYX8` zkE<1&C|tv#dg_WO+qi`N;vQzKxMDd@|Gr3PpdYZ-NthE-OhfrJd@>O?9#CCRa-4)u zFfXw(g%sdFAFrNZ*@5U{O^*J0P;I)V;>Sm1V^b_bN@%`Byl~HU>HplY=qW5+Eo!fnLS6p-3FdYt5;5)S)&T=gC>ww`PfMu0i_tJL< z$^Z0~={6FGG(r=aLa4!A96>4Vh){y8Etam#AF5{dNuMf+byA4Z3yN%Y>)KFmUDVsy zhi)qf{&_bST4J(VA&^x0<1t$l=C`Y8-3Z~zRE{`H4QukA-mGQy(!FKY9{9S70s=#P zY}-nbS-Of;sGi z(v<&Hj?4Sm&M*VDUR`Kr4r+AZVHi3<)KQ9ZgrtL8iAOcs&aXH6vxtA7qE)3K0FghI z=(VUAV%HjA9+92lP7Iykf*Z+0*rjO7UXMIl4UMv@h|py>F1)rXVBTN9zM+_8o@ntY zLOtvi-;zQECt2}3ub6qCW`MG;c&?JstxDJ z;;8h=PCi^b{>@!T;oB&0=X3k5c?yT!jO$=m9BTe3SgW5I$)7X0oGR_~-nuc$f9Wq= zzSF6(>A`8}QpIyz2HrMT< z(7cp)k_QBUuWtJ%cIM=pdQeeiGzu>Lx|-kjLT8#9j6B00L93#NeOpE=7vINQ4Rtzv zRSx}lOA{c((T|sD>smC@`-kiU54IhjU+-QksEZnfmMPs%&_xBljkpacdy~xSnTey) zCZLrzPbB|+{G?1FTN-SXGJfQU0ZF0dNk*yKw>R`WuhBVMe_ES1b~G0*20PW7fyHom z*Y#W-?v$$3OW%x3#`*mx*xk#5rTES-@)icFOsFF~W{~w|t4-;Pd97v@FS-bo=j7~x zj`e9q8feSeab}R!8P1mGP}1Bb%0lqIdHk7ZvRKM>)(sl6w0VZ<%u7h{Ifz8m*R7bu zUfq|-hAC>Lfn+2q2+ixq&CCZ+?6cW^1Rs)EP$9h0=jyje=D|yfkEO&h3MW88VZv&0 zY#IWK5`g}x=IKWEYIYpH961v#!MCG233Sl-kiE9kr*xoxugml~K{6Qwl5G7h{u)^g z=(ovhrM!acHab@>*7bR)HE=@w51+8$ez$@sol7ovy;yZ7e4Sq{*)gWUTskEJw=kuj zOy~4&!=Jtab@EJ^@6Q9{WY^<^|6cR5m$h)_OH4Cz8Wm|1|Kw+9otC!JMQ1Voxb4@? zus2*i$8M`n0-K1Q*kk1|CmbHofZtqTd9aC7?&ue^?`^o0-Cfuwb6vqIFi*N#ZvsSTk2kG-aWaGY-zvI`Bfn0_z`pjT{dyZ``V) z3;)Wuz0WU}#s12l2+=%6NQ4KhVC}|7DoBw9p?injqQW!TxaB5PW=2Zc?mtX*;LJ+C zm0zvMw)Ut{^4ugOd?T~6dXgfRtawy%vF67Ps7D$+>wKQ*sVUlv{%{}$4;m1Juc5*s zO(5R5y~r)1RRI8t`j5>G)kro_a+5H2k{RbYV0Rl6ecx@dCS zr`e`GWyhTZ#p3tnTj*3;$2An#3vW^lGL*CZR>AeJ>~8haUfU-GMUF~`-Br)f6O!nB zAXw48w21~Z(&1{HiG+h>LKHEnD5uqGMBOJqGzxRVUztiE-D*T}HN}ip}Wu=@1*N?67G;L|iA2nDrqS1?t)`&4w%YZ=gNy0i9{>w{!p+%u9w=H0RX zw1lV{8XMji>1zH5jj5#Y23m^U-4bO$3X)X%=oE@h z#eR0W`K}FJ-6g4tAxqZK{An%eu=nYAubH>P|DuoWb2@^s zcaxf`)WhgTfKu9gBNFBz@88tPn*PSe#5IbP!5kK8$dcgsm62vxYVzqD ziTA5*zn(7hB31s!4&J(bSwQUHPU2$lRdpMt5E;VGwbi(;D(h;dk~UpRCO9+-ue?N##WCBcMN;dq%s7GXad7QJsXFaXnd#EB}idR~=``NczQ z<7sGRKY_q6-`4!3mvzH$cftz$sm}68Nd$#Vd%0C9RKCLoP^l&3+Z&(;L=tQ*{Zfz7 zpVU%YvXcN{l3owYjeh3GnP{k~qKow9U5xPrM+&%^jPLO60(o>R-6NrxQn# zo3K8#0V221P2CNytIb^W#NbOQNdr< z1Wu_nouv+Z+ya@IOB5vPaXq#Pk_neya=RinSuspGjO1Xvq@?9P-{+E=+9FwVp^fY^ zBL4F+{fQ`k9XC|0ET1mrX^uPF>jXR_c4|1H9P3XsOeJ0n5o2;P!nKzCb6xcGelnVFy%CFOvl-QFRj1gOI{=VvW0MoX=v?dPy!{aD-$R*&wy|0@7 zKuZW%B`I(3MTR6+j)03;p+cIWQvl0g4YtFDV&_DggO$#8C);uK*1QUoT@s~!ZWu>= zUh`WDktOO;j;8(K43EPz7Lh%td1~>>l6FN6+~CMQQcjuxYeLWuz{4N?R_R>FMFJ^w zk>a=a)JAZ|q+gK{pJcSULB|W4VU*v}P<6`m- zWV^T)H!5z+T(`CMC#AJKUzasF8PhKm3aQQZ_;8jjvZqqHv0oSLP5Y;g2~wkAc5*1u zhd6Z1M-KJd}x5ksp;1+arCHs-c-z$OUz3Hx5cJ1x#{0Vl$yw}8Gnf{ zWASxM5{KRFeLT5pN!TP1N}HoEksC`t2~BozCnqNummOYzilc)_ya!;BoU7G#L2e}ShMkZ3hPeVC zCvK7jH82FtNtR^#TQ|>=xGU2AX`SX@)j@8mvw9L|@M$M_XZgC-vwE#%lbtaMeJ`48 z@7e{3Ab?}EMN46!5z})XeHveRa-V8SKv#Iy3!n5}B~}{K6g~B^S7nBJ%BQX@Ntzq8 zIC?=9&#h-Q*N@s~HCeAVf<%EWXJ*cW31%B}i;ev9DNZzlv+C0?@#n|B;e9jJMTd`t z#nvp~Q_<{K`*Fz@o!wDz9LVgxt!;@?kHuV1G^kje1xH-ln!>oz@|*Ju7%TrIVr6FK zrjPRa&wG5OFfjlOiS89SK3_!j825k3ML-A+8r<;*-t5Q6LZRM#bPDer@Fy3V5eAvq-MhT#0KND+S_H=PkC!`nyOT{llX} zDBo_cavcK4$4RQYyS<_CP^FHL_R+YMZ@4mml!^)+UIw5MVVA0t&n`OPzn5R^{Sdr0 z|D4-O7`}?K<>l>Q0;*jnZBeD_Fr6~llI7gwWMz!^=e8x{{H8}r(VNMGr77yp7nZb+bU0te50D)TeBn~3LBbcUFaZ4d>;r=0psU!`cwMru+8 zza;oXCTX<&ghm%BE=Pw$0>ER9^(hkuos=k{eMPWbQj-xisO(;C6Pc(-4lYT#2&}hhM^xH9s zqqE-vK|r7h>|UxATRn`OH!!AgQP7 zU_S;lv=i)npPeQ#PwVMw{q3BU+K6VV9mZVi97(Q1)x@EvF(%D|!Rhh#1I;=_7(7Ec zI#$x*Df4+&qS4bTfsmre3(Aaf%gFzF@z9xC-t`_;=0{wPl7IUmgypDA*WZz$ep&YF z?QoY93y)Q%q7Te_@N6m%&L8=Zbj5z$QUgT zEMB%>v$h_32INl<;Rt>>daytb(qJ@Q$faK-rh_=;(A#H9o1xWPLD0lb)|m)L(Grx+ zTCCjI=3f)?RM^oTL;+txW9u;EcrxD`je<&;M%PYNr8M>~!6}~pMv`&YyuU?*W-{l9 zqFxHUbDHv>sw7$~-COUozuw1Bf5uXN1XY{}vxqupRgwY*ZEpkmS-sa%!Sfx=^deLar$tR8!VKDuoRNUW|Vl>{E$8y_l5r(=;Dn>Y+4;hhjF z*p4KF6AA}dq3d0(Zr@!PAQc9RnsDEr$};h{f6@A%?P)4T?>>bk=16~UFe%<4hCi$A z>fpAW^P_bcrr`vmCdu%NH_~uR$ejTt;tOTM!SQyN;j?Q3vYnPX9G`>k7?=c4@dQHh zUVf-Wd2y>$KILkoEJk0R>&gK0hks7t9#A@>4SM3-IYz=SJsGcB8;M~k;(1N&WJE~Y z6#!{PDso*Mq!(i3x4&BK#{1ygP^`O-fpzpq6@9kS-RYq~ta2u*-ZL!*nV(NkP4e|{%<{|1B1jXmATjgCDih@3Cw z`-}-CUIm8%wh93PAd02lXo+OmF$*|qub@~oaf*@h=ZkE%O$$WO!($?x^uL)0TtwJx^HsayrdiYd9E z%6VvgMo@H<&2+{SpH>!C^Edyik!Y<{cyKU#MWCCn=kPiEj<(O2e*QWrFsN`GP`De> zd7Mv*66vrxbOE!~J`U}W6NOH&%*+PHydFkSJD1}DxD&iVKM9pq05sW&+ zqrkPEPQexp$jcFFa^q=WfOQ%dvF)9F_DlPnZ&`;ZG)CFOns&3Cd-WJsvYZX7S3hlQ z$}yXZe(l!;V}cG}NH0TJs6HvrF?$$piF-*N~~&*j2I5dp}(bIh%W1K9%kM{F+F zGj;5_N?q(Q&{}IkRAXA5-F#snzbqtH3+EFqys~4QdUM-}?1CP|atyn@l~gVniPutj z=RGUBqT~1n+J=gl3j6RUdP(MoVd@dv%Ciz&QPN-$aJ53Ntk=i1D62nA=zKUmaZ(n$ zmii4iiqfH%J3Qqk=p!h)sCc3d_;oN6A2H1p_`hE6;=3k{5mn%v>^;>1J8m1LzCSu% zgU1Ux_^slLqMR*!MmvgGivpAueQ4_8GJm^kM! znAEE>_)v~ejV=Eh7fte^RxS&C(HSdN*8F!Z{{A1+lW@gfq{weTlU-?+?g*(5M^AvH zM$+E|EUGe0_)PAEY^p-q?kreSP=5+>~lO ziYR!NIQs=BjNn**rlpy6>Faoj^aQd{kqvm2;wzRqbt*pWe|}~EE)YSr9@dY7v~8Bj z&QdMfyg}6@91SB6_5#2P9lTBnfjiDB6mX4+JA5-|T54NDjC%hAN@?JF=GbX);0rxv z2A=-n9UTI}9p=$NM`G4Nt=Fh3lvw{j;cRKM3cHKFk=W(I5K8kA&>O50{ov?g& zO>}*9G9E1YtlP*}sAa((!&{57 zUf?EV*C_^%ACAhbMX>atf!hIS8JMPGtBQsWHRu7YB2Cbo zLl?}di6)Oa{M182w_^F*zK7o-D`t9oqv8C5t&swzl>y)8fY#kr_e6)t6hA%&Z-N5) zpR<1JwDRSI^g}ZPI$LZ3--QSZHIF#D^PK;>z7``Gsy{4TjFa2#p~_m;Cc&UR^x95@ zy1};bCww2%4}r?qVjh+mVy{XmMful)z%b@##b-Qoy};7Hd?S3t8mFpZ$$*?cMVg?ff& z)CsvygNlkcOSF1ipXw2{3H}*Ye~o%_j3*q(ZwJ%3&E`FR#(cPtW@jT6zQGAwNJ3~_ z)a#AZgxo%X`gJP&89M7an6v+&9~537a7j9N1YYI&F+Z1E2S)5KVS?J(vxb3XEr9a=!*2&ikDkJ`j%4#<6QcElRRURa8aCeeIFISLInqiTnfnkweedWLwrvc)2d%CbOE;QtL~x(+(rm z0%ArYLf;gP6|j>;~-Jt+u)Dk`~WHpPWdV&2O8%+q;i{B!TEu>uL78 za4v+oJT>|^oeyQb!o-mR|564EDRX2bNgzp%f{j=&rS86)I=KzLO!YXGyIlpi;9Lq` zPL{NbArH1}nnh@3bQ+k<_W%t=u~W?Is2x|uM(k%7MJ3nM5=ld|kjDGFBdtDbSi0Ss zrlkE*ctCW^^PH>G^H5-Tj19g#bg}=K%B>u5W%Zma`Va~+I|0)Z3BVJ_RhjELd1%1H z+AFj0&Qc^&qJjY#WlKeNqK729a^X2ibqZf{nr;Iu64}6MITY%M#Maz!%F}XRn>CB) z=EsowDx5?D>30GT)Z>EE$_#qhKs1?~(J4NfP|ba{`3cKdDRI4aGLQoXI}2$NNguMd zgb!~}9r}a*2u2#^x7vfWXwu@{$MkL>^P+!?OTBm2vT90v6C3aTQg6$g#*pXZhm|+n z1}Gb#^Vh@PJB$vU3XJ*D@u~aIW64E#_7S^pPw9)4htVX#(N(m=O!w-R(_ZMVli0)E{@9db%O)eO<2grnF_mZZ{K!UJ^}ETrsM zV&A*L(@SL~wH-nyKaxs?t95J&)V5~Y^o)taGQn6TML^NQ=5lH^aDA2p%XGu^WAYL% z{%9LR%VZkN%U@*Hz<8|*fd@QrvNlTyth>}X#7w%SJsMLkLIn0Fw@!B;PUHw0%mx4R zqsG3D1Zt_~6VA_cZ6 z#4GXfQqGT(X|-~b967rIkJoD@-m@9zmq91!zt=FtwG=eG+FG#unu%RHll~e@W#7cc z=|(Fehk!fe`-^OivA=m2%PAoDXt0d(oxtI2ZZU$euD|GGJKqoqmM9|YZe{?*|17H4 z(jafh4hdOUohWVi6H911h*T;NarB|)inOS7DDRP8wOyR)!avZ-D}_(E_#P#}QVmP~ zjI~K3`>3T&cVD{{1{?KNU-d7`^CTeX_QVP=$Q3J*_e0y_hwjlO_c6kNp4x0DaHBp)g9HWpN4=eK9zt4BEbA$1U_d zA~zXXA`?b_d-aPF#{3lI#!+WVymd#iOV8Q1U) zal<&f3(I8z91xQxeIMT&inoc=WP`6v;sI)T9u&sEEhIf6AG+E+4hW>f^|^tlUC4MG6ur-{15g zKmu^JaS;Jk;7HQ}Dn4DIbm zXe`%xU=xbs_2ay^aVHLQSx@3$9&JF=wh%83Yl&~vkg&0`#x{wQ=wXu*F}a0$6u!E< z4Q_Txw`=g$&^^L`(rt)a=M1`txF&Wnk&n%)4elWNx*|{QNm{(T>p7oW-`x7RYzaMm zmZ?;XTBBE!+WUD+Ue$d;SH|G-yq>{1dIrp>gD-^(Ys7UaR+e~KBho46)cmUE zw`qL`+IAlNZtqnL0R*R@+GLDsr4=f1jm+6pUW4DXZ0?Xo;Hz24zawg?>F@v@j&Lpo zC1#-6>3z)OmR7TyKjP?|NFBHD&5$v+;?yq5&s}VQj5N4qkis0@Rj(rr2nSRR^Jo6hbch5c77Hg)~I=fIyDr>iFH~UgfQ#o$Qyw`!u5t z4erVBxOL2F18REu->q;i|LF0mu`X-y-q>9J%cD}$x|_0Yaqpf%xEDDe&}Ssxtyw4f zk+%OKCVun)ejMgIg)jFqGEuD(A1_5)=iV2`yGMIoH@|qx6G!isl+!;HR86@4jtbE2 ziFOFJaa!u@@RA%)AUpDYs{kAh8QOeB?<8E}jP&v;{kQ@}FW%NSlF#`;6|m*~P`^p$BRoI0Eb^)YIE=)Skb(XL%eDVJuAYXsZ7-)CsiBe-`4I~>dGqfM>qL&q9FXD zkEv1T1fs0)&XOa!JdXZe=G4&Fl^Er_`|+VJ3@nb4y-W}6?tmsmX`B2m+`T7(S0-z4 zU!s1&H@C0>D$|9F;j_^AdTmLLtb2CS?zpWaJ)M*ZL`Lg0X0ql933Ph<${73#ZhsJ@ z07Svd2fX)}B`@ZlxVYm73@p8;W3t8hKxqqgY{3??9z!mL!HEu4$jNHi%{mNQu* z*+sHm2JiSU8}w6{M1D&1fTJQ2GFT!P8TB_vlv-@Y-e!0}0=An!1ln6N#&SRi`prDHTSY}! z6vo<5bMnAkRU}j>rx!NSYOe*kfuc@SSw(P$;c$6AGXy7Rc~4(j|kD|Zc- zcpg}J!XfK4AP{3XbRf?3e8*L45I#+PEyvtji=m_^j-MqUWgXXa@-F1maCEI}g^D6K z86&Hpq|fv}N^8Q?V}mXjh?!_1DTbVSSviCX9Wvy{$cQlY#Q+SjHXvCk{;$9Y;?$W- z1J7YZ*04GiwMo`Mecy-Q(wMJzKeC37T=ZsfRTvCJO!PknK0jWbihc7;F=Z;5WHHsd zWn|i#uG^ZPNaKll<08p`rkEaDQS(oMvGM$pJ8jVJkC|72KVBIp?`+`r)UmKfBDx^K zB!Nny8A^;DiUCs3C(A+il^|qMJAh zQt3n5rvH>UvRh7%XyjK8wn(w*kl}3cx6~x<`LF(GTac9v=i_r2f*x~9bXi433~q~6 zajxIOop^IEp7;GGxp%C;!cm7GqK7oT+OK*HIm5DaGx032c%uIw?*8(xiud~ig<-pC z>Fy5c?${uWgmfb; z+g%+}p=^|;e^R|gC)dMsd!8?a0rWKYeji?CScmSv^-VjxdUkN6OePR;nUNKs8d_{nf~fp7Q-(F*XsVs;13;PWwVW*7IkMz zBUd8zmmZzO&OvaSkm)XjDJ1F!s~Vy$A5ri9^QmNC{Zq_o#s+)@{f|Ett1+64bw|42 z;ki^{Q-5+MXbcD;i4x$It&#G!)~U^pA{F;i{`vSyoYzl<+W?p5k}vTagIf0ia})ZB zFz~#9Yz-)YkJdqV)#if|wg7E8N5zOtBo43*$YWnY^?@BlW#%+{UFLUn^!{`5`S;xJ z;^TRx(>E^%SEi?7rWw=Tc-3>QM_@hdMR{}7)fhwsF|4s?j{pI zV)0>;Uw6Gy9?a%7DD^rGk|$#NX&O(z^ua1vzOq z`XA!kO>C>5Tc@viI0R@v_U+pN_d5_Y){6O@Xc)XEsS%#-BC9E6T=m}zz?|ns6Lt`< z>(z_(F|sP>Z~Oj^Cf=7j$q{K-GHii4HSrwkJt?TTX|jItduunxT9Ld(9!|9o+$WKO zIwV)Lwrue$#$14=aC`XGR%$9joqXgXb=b)#;?C*05j)qI9QU#@rAUfj-F@9^V?pts zTH_Rq`HlEtIfrU}fxgl8G8Sq-JVsIERVUY1Up9SHOMQ;j34Fcdw}(|#3VRUK%Kaaq z7?1)K0UuE z?LAl+(fD;oY0B295W|x&P`b&8N)&8cZ|GsZ;8qTbTQGX>o0rT zz4Vmm(D41xLoX%#-c2;S+@U=Q5-WqCp(rm$Th)_w>_&co+EHqb)AH;djZUY?S0jI#BQQ9B=P>b!wEW z_eUj@dqi)RsiKDYj-Am6TW`5smEUR=Am^k7pKpalzt#_2YyTjc&l%OB`J&2*m`#{C zBFp$q6fz^rM_FlVzOMnk7H@U4gD<0F@L^w0jjlXo*OczAFUN$ZK-Jtwucb}0#?pILb|abae|d8;)s9%5S0Y}EODXQ`6N zAVPuR^hR#{w%-RwY%-j?zfmn|VmxZW1((?RoXNGEa7?5j-+Z^_mLDAl_@5 zziKMUo*vzdubgW;V~9=e?CTHg(#XQX+PY}hd7*c`IY$1bc>QJJ6=zrNY1}4(#7k)^P3J4a&sEip zRG-XvoQUyS8t~bYJ%c&D3vlQ$(XvYC>T*|?tkL*<`WVVAwyjkY+hu6q5eI}`9&x3g zEtB?f(lm+$eZN!6OdPsXaX&hV7xFK2tqkGPhtt`h%0zEnUf2^gXl&9y7#n9rqMcy% zOe(ce${cc6dsfv@M1<`qCWxgqDr6bE%_aPiNobxY>peL#Ym~4@HyH_6ktfzPWA~K{ z-IV-eU}to`qsmFbth>1=rX+APf#k5H*PUDcTUJ=uLV8%m#Gl`QIcj+ZcPh=mQwC$n zxm1kH-|n{nmcv}byKcGu3jybadNn&-O^+nt}mTjguh~n&2_qWT^f{J zLomMCA;X9|R{Afpj0J`~si>-OYv+RO0R zsg`W;#^rS$-Eyb)*Djbqo{TJxBjCu7B$FlQ+tJX zJ#b(@jK_Mol&V$ho~VY@s|Gw%o0jFVCOUpOLy05ePDHB|th)84hL0^a4;Rb&LAs<8 zuXhkSp62)6>xc>-^GiB?&!Hd*W7z(t3o^OZD%Idui%bzr!qRVcNOQ-1wk6-g=bRry z+d9gum|R<;eT)>#0WOlAS#`DUE0W4>XbC4=`1Dnr)ie!1+k-JhlQ2%)?}gIe4B->+ zT6age`9|JOT#rV7^N*e*|KVcQokrhdd)B|Uy54>H|L2ct?J)$T-?}&gH3%`U*>$l(D4b--FeQQ8p}LlG56H=Gylr1T%G~kP zXO11bcBX4VxInr6x%@k#Pb>IJw(R)p+E7KZQaVItxF)kf--BttzeM=v^-mX(*m0~V zO?NlEuZAX&0~IYQ57}<&+FUwPF@l)(S5Y!{^9T{Syf9PL@M0x}ovhaj(<7P>NDgN7 z$GIEL^sO$vv@4Rrc-#7nAyYWfs2F`lX&qTmGBlnhfpr(ZZwk+6@ZL?Q%f2ndw4Pbn zqZYhUunnFLiLtRIIL8WpX12z9qgH3OEY#i+08g$TOUtn^%zUUSApvf;*4!UZ_|%eT{8)3R?lk*T zx{?K}wRU(*JVYtyi<~g0iy)44{&1?eE>+OHJ5eIOfjOLt{F<3$b?Xkdpu;x%tk*f% zx>LG!?3w>n4>hhLtS^R{hLG?B`kuw5W}WWH!|spYBW;47%)Rx}blSBA;YZkzi|V#z z{;t>izNyoW@yLyV3atD*o=bB{YLwy60^A)(zKmT(knHoWuPZioB~)ZKo9CsS4Mg-d8d*JG7n_T&cEnGuI$B4}makOqEF zt;bGDoS6Z5yy2Bjt*>O82LAa_&FwcNA*pool~nL-YA@^C`e(oB!Z1s&nx=Bajx&vR~PbtUuA z`>2SCVi}DI^&I>j9l*7ct9j2i#xk`iIipAMs}3A!DB0kj@$oehPqO^pLBQU;xuqL5 zVxmuucDm>O9{Hn@EdVPK3j=|#O9pt7hI?%;>4>b};d8rx#2 zN4Ej(toG4BB+Brw`W6&7gaEMMJ00jU|3CsyzS`rQoDA zqhSAKktpE(D2|z;Sj@QY))>WR$5$n`_n$BM3N=?l~)rEtIPmhgRsRn7R&kv*e$O$KC%bt6JkKoYyKY*?)B^s_j(vkEwWZWcZ*++00 zEj6yC>1h3ctkm)0XzaMV7aLNYTd~4_B&PR=rnvQfwZ-MUOxvMyDE%Rs+aMgY<@mYs zHA9OpeOZUTe@Q$dm6UG`OZxkVXWk!w;~!It$wt0}WY{XFTcx=>b zvFY{$E6EqR})hqjbQre&-QSwiLOJQXJA=C0M| z+@{ z?!@(aCt1cdWQ>jUI=I-iq^W$5(A|<3x}hfX!9OH8V-WA<)c$2=+aglK{Wov7l6g0s zquk|pyda>RJ9qQp^jj?_x%}3kB-(xt@z)E{hWBmGo77xXpW@Bdu_`r3GOl3{?;Xv! zlhom)^O$xbn-Plo3sEyC#e3L6w@OB}I)fWBqaS&O*98FH&0Haa*xm5a|bp8kT5H{?u z``|99_j=!P7*R*Z&~RTUSABci8XTpzTM`8#2K@*!&{a2!CMcLegtt>t@xYaxWAwmf z&Blo2%RugbvBD!iO^P9P^3knoJB=jHMFF_SxVDs(in2%BkomKP;78d)cvh;%2=&oQbiAw{*-%LYSXh~Sib@AZPTian%T4CnWphEA;#V(v+Rl1!dKY?} zNm#)Mp~Gp{J#Bs77Y^%9=h^dt#U|^vG1{03S_9`jPGoErK zgSI;v3q+Nw7=-}$?FjV9-ndiS=^j+xEAm z^v#-D3roy-u(pYNHRc3O&QLO}+!~z7Yo54utjm z8^q{9-dCA+={h;uld?Pex;z$oU2xm8ePJlC4ALypucl*y@H*fvtkwR7Dqq7)eK>raVYp&2iD?q z*d-RiQ=`>}Lmr#aEQDMXXcY3jM(4cO*r@EiXoIDU!()R_i zJq0NEk^E<{Vy<&3-15vMef%SNdC&%~GV=0r$6ZO~9Of{cPCh3j|Lso&eR!L_K7``(d zB01kP*tL$dz0p~mpd^h*uXO7kaD5m2i0Z`<4^bK&%~>cvvEtrXlQ%uh$J9ajhBS!i zT&KZo171VMCYS4$1;H&hpBCkgBQep_Sl!*l}B>UTZ^O+jTqV0`j1(9_qK{; zczu_|X6K1CQd7#N_%UD3@VqN3uXR@joG_!8j6R?iS7*c*KbXu7nXhxYCyH?fQMjR0QtO5t@liDXP!wLQ&! zv%EQiDX%LFv-DLWVgw_|(1DY~q;DECUksz8qc)mPN<-P(bW0&_NpSmW{PKYRKG5H3IP@TaQ$=GHcw3kKMW0O| zB(I5{%weoeBDtkqaF(Td-f&VAW|T2HO$1I;_OKYKwO3DI9JV}0l-`x(qq2OK%WN(+ zu~?YU(zC*IyKZ2|3=D~uFgq~GrH6-q}*<{z2?^L<33UKXVj^Ph-@qR zl(m!QBZC%4gG;8Ba5Y5!i9TB&$I{93EdxDMGN0cKEKF{KfqZ>dOt=2jZWho}Tl>zfA4Mu(Qu?eR7QdK-4!#e_xoE;UO zG^Tg1l;7r^^%bxZjIrK54@>1s?EW`NBtJm8J@%x5AywuU>aPK4cl6s^&EMg8-t z5|7gfexH#8-_Y-Rp#`Hl>`_dEJ^XienU-oVv%<^raQfX%b=NG&|3v#{Z7PC<1yD`U%sX7&4VHX;?iogNKBqC#N{7n^ zVw;vSqeeI%Gr=fC`sIaRpDOiG?VcBs4puDLdMd1QBeZld6Di5JH@sg7BF{TpuB}4! zi_z10K$=}wUcM-#{@s&X9JS|Ul3BtqPSo<8o`O=vA7A5g6sK!HL}8$~)OgJ@s`NIK z$JaLN>lhNieQH^p>0OVinVM;1;W$bm9!e1_P9eZ&RO#smsmGk`NWLk>8}$XRFSV z>(HhMIIE(OGsS$ZqQc@JYxKRjtqWBpirUsg=O(iZU^=W|x{iG7`597@yJ zN5{AEs)f%_#%2$5?J_X7sE&n_$zC$PtDIb?jz?XoG4tRQyU(IH-iY&#HgiTZYq(F* zZ6wxX%~yZTRU|NC4$arVDKgwOmb8ug-SZ+{k9L@`D!eoK#JSvT+Bg0W^O`NB6VW9F zH0{`lvC50>W@FGuYzLJ}a7SP1bQxTCD9!Qa{OgO<0G~Gu1Kb8`LEE9LuQhOe-|O-X zd_yi9mt{G<9btg4)|-rXk<;+rXroPW#VLf>*IWTt%`oDYxyp;sPcs5zh5{&j1m7es zxeYEgN&U8&rXczGkVi?K={}llI#2j3h-H*|>{E>$0aiLl4?Bvs4W-zr`O4M29f7*O z>JGn#E4ah!eOVh$GxrCLsd2}kJah}xQZ$6;S8tPh*2JWzE>fl0kq)}6QolKs^f*bV zx-~0rvmRMut@0Hkwmv)Q8QLkfE}~5;RkdPHH7i!J1!fa+Bn}f_MJqddM_uo)<_DAc z=$Wa<6U5NObXzxI54XKId$csb9PX~~rXKn7^*g-1%`4T!Z$yO@GtG0HuZ1m@`sBC29ER-ji1l*5BKm@SQrAKmJPa!w-0itvz`Wx`SBv zx*LKyZB}4RYXxJy`ZlhU2tgz6ub52uKZ6Ca`+7AUK@9nGjFDVULQ6O(Tl!+XM|k9C zp{uCcG0DSQP2?F7`jBy2UujSD(DBKJuJ)u7i$U&z*+w#xF05yX%uog5s;_PFlxe-& z9A!ocI4*1@0|&2gBawhHdMl#9TtpZ|bvmfTJ6s|}_1IDg=C)ATu&o7$;6A6b%Ev=@ z{9g2)AWNAa3mo4o`v*6{D<*Yy;WocKuy0Adp0MKdZGX*)VbAVUDNFQScCH`RtEMd>`*}vhU({mk9 z!$oBr7rIa=$7*+kg#}CAOf7;h4;7a)du>CD(!LWPl8vg{;+G64s(qX?d5Q?nc^crK zuAl<+m^3-+#k_cSf7pipo~;uWuDsNKh00j6JEpPFyDJ-FrT_fI>6I;NjYyTNZLALq zM1md*nD-ab3eWir7MK9HPY+Wk|NFu-?Hyx#vG~h>0|Fyyo_?}0 zNxk^)cantMzr%m08PWr4*xARAM*cg{^Uu`COfV2K6JgL<;XwcW3T{+mY#^K?!;1PEJL%inVUX9X}IXMp>pVISDS|5*Vr2af_!Khp8C>i?_& z238QsFGj(#-B11hhXO4yFR>o%?(pLOICxqbFfXkyx7rB8_`ejOSWn6V2{+*UuWeG z&l5y5K0GhTrW+v)vHVY@4C#RGyQ}?~o<<|a__eG|6bl5{@jYMj)Su9pg?FZuAmX(f z(I{CfXMm-2dH98wT0QmVr?j*TG5@#LP(nT)eUmc>f&V0{&JNfdQ^R|49tsFG>{=(S zzk0$AqyNvBeeA59r`T2Ktgg>YGl?<$+j0x#bQUU{c_ey#->-@3^|Pa#-xonu|C%lf zxP!i4*OTZR3^J!Xew=H@=NBpx{I<5GzA!h^A`XkIa$K7U8=ynG0L#S$Pt4)$*Vn;M zvtLngN9eG{wf4c{WDP!{cYlA_=Z*Ah3+edasL%8j(C}e!oRTEG^tcAR3y#@3A@;A4 zxvRyZ>PPvvvTnDI_Df9~`MdYKXI61~F1x9Zk6x_D^-a6u&{fy1p1F@jzY}m8G7=pk z?J2(EnU5nRj9zKDvG}w!y`kEX+|Hz~ewGeOp{COk{2$3Q> z8Ai&^`uuq~ta6Y-T4!lCf;07UfQ7yy$j*w5O#6Ru%W%(iPbGrOeka_nZN>1CUQ?XVitgHA|!a|{8K5N~>^NVTBKohVH^ydDkQ;d)A< zseG$AT6$50VU}|ND3{U6_tRcUZBV-rn}E@^ai?erMuWZ?@-M%%#-LjbG|xL}tTL(zjt$ z@~ASzk!*4$@h3|w5ci|1ho?!V@aq@{SzI+qo$lQvsMS|&eY!_s^DK23(rdKu^!mOK z_NY^)>Z@4xqb+9>+tSXxWc2oI;(HsphO50H=ll1%PsWEh&IF0kDguF?GU|etY zM@~8l**?7aF`#RRVLVxS#?;Kaxc;J5!A{OytNK^Wo$I4NT4!298nGC@0f*fr|9Kcd zrGcTNUELtn#Yvh%308Tqe3`47ltoKP`3tM=+0RLjZ+pmBRyn_4&C;G6$ts5~*!Uad z&)d}5?xnKwMpFJ+25WRzdxuR_P$FPqBCt7&DY=>6riqHUD#YGj0 zhpi$v75;JuKRxBxb*Hgf1y~Cy2@wEU_7wxxY-pL-f*}imByJ?vym|FzTL+!8{8of{ zv!LMl_wRTfU%KZk7aaBJiCkRS%g=KgnsUgW{6z!s7cI;57R*oL5T^I~&lhNB1BGSr zN{xiZ1v;p5W}Bb$4t+Uif`^r<{a@WBSrgcl$}=JtZP>m6JZ|; zQTE0NYdtGcjvX($Eq@6`J*yH(0?;)x?i)%c1^S>0jJ{7DF@zy!KiR!6TI2BgB3n0O zyo5f+3=GL1s#Q3o5UXFXU}=@uV5Fp7v6SqNs`+QFoON1XEEvDt9K^lR%sDUEDiqJX zC^vr{BIDiOkn3`j`)HkoLir?p9we3Z$u3E1Fve#Iij;O#B3m&GH*H2=iBQ3m^2AGN zwy$q@+K#hc+}q6By4roPncyOiaq;y6sfhwld|ZB~P?=D*n!h=vFs^CU^Iec8>Sh<#CT zO^eM~p;LKf$(2prfJZQH!^Jtzq^6zic69c&a?q`>sEX%uWK`tAmhzLPH33cQSq3#= zKr$Oa>9_uyXc&4&hTB?VstWcNcb5{m_MNEd98t1&Ts8vvi@e4>Z<)|h7X;mpk{*}Z z6zgTKc&neptck3$t}DO-4uk;#uk9@y){;(}80RoK&^KaNbKA^fk%8;>Mjv>n8$Ce! z{px^Nge*};nRR!7i~4G5bm6U0Y7}5Y+%Ey&CoDP-0mz|%2PEO_Mt3RDk(3sHxmuq~ z*S&O&xGSD%TnWe|+K){$SVQB!@fU8HWJNC*;s5YRT3H4&x?ZtE1`8$ul8%d6wj4Gr z7!8ws(+%w=KCpe4f?2wljHK>Mn@ca8?osqJdNNmbC@J0ZduuB*?;WU+<%NSjh>{Zw z#BD|(Zf|$R@q(04-g>J}o=(vTGjBe#SuFa_+rM#9xQQLq#L3x`dw)=#o@h<-Cv}ie zL1MfR;eDlGU}eDJf}%EGh(?5rO@2UGmss~8ZQiTDxkXdh&j&j<4lEX$61jW+SE?#nM?K%$0ngBhI0RQ}^35u}k*9;0T|SR^!Pm+X_w%;Ksx zhF}Z>Sl1gOAWvF3@CN|eouUTFF5Q2{4#R}xcm@a zo-gn>1s&Adafx$hx8QS=jU8VP0~?~~#Ju=6DPaAdb^WN^tcfF^;=3yn!I@|?C|Q&U zJ76&1{@d#(Iv!7Ss&g%$=xD;0o_m=dRLrDl`1A32A>PUWVJ8nOvoB3P=mFr_&laRh zdTDJmCn3z8&i*{~j{Le=D)?~i=t&-`03kU!E;K+MKmGhBH!qxSQ{kIrH^xo0?-$hJ zEmXi54u${#xhaPMrb|b|!Nw&Z45Rf5pBx+PKc%u{Hb~6e2UF=MShI@pnOKJY#d#%r zAO|59z=jG=;|HXt?D-C{#+>>G(rfFuMm=Uavkn=+sSALxElnR;@g!(wM9IMo3rZyu zNf_uo(lJ}c{m=`-KUw8T0>daO>w=HCFu)}MV!%gV25|20&G_*D{soS00alathztY# z8sG}}SXTxjweNd*>Hq!(q;LjsI_wJv^M}II$0H#{-41BAuAy`%N*4N~I8 z4X;I~oFAO9+9UzH3}UhT>sQFLV~n3J4`#Ws&Ta1lId&;u__^K83Kul9eqa>r3i`+5 zD*#9eB|N_~#bp;d(j)LpV0vj<_EWx95qVzs&ORyeC?0f`=JY)Q8naq&KZYL9newdk z$ZBWTyJRx*ZSY?zf1yO7nKgh-N?g&rIaM}nzLur2!P#}D;PmCo(dT>g=xZI!UB~Nh zEKq4d>v0B9+@7EwDmnz=udze}hA{s_Ija>({ciFKY`u-5I!(6J#Sk^6Yrq0`J|XV7 zB>KYhvHX`sm_72G^HnI6E>dCY@5yf}0Tbzti<|iQgk!CZmyBfZeN!FKP|RDFw4@(> zGCz)5FXj2h%S_g})3s=2xU5to^?rBwcWEG2+J#kDxR;O zqpm-`>}zc{D!H_E*Jf&+Ih(-q-W=#vi;P(LQ&s?Rp&zg#MR?c3yX_Uu#w*v9^?RYj z4HlkypL*er%R>qkK22BK`^Ndj3LTS{{GS&&3%cc~EMZ9wf9)_+9AF$CR^TDiZ^p+< zFLUs!kA4n$zB5nO&e%O{|9)(ne!BL$d$%m&s_5t3{@upWHf^yqnh=!q-|_{XB&YZ# z4HhBrre48p!Bv{iR=qr?xJiqF)DSZu(AdM_VD6MNbjYXCA;Zx1ce>TcI>leN&<234 z&B3@CJ-dfar_6??pF$P(NqmfJUkvWRXI$}0EZJ|(3YvD)t1num+%A64#alZ5bC(jp zQETnwt8k-%)Cu(MPJUS z>`f>0&v{V+p1H^0b6S{R-EJ^)55A9860vGg(zLn$jCRrc(RyXnTNz_%G*OG>{l#?5|-9+Z}h|p=%ZD z_F@~7y_=hS(S1DRM0>rzIa;w~Xt9 zgL>D?l1)C`kYQGTNw$qUIztI_}73SlJ zg{*!Hai@od>hu1Kr5(CJ6t~ImS?q8w7_Q+Hr24P62P`E%yKh5JC8^_E0?hbolTtRv z-z8H-R{&1;efbOd;T^FNn?Q}ZC_2{PGy#;pKz#S~TvIMn)v*86uH`BD&*oNfGy5W@ zxUO3J^-!|;K2-i+zS0Cv(G?N7gv(E`s%K-Q`oEyw1AGyAwLJD5B<+<;nCw9CFK5Ua z0NLu|1u(~57u|x{Xes+&VoVIP5EUt(DJ!$5t zg)ywJgHt$T{hB@CA1y=@IsowzAPHGuF{tC&CGmP$TK75v@LaotAk3ed8z7qL2$+8l zYTBMSo1P|5o@f61kT`ab02Vt=qd>BvsyGa^AIN|7YQ{_9Ah?Yss)%(;Za`Um*?vej zy?kM3(mxO>RGI+=9B9@EOG4OMkCUnfzLN&;ua*OVgAySAToEse%CDmYAY<9xBNyXW zjyyF$-uV_V+Dq^&3^TU3RMG%{z+8a7(r00LYj8kBwygQ(%j?6I0bqYuXoYUAh~(;% z9T4FG?0^1wfYwrmBFY9rZzzRaBAO`x($_naszIb+7Xq#~)95%k2-3A#OG#8P=g6jq z06e@y3@~5(krwfVN*-trNdgcaQ)F0#A~VUy!RXs(vYTTP8bpdW?GPY>J`XfYm~MvwuE-kKtuo^GGT}~^t`mZ97c};4E+P3#uFXEr8vW?ReYaimI-;0 zU14|_L~TYDmIUUA@Cra_Ptqe7G9viY6(A>uTJ3MpYF<#UrK2fzZXLNQ=wLHwL>3;n)3KM)26zh6#L zT>Z|Vb*i7!)IOG}*_Q>Y#iDEFejDIR3RY_BjLI*PMsV&Ws`_j2L&sX0W>zH0~C`eEP?>VEy z2#+k<4t}aV9TWk;TT58aZPaS5K~c<6a@MSsPFIiW1NlC0WmOOm2yq3klhc*0T39xH z!U1sapFj||XoA7OYH$O|^4idm_ON`BQxCjC)PQ37E9AU_u>9D?X;+R277Un9QaQ91 z1T?!`@e(i7iHW>}TEuOu!2>xflf!t>89EHy`h*qZx$jhDfM6&%07knC2;bI$SQNa- z6Zi@);M3hg&nuODzRz5CdmuJYIlSp-*NUZms&n690l$`0nKsXd?8Tz^FxPW@n7*12 ziq|wsVtCw!f86p^X`%vAJ7M$9yXeS#7w225-h3BIwZUP<>V z`;%d^clsz5barL=N$Y@#Ndv@*vff%`_#GOa1nAE^^nEkY@>QhzI$dj@{E5LBmYRtX zOihgxC3xYWyK_1&Bk&YPxB(5?1wfIh2djAv;Uf_Sx5&E443!61I6 z1~7l5P!1i%Pp8SiN2;s+%RV-{qyYyZEP_}{3|S3|n$iwjRMOk1ywLB=gRvY z%S|b>%Im?2?cAPwAazs!lx9Bwa*n)IB*o_UGu?9Y(y+T5hrZ35CdAJhbU)bJCP0}@ zhlG=)Uo*7VwCoHR+12_Cw@=_Kjb)RfGDV5{FX9ieF}ZykKJ#+=(?Bz!|EY1g_Jbi2 z@oi~DN#z}CyX^?DYagYcJ)(z2WK8k10&;UI|n)dVodS4e`yiF=vi6 z6s@{lEbeL*+4IW#YPzEP`$H1pUSagHG3@IIGFOeK`>+q@bpidW86f%%UEX7Q`-41Z zioTh+9p!HXV%f*c5wtNcoJfSgD+-8^LZ3<53BFVoD9T5vSMrE{2BaKF9Gv@cNq>iP z1`Bj_A!TU?VUsO^cd!~)V6=%zx@a{^la~T)8^S!42jFln;3@CmW$$@sN@df?L z4j8SZvbp9;Kj*^f#@A0Qxs0V3qe7mdW-kfDr}8*cLlg}3k%xggN|0}ZMn4QvS$sv_ zQbyjdYI=*F`+!oL`jQy@3#Vd7qb2pEtYvI8xjoNdp*S3gnH ziQgty17ernwc1$Mu5@t}+xIBiHB}#3@gC_Om(4!Gfa-v(JuCXM%QC`G^!>)#Y4Y+_ zvC5UaN_!0*J4n2P3buSX%JsveRz@WQjRa5=%RD9RH1}WWt&o+H#UUTxl$KW#-tvdS zB+1^&sUbi)*kUrcv|Vd+;EBxY`{SuB4Tp(yx;sN}{Jpp=aba zde|Ol{SdIfII!yJc#>DgXBaJ~UYcucC_ET>0pIqbb=l0La2NHLVlK3d9~b%x*`1|S zEURN^eH@b;)Ck11p@Z>Q?O!0z{vu4Bz?IpKGo>Uim-dYF>+oQRE=boa>!>z8Y4SS! zsSJDt-~^#B$affTArziA$x9b&w5M-od45HZq9pM~qvunx$N5c7dg|SIB#H2-2}S-@ zgR))hZ1_g1-&2b?6WNzOBx4QmuG9jE?$!E0r^QRao&@Me%?O_ZVHs$eGd1#1jDn#v z$lHe+?U9p{6VgVs-p_y3kq^HFT72!feS2QEX(X!>K$E^~UGO&?#lt!n&2WO{ck6ul zUE8{}IgJ+hS3@k5mLtMCbK*Sv`c{yfJ1_5*nF;?R{c3C0Mw|G1K^bi74DZ5Xt1TVC7gM7WV4=kYI&+Rp4zCh ztJ_JU7_zz)y05cD7#^;^_L>U=u{l!{_OQQHBh(eKi{a-?wZ;3!PLhNAI#E2+dgnlh z3CK`k5dHi{3zeRi;@K0FbODO$NhIm_;8iSb=V6UIAu;!ED<7|3%Y-c{E%zCWzHmQa zH{c6WA_NoCAqPt){N8C+Zk89CWoslL!mRPwug)agWjePGyi*Z0d-k!hRm-t?&X{u_ z$m=QKhFF`icNk&SptM0PF~NNPd)-FwmDiS-vcg;-!*9NvT;H$&K+g{pWPAJLaSgVh zrmfxAz)!*@^zK;X4U>9;{7OOTM%OZWrqrxYiRTs2uzK}T46{NqbFF1!N;b=@^0Kvv zmJ$;lv<9x(TH>-pAb$d&D1dXYq~b_OWaez0Ua;7f=I=@vW1xadu%eeZYcA`3=88Te z!UNj=3?xc7EF4$LH~x^l#$sPpRFvKU8@0!UAoowOP}z>N7EB;J6QTka?{Edc!<^-( z>r_1L>E_Mc$QTd%c!Xa*+uY#(IqK!Ry91;k*(V@X2wXhBZUqsfiP)!64Iz^is{J`x z*BR=E_}vP&QO`JP42A}1(f-@cE7${(Zj}l&PcPO@xFDP=>vgQj+s&$VTmuFW2N;0j zD{hGnH0WhI&(@9Y!BQnPQMQ9xM|i^*J{@X5$Rq$)t9ebk_F5oY9>pUkdUIs1WFS9v za83RTC$$($ptDw>1*Aqpj#=tAP`{GgJF@cW)30z@7#~eRJq152;&tJqF0zTB%@ozeBu*QxfYL&SK3 z{1jF|(*D{_hz=!y6HF@i8Mm=%<>+DEdD>Y7B%%)lo=p=Xx$oxC;q-}&Q(fJ3ry0Nb z|Jn(k833Pt9Wa7urjpsscFw4ioACsXp00}q80V=XgaQG?B9ipjVQ=L@gHRX^`mdm> z1c>&0m!(Ji%0{u5^!xSgYYf=`zHtS^Zk ztMCsh0gB@)Kdoc(XV3m#?N8z62T2Fl74orQU&TE@Lq7? zODk}>#><(IAZ)cY0q2uwcptrfe`&<-_&B{)aT@g6Jk(;J%a@8h0m6fLfj!bTEqHR> zrn<4!J}Y$H8FFXt7zM4cHY6Y<^jIGjvR~po;m>$#PJRWfCJxV17$`74AMdMLYXL=0P@MUY)^TDF!GdXRV+fYx^n_IiXKk2 z*4svLlZQCwE`@Nwuvvg~WMR|xiPsl2Xs!5y(Hfsw+aa?rx1&^JD9v@$jp|kma<(-x zGX4n5m?YnN_yg+;l^-M)U`EcG3l9{G!Yuk>YTavdIIUPX%GZp3%J}Fr{G92M#bz8)UWkQ??_-Bp@Rj{-N7QNJ$Ia%g~nz;Z1+lN5M?_ z2PmMBe9$;#*K&*t0^AUzLKUdb2COIdOrxQ^o|+z3Q}2jw!{F$l(=|NeY90`aV;P|oQ8J)#M)tIL53 zhCW56Kj8@@A7C#v`5eIOe~-Kg1X{iS-xKOM2lag3A#V9lGwI6XBK>#&f)i*sq^$H) zJG+5DQWU(Hv98rs6Z+TSbOT(oWrY1^L*k9Iv-*&80Jt|T3WL5faATe>Jg`qhHhU5>XJ)` zIchik*B_(c0?n62e<`E_65~^fc}h>ZfDUaEvPu65+z(;_!~!lZ4RGlHDM96F|G!kV z<{vYF@q`0MEb>ADVDujySg>WGTR8t^DY}rYev;xj-$? zskqdnG7(8|J}tUxGUK7QoG^RN>6|AoZEs-wch?Cb#c>g^!C8!Vj|#g7H#rbR6vOlV~kVf0Kc;SHG4P`$z5n z>FL_TnO@^~W@9>GQ=25=go6}OR4U6oa?K^=G(DY6hg{ktbkUD$4yMx_%3W@e(uFp2 zoh~jRMi!D5i=Fhiv?upC-=9Cu{@e3x@7{fH@Av!pe4gJM27;eEp%{z`VftWbGhB^O z?@wC49HArKdo*D!@cx&%?9uNY9E|BF4Ye&M4?fi-FxJgQx7@Q=hl&HV==sdWWG01) zX_$Kd&bVfz{U&8A<_O^xU;YYegfhA5Q?K5MRW>7oxatR9wWOV+;MzAcbZbNS5sCxY zLa)ih0P_Q=x$Aw;`UWe~8)&0!N>Gu=blm{-H)G%%6N?*K!Z-@|q?gIh#eKde7wPnO zIE9E~6e^9Ti@0sZfE#JKuoX%0zbeQHoQ#Z&hexY3+h(b7bVy*7p|JFolyG&sk+Jc~ zsh}z}h8`+W>ov0&qsabWcBVNTC-aWawGyHC>E~RW z63Ob4M>T35~ z!<#h>0fNrDlcA*u?uD~t=?l_4o_i@WJrIr(fCHgmROCo3J~mpu!e3bB?x@Bsi?SF z#Y;o}#WtIX`}!8znfGIS+JYBHw!-K0-5|?tar@N~pY;bN$@>57Xw3dHkbRR9f^qw- zLaH{YC)cotOG49LZSJ+>p**q>6fi^HJLgp~f35GxGAmvEX;y6Aa>fXosYj@{k6ROB z*j&?o6`UbAg2u1$E+Z)4GaK;gK%W9lAeQzHwk5t1p7Dgdy?y4{VjjfsNc zHCbLTFxk(8Oi`qV(0Z5GEzH-K@O+xjGQ)^raE`xcM(a4DTZlx&T(z#ppd55@+bJG{GN}Ep9 zMuv!^x7{lGjrP{{hun3354D(;=;abMvoNlBvWfYemlk%pE9vR!3PTHDNT3E>H7jds zLEF3?H~Vvz6Hx3vv)8-QAWx6;0v_;X`#M;uJ&u(0I)FoOBTHdfU@?Q0%RA3dtB|X zj@@=@ANT|Nzj_JIOl0!1xsq9TVU6j8D2Z|as4b_K95>=!4DSnFhirftI7@qm?^8oO%MUNtA2OVbc2Nq=j<8ER)~hsq z<&;{ui1=xF%&al|sPAs0U7(%=Ey2Wac51jSc317U?{jl=Q>*Zp65g+gACr%52}s$P z;3W@@`O5JN=SZt|# zc{l7fbB36D3FA8V#ixkZ0249Y@gV#|TXX^?} GYTSR0nC~0_ literal 0 HcmV?d00001 diff --git a/Macros/Options/logo.svg b/Macros/Options/logo.svg new file mode 100644 index 0000000..0345444 --- /dev/null +++ b/Macros/Options/logo.svg @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Macros/Options/project.yml b/Macros/Options/project.yml new file mode 100644 index 0000000..abfabb5 --- /dev/null +++ b/Macros/Options/project.yml @@ -0,0 +1,13 @@ +name: Options +settings: + LINT_MODE: ${LINT_MODE} +packages: + StealthyStash: + path: . +aggregateTargets: + Lint: + buildScripts: + - path: Scripts/lint.sh + name: Lint + basedOnDependencyAnalysis: false + schemes: {} \ No newline at end of file From 52559b4a03770dba82b909891740e5506ca5c341 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 17 Jun 2025 20:38:41 -0400 Subject: [PATCH 3/9] Adding TypeAlias --- Macros/Options/Package@swift-6.1.swift | 66 +++++++++++++++++++ .../Extensions/ArrayExprSyntax.swift | 3 + .../Extensions/DeclModifierListSyntax.swift | 3 + .../Extensions/DeclModifierSyntax.swift | 2 + .../Extensions/DictionaryElementSyntax.swift | 3 + .../Extensions/DictionaryExprSyntax.swift | 3 + .../Extensions/EnumDeclSyntax.swift | 3 + .../Extensions/ExtensionDeclSyntax.swift | 2 + .../Extensions/InheritanceClauseSyntax.swift | 3 + .../Extensions/TypeAliasDeclSyntax.swift | 3 + .../Extensions/VariableDeclSyntax.swift | 3 + .../Sources/OptionsMacros/OptionsMacro.swift | 40 +++++++++++ Sources/SyntaxKit/CodeBlock+ExprSyntax.swift | 2 +- Sources/SyntaxKit/Infix.swift | 2 +- Sources/SyntaxKit/Tuple.swift | 2 +- Sources/SyntaxKit/TypeAlias.swift | 65 ++++++++++++++++++ project.yml | 2 + 17 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 Macros/Options/Package@swift-6.1.swift create mode 100644 Sources/SyntaxKit/TypeAlias.swift diff --git a/Macros/Options/Package@swift-6.1.swift b/Macros/Options/Package@swift-6.1.swift new file mode 100644 index 0000000..78d3f91 --- /dev/null +++ b/Macros/Options/Package@swift-6.1.swift @@ -0,0 +1,66 @@ +// swift-tools-version: 6.1 + +// swiftlint:disable explicit_top_level_acl +// swiftlint:disable prefixed_toplevel_constant +// swiftlint:disable explicit_acl + +import CompilerPluginSupport +import PackageDescription + +let swiftSettings = [ + SwiftSetting.enableUpcomingFeature("BareSlashRegexLiterals"), + SwiftSetting.enableUpcomingFeature("ConciseMagicFile"), + SwiftSetting.enableUpcomingFeature("ExistentialAny"), + SwiftSetting.enableUpcomingFeature("ForwardTrailingClosures"), + SwiftSetting.enableUpcomingFeature("ImplicitOpenExistentials"), + SwiftSetting.enableUpcomingFeature("StrictConcurrency"), + SwiftSetting.enableUpcomingFeature("DisableOutwardActorInference"), + SwiftSetting.enableExperimentalFeature("StrictConcurrency") +] + +let package = Package( + name: "Options", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + .visionOS(.v1) + ], + products: [ + .library( + name: "Options", + targets: ["Options"] + ) + ], + dependencies: [ + .package(path: "../.."), + .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1") + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0") + ], + targets: [ + .target( + name: "Options", + dependencies: ["OptionsMacros"], + swiftSettings: swiftSettings + ), + .macro( + name: "OptionsMacros", + dependencies: [ + .product(name: "SyntaxKit", package: "SyntaxKit"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "OptionsTests", + dependencies: ["Options"] + ) + ] +) + +// swiftlint:enable explicit_top_level_acl +// swiftlint:enable prefixed_toplevel_constant +// swiftlint:enable explicit_acl diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift index 2cab374..20e52d7 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/ArrayExprSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension ArrayExprSyntax { internal init( from items: some Collection, @@ -41,3 +43,4 @@ extension ArrayExprSyntax { self.init(elements: arrayElement) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift index c155633..bc36764 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierListSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension DeclModifierListSyntax { internal init(keywordModifier: Keyword?) { if let keywordModifier { @@ -40,3 +42,4 @@ extension DeclModifierListSyntax { } } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift index f4981a4..49d781c 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DeclModifierSyntax.swift @@ -29,6 +29,7 @@ import SwiftSyntax +#if !canImport(SyntaxKit) extension DeclModifierSyntax { internal var isNeededAccessLevelModifier: Bool { switch name.tokenKind { @@ -37,3 +38,4 @@ extension DeclModifierSyntax { } } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift index b719a24..810087b 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryElementSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension DictionaryElementSyntax { internal init(pair: (key: Int, value: String)) { self.init(key: pair.key, value: pair.value) @@ -45,3 +47,4 @@ extension DictionaryElementSyntax { ) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift index d21549b..025638d 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/DictionaryExprSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension DictionaryExprSyntax { internal init(keyValues: KeyValues) { let dictionaryElements = keyValues.dictionary.map(DictionaryElementSyntax.init(pair:)) @@ -39,3 +41,4 @@ extension DictionaryExprSyntax { self.init(content: .elements(list)) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift index 07035df..9ca3717 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/EnumDeclSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension EnumDeclSyntax { internal var caseElements: [EnumCaseElementSyntax] { memberBlock.members.flatMap { member in @@ -40,3 +42,4 @@ extension EnumDeclSyntax { } } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift index cf1eb6e..8f42eae 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/ExtensionDeclSyntax.swift @@ -29,6 +29,7 @@ import SwiftSyntax +#if !canImport(SyntaxKit) extension ExtensionDeclSyntax { internal init( enumDecl: EnumDeclSyntax, @@ -55,3 +56,4 @@ extension ExtensionDeclSyntax { ) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift index be7dc57..84156e9 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/InheritanceClauseSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension InheritanceClauseSyntax { internal init(protocols: [SwiftSyntax.TypeSyntax]) { self.init( @@ -42,3 +44,4 @@ extension InheritanceClauseSyntax { ) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift index b682f69..f3e24b2 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/TypeAliasDeclSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension TypeAliasDeclSyntax { internal init(name: TokenSyntax, for initializerTypeName: TokenSyntax) { self.init( @@ -37,3 +39,4 @@ extension TypeAliasDeclSyntax { ) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift b/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift index 48dd371..9b221a6 100644 --- a/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift +++ b/Macros/Options/Sources/OptionsMacros/Extensions/VariableDeclSyntax.swift @@ -29,6 +29,8 @@ import SwiftSyntax + +#if !canImport(SyntaxKit) extension VariableDeclSyntax { internal init( keywordModifier: Keyword?, @@ -79,3 +81,4 @@ extension VariableDeclSyntax { ) } } +#endif diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift index 8f93f5d..75b05a4 100644 --- a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -30,6 +30,45 @@ import SwiftSyntax import SwiftSyntaxMacros +#if canImport(SyntaxKit) +import SyntaxKit +public struct OptionsMacro: ExtensionMacro, PeerMacro { + public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw InvalidDeclError.kind(declaration.kind) + } + let typeName = enumDecl.name + + guard let extensionDeclSyntax : ExtensionDeclSyntax = .init(TypeAlias("\(typeName.trimmed)Set", equals: "EnumSet<\(typeName)>").expr) else { + throw InvalidDeclError.kind(declaration.kind) + } + return [ + extensionDeclSyntax + ] +// let aliasName: TokenSyntax = "\(typeName.trimmed)Set" +// +// let initializerName: TokenSyntax = "EnumSet<\(typeName)>" +// +// return [ +// .init(TypeAliasDeclSyntax(name: aliasName, for: initializerName)) +// ] + } + + public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw InvalidDeclError.kind(declaration.kind) + } + + fatalError() +// let extensionDecl = try ExtensionDeclSyntax( +// enumDecl: enumDecl, conformingTo: protocols +// ) +// return [extensionDecl] + } + +} +#else public struct OptionsMacro: ExtensionMacro, PeerMacro { public static func expansion( of _: SwiftSyntax.AttributeSyntax, @@ -67,3 +106,4 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { return [extensionDecl] } } +#endif diff --git a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift index 59b8ec9..4cf3469 100644 --- a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift +++ b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift @@ -27,4 +27,4 @@ extension CodeBlock { fatalError("CodeBlock of type \(type(of: self.syntax)) cannot be represented as ExprSyntax") } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/Infix.swift b/Sources/SyntaxKit/Infix.swift index 33a2825..ad18e0d 100644 --- a/Sources/SyntaxKit/Infix.swift +++ b/Sources/SyntaxKit/Infix.swift @@ -67,4 +67,4 @@ public struct Infix: CodeBlock { ]) ) } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/Tuple.swift b/Sources/SyntaxKit/Tuple.swift index 5becc9a..a294607 100644 --- a/Sources/SyntaxKit/Tuple.swift +++ b/Sources/SyntaxKit/Tuple.swift @@ -57,4 +57,4 @@ public struct Tuple: CodeBlock { return tupleExpr } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/TypeAlias.swift b/Sources/SyntaxKit/TypeAlias.swift new file mode 100644 index 0000000..7f8672b --- /dev/null +++ b/Sources/SyntaxKit/TypeAlias.swift @@ -0,0 +1,65 @@ +// +// TypeAlias.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +/// A Swift `typealias` declaration. +public struct TypeAlias: CodeBlock { + private let name: String + private let existingType: String + + /// Creates a `typealias` declaration. + /// - Parameters: + /// - name: The new name that will alias the existing type. + /// - type: The existing type that is being aliased. + public init(_ name: String, equals type: String) { + self.name = name + self.existingType = type + } + + public var syntax: SyntaxProtocol { + // `typealias` keyword token + let keyword = TokenSyntax.keyword(.typealias, trailingTrivia: .space) + + // Alias identifier + let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) + + // Initializer clause – `= ExistingType` + let initializer = TypeInitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: IdentifierTypeSyntax(name: .identifier(existingType)) + ) + + return TypeAliasDeclSyntax( + typealiasKeyword: keyword, + name: identifier, + initializer: initializer + ) + } +} diff --git a/project.yml b/project.yml index b2cfbdc..959c793 100644 --- a/project.yml +++ b/project.yml @@ -6,6 +6,8 @@ packages: path: . SKSampleMacro: path: Macros/SKSampleMacro + Options: + path: Macros/Options aggregateTargets: Lint: buildScripts: From 8db2e538f75b17ea111f09825079632ad479f4bc Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 17 Jun 2025 20:43:03 -0400 Subject: [PATCH 4/9] fixing expansion --- .../Sources/OptionsMacros/OptionsMacro.swift | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift index 75b05a4..d3668d9 100644 --- a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -35,24 +35,14 @@ import SyntaxKit public struct OptionsMacro: ExtensionMacro, PeerMacro { public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] { - guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { - throw InvalidDeclError.kind(declaration.kind) - } - let typeName = enumDecl.name - - guard let extensionDeclSyntax : ExtensionDeclSyntax = .init(TypeAlias("\(typeName.trimmed)Set", equals: "EnumSet<\(typeName)>").expr) else { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw InvalidDeclError.kind(declaration.kind) } - return [ - extensionDeclSyntax - ] -// let aliasName: TokenSyntax = "\(typeName.trimmed)Set" -// -// let initializerName: TokenSyntax = "EnumSet<\(typeName)>" -// -// return [ -// .init(TypeAliasDeclSyntax(name: aliasName, for: initializerName)) -// ] + + let extensionDecl = try ExtensionDeclSyntax( + enumDecl: enumDecl, conformingTo: protocols + ) + return [extensionDecl] } public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { @@ -60,11 +50,14 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { throw InvalidDeclError.kind(declaration.kind) } - fatalError() -// let extensionDecl = try ExtensionDeclSyntax( -// enumDecl: enumDecl, conformingTo: protocols -// ) -// return [extensionDecl] + let typeName = enumDecl.name + + guard let declSyntax : DeclSyntax = .init(TypeAlias("\(typeName.trimmed)Set", equals: "EnumSet<\(typeName)>").expr) else { + throw InvalidDeclError.kind(declaration.kind) + } + return [ + declSyntax + ] } } From 09b5dfc60cde14fa297828b9e286f57d5e707636 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 17 Jun 2025 21:03:57 -0400 Subject: [PATCH 5/9] Adding Extension --- Macros/Options/Package@swift-6.1.swift | 16 +------ .../Sources/OptionsMacros/OptionsMacro.swift | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Macros/Options/Package@swift-6.1.swift b/Macros/Options/Package@swift-6.1.swift index 78d3f91..402bd68 100644 --- a/Macros/Options/Package@swift-6.1.swift +++ b/Macros/Options/Package@swift-6.1.swift @@ -7,16 +7,6 @@ import CompilerPluginSupport import PackageDescription -let swiftSettings = [ - SwiftSetting.enableUpcomingFeature("BareSlashRegexLiterals"), - SwiftSetting.enableUpcomingFeature("ConciseMagicFile"), - SwiftSetting.enableUpcomingFeature("ExistentialAny"), - SwiftSetting.enableUpcomingFeature("ForwardTrailingClosures"), - SwiftSetting.enableUpcomingFeature("ImplicitOpenExistentials"), - SwiftSetting.enableUpcomingFeature("StrictConcurrency"), - SwiftSetting.enableUpcomingFeature("DisableOutwardActorInference"), - SwiftSetting.enableExperimentalFeature("StrictConcurrency") -] let package = Package( name: "Options", @@ -42,8 +32,7 @@ let package = Package( targets: [ .target( name: "Options", - dependencies: ["OptionsMacros"], - swiftSettings: swiftSettings + dependencies: ["OptionsMacros"] ), .macro( name: "OptionsMacros", @@ -51,8 +40,7 @@ let package = Package( .product(name: "SyntaxKit", package: "SyntaxKit"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") - ], - swiftSettings: swiftSettings + ] ), .testTarget( name: "OptionsTests", diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift index d3668d9..87070a6 100644 --- a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -39,10 +39,46 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { throw InvalidDeclError.kind(declaration.kind) } - let extensionDecl = try ExtensionDeclSyntax( - enumDecl: enumDecl, conformingTo: protocols + // Build `typealias Set = EnumSet` + let typeName = enumDecl.name + let aliasName = "\(typeName.trimmed)Set" + let aliasDecl = TypeAlias(aliasName, equals: "EnumSet<\(typeName)>").syntax + + let memberItem = MemberBlockItemSyntax( + decl: DeclSyntax(aliasDecl.as(TypeAliasDeclSyntax.self)! ), + trailingTrivia: .newline ) - return [extensionDecl] + + // Build member block + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax([memberItem]), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + // Build inheritance clause from `protocols` argument + let inheritanceClause: InheritanceClauseSyntax? = protocols.isEmpty ? nil : InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + protocols.enumerated().map { idx, proto in + var inherited = InheritedTypeSyntax(type: proto) + if idx < protocols.count - 1 { + inherited = inherited.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return inherited + } + ) + ) + + // Assemble extension + let extDecl = ExtensionDeclSyntax( + modifiers: DeclModifierListSyntax([]), + extendedType: IdentifierTypeSyntax(name: typeName), + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + + return [extDecl] } public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { @@ -50,7 +86,7 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { throw InvalidDeclError.kind(declaration.kind) } - let typeName = enumDecl.name + let typeName = enumDecl.name guard let declSyntax : DeclSyntax = .init(TypeAlias("\(typeName.trimmed)Set", equals: "EnumSet<\(typeName)>").expr) else { throw InvalidDeclError.kind(declaration.kind) From a60ab4e7e59f3923ae4481254a080f42e253d2ed Mon Sep 17 00:00:00 2001 From: leogdion Date: Wed, 18 Jun 2025 09:16:16 -0400 Subject: [PATCH 6/9] expanding the existing code for Options Macro --- .../Sources/OptionsMacros/OptionsMacro.swift | 130 ++++++++++++++---- .../Mocks/MockCollectionEnum.swift | 2 +- .../Tests/OptionsTests/Mocks/MockError.swift | 2 +- 3 files changed, 109 insertions(+), 25 deletions(-) diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift index 87070a6..313baf7 100644 --- a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -34,26 +34,108 @@ import SwiftSyntaxMacros import SyntaxKit public struct OptionsMacro: ExtensionMacro, PeerMacro { public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] { - guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw InvalidDeclError.kind(declaration.kind) } - // Build `typealias Set = EnumSet` + // Extract the type name let typeName = enumDecl.name - let aliasName = "\(typeName.trimmed)Set" - let aliasDecl = TypeAlias(aliasName, equals: "EnumSet<\(typeName)>").syntax - let memberItem = MemberBlockItemSyntax( - decl: DeclSyntax(aliasDecl.as(TypeAliasDeclSyntax.self)! ), - trailingTrivia: .newline + // Extract all EnumCaseElementSyntax from the enum + let caseElements: [EnumCaseElementSyntax] = enumDecl.memberBlock.members.flatMap { (member) -> [EnumCaseElementSyntax] in + guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { + return [EnumCaseElementSyntax]() + } + return Array(caseDecl.elements) + } + + // Build mappedValues variable declaration (static let mappedValues = [...]) + // Check if any case has a raw value to determine if we need a dictionary + let hasRawValues = caseElements.contains { $0.rawValue != nil } + + let mappedValuesExpr: ExprSyntax + if hasRawValues { + // Create dictionary mapping raw values to case names + let dictionaryElements = caseElements.compactMap { element -> DictionaryElementSyntax? in + guard let rawValue = element.rawValue?.value.as(IntegerLiteralExprSyntax.self)?.literal.text, + let key = Int(rawValue) else { + return nil + } + let value = element.name.trimmed.text + return DictionaryElementSyntax( + key: IntegerLiteralExprSyntax(digits: .integerLiteral(String(key))), + value: StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: .init([.stringSegment(.init(content: .stringSegment(value)))]), + closingQuote: .stringQuoteToken() + ) + ) + } + + let dictionaryList = DictionaryElementListSyntax( + dictionaryElements.enumerated().map { (idx, element) in + var dictElement = element + if idx < dictionaryElements.count - 1 { + dictElement = dictElement.with(\.trailingComma, TokenSyntax.commaToken(trailingTrivia: .space)) + } + return dictElement + } + ) + + mappedValuesExpr = ExprSyntax(DictionaryExprSyntax( + content: .elements(dictionaryList) + )) + } else { + // Create array of case names + mappedValuesExpr = ExprSyntax(ArrayExprSyntax( + elements: ArrayElementListSyntax( + caseElements.enumerated().map { (idx, element) in + ArrayElementSyntax( + expression: ExprSyntax(StringLiteralExprSyntax( + openingQuote: .stringQuoteToken(), + segments: .init([ + .stringSegment(.init(content: .stringSegment(element.name.trimmed.text))) + ]), + closingQuote: .stringQuoteToken() + )), + trailingComma: idx < caseElements.count - 1 ? TokenSyntax.commaToken(trailingTrivia: .space) : nil + ) + } + ) + )) + } + let mappedValuesDecl = VariableDeclSyntax( + modifiers: DeclModifierListSyntax { + DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) + }, + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + bindings: PatternBindingListSyntax { + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: .identifier("mappedValues")), + initializer: InitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: mappedValuesExpr + ) + ) + } + ) + + // Build typealias MappedType = String + let mappedTypeAlias = TypeAliasDeclSyntax( + typealiasKeyword: .keyword(.typealias, trailingTrivia: .space), + name: .identifier("MappedType", trailingTrivia: .space), + initializer: TypeInitializerClauseSyntax( + equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), + value: IdentifierTypeSyntax(name: .identifier("String")) + ) ) // Build member block let memberBlock = MemberBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - members: MemberBlockItemListSyntax([memberItem]), - rightBrace: .rightBraceToken(leadingTrivia: .newline) + members: MemberBlockItemListSyntax { + MemberBlockItemSyntax(decl: DeclSyntax(mappedTypeAlias), trailingTrivia: .newline) + MemberBlockItemSyntax(decl: DeclSyntax(mappedValuesDecl), trailingTrivia: .newline) + } ) // Build inheritance clause from `protocols` argument @@ -63,7 +145,7 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { protocols.enumerated().map { idx, proto in var inherited = InheritedTypeSyntax(type: proto) if idx < protocols.count - 1 { - inherited = inherited.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + inherited = inherited.with(\.trailingComma, TokenSyntax.commaToken(trailingTrivia: .space)) } return inherited } @@ -87,13 +169,15 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { } let typeName = enumDecl.name - - guard let declSyntax : DeclSyntax = .init(TypeAlias("\(typeName.trimmed)Set", equals: "EnumSet<\(typeName)>").expr) else { - throw InvalidDeclError.kind(declaration.kind) - } - return [ - declSyntax - ] + let aliasName = "\(typeName.trimmed)Set" + let aliasDecl = TypeAlias(aliasName, equals: "EnumSet<\(typeName)>").syntax + + guard let declSyntax : DeclSyntax = DeclSyntax(aliasDecl.as(TypeAliasDeclSyntax.self)) else { + throw InvalidDeclError.kind(declaration.kind) + } + return [ + declSyntax + ] } } @@ -108,16 +192,16 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { throw InvalidDeclError.kind(declaration.kind) } let typeName = enumDecl.name - + let aliasName: TokenSyntax = "\(typeName.trimmed)Set" - + let initializerName: TokenSyntax = "EnumSet<\(typeName)>" - + return [ .init(TypeAliasDeclSyntax(name: aliasName, for: initializerName)) ] } - + public static func expansion( of _: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, @@ -128,7 +212,7 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw InvalidDeclError.kind(declaration.kind) } - + let extensionDecl = try ExtensionDeclSyntax( enumDecl: enumDecl, conformingTo: protocols ) diff --git a/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift b/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift index 45a530a..6dc0e00 100644 --- a/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift +++ b/Macros/Options/Tests/OptionsTests/Mocks/MockCollectionEnum.swift @@ -38,7 +38,7 @@ import Options case c case d - static var codingOptions: CodingOptions = .default + nonisolated(unsafe) static var codingOptions: CodingOptions = .default } #else // swiftlint:disable identifier_name diff --git a/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift b/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift index 1a7037b..9915aeb 100644 --- a/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift +++ b/Macros/Options/Tests/OptionsTests/Mocks/MockError.swift @@ -29,6 +29,6 @@ import Foundation -internal struct MockError: Error { +internal struct MockError: Error { internal let value: T } From c4f6a35aff2fae7cc86ed30ea1482d4a1a91cbf0 Mon Sep 17 00:00:00 2001 From: leogdion Date: Wed, 18 Jun 2025 09:58:43 -0400 Subject: [PATCH 7/9] Working Options sample --- .../Sources/OptionsMacros/OptionsMacro.swift | 30 ++- Sources/SyntaxKit/Extension.swift | 96 ++++++++ Sources/SyntaxKit/Literal.swift | 51 +++- Sources/SyntaxKit/Variable.swift | 33 ++- Tests/SyntaxKitTests/ExtensionTests.swift | 179 ++++++++++++++ Tests/SyntaxKitTests/LiteralValueTests.swift | 112 +++++++++ .../OptionsMacroIntegrationTests.swift | 220 ++++++++++++++++++ Tests/SyntaxKitTests/TypeAliasTests.swift | 177 ++++++++++++++ .../SyntaxKitTests/VariableStaticTests.swift | 155 ++++++++++++ 9 files changed, 1048 insertions(+), 5 deletions(-) create mode 100644 Sources/SyntaxKit/Extension.swift create mode 100644 Tests/SyntaxKitTests/ExtensionTests.swift create mode 100644 Tests/SyntaxKitTests/LiteralValueTests.swift create mode 100644 Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift create mode 100644 Tests/SyntaxKitTests/TypeAliasTests.swift create mode 100644 Tests/SyntaxKitTests/VariableStaticTests.swift diff --git a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift index 313baf7..aa17029 100644 --- a/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift +++ b/Macros/Options/Sources/OptionsMacros/OptionsMacro.swift @@ -104,6 +104,7 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { ) )) } + let mappedValuesDecl = VariableDeclSyntax( modifiers: DeclModifierListSyntax { DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) @@ -160,7 +161,34 @@ public struct OptionsMacro: ExtensionMacro, PeerMacro { memberBlock: memberBlock ) - return [extDecl] + //return [extDecl] + + // NOTE: Once SyntaxKit is properly added as a dependency, this could be simplified to: + + let mappedValuesVariable: Variable + if hasRawValues { + let keyValues: [Int: String] = caseElements.reduce(into: [:]) { (result, element) in + guard let rawValue = element.rawValue?.value.as(IntegerLiteralExprSyntax.self)?.literal.text, + let key = Int(rawValue) else { + return + } + result[key] = element.name.trimmed.text + } + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: keyValues).static() + } else { + let caseNames: [String] = caseElements.map { element in + element.name.trimmed.text + } + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: caseNames).static() + } + + let extensionDecl = Extension(typeName.trimmed.text) { + TypeAlias("MappedType", equals: "String") + mappedValuesVariable + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + return [extensionDecl.syntax.as(ExtensionDeclSyntax.self)!] + } public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { diff --git a/Sources/SyntaxKit/Extension.swift b/Sources/SyntaxKit/Extension.swift new file mode 100644 index 0000000..cb93a5b --- /dev/null +++ b/Sources/SyntaxKit/Extension.swift @@ -0,0 +1,96 @@ +// +// Extension.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import SwiftSyntax + +/// A Swift `extension` declaration. +public struct Extension: CodeBlock { + private let extendedType: String + private let members: [CodeBlock] + private var inheritance: [String] = [] + + /// Creates an `extension` declaration. + /// - Parameters: + /// - extendedType: The name of the type being extended. + /// - content: A ``CodeBlockBuilder`` that provides the members of the extension. + public init(_ extendedType: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.extendedType = extendedType + self.members = content() + } + + /// Sets the inheritance for the extension. + /// - Parameter types: The types to inherit from. + /// - Returns: A copy of the extension with the inheritance set. + public func inherits(_ types: String...) -> Self { + var copy = self + copy.inheritance = types + return copy + } + + public var syntax: SyntaxProtocol { + let extensionKeyword = TokenSyntax.keyword(.extension, trailingTrivia: .space) + let identifier = TokenSyntax.identifier(extendedType, trailingTrivia: .space) + + var inheritanceClause: InheritanceClauseSyntax? + if !inheritance.isEmpty { + let inheritedTypes = inheritance.map { type in + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier(type))) + } + inheritanceClause = InheritanceClauseSyntax( + colon: .colonToken(), + inheritedTypes: InheritedTypeListSyntax( + inheritedTypes.enumerated().map { idx, inherited in + var type = inherited + if idx < inheritedTypes.count - 1 { + type = type.with(\.trailingComma, TokenSyntax.commaToken(trailingTrivia: .space)) + } + return type + } + ) + ) + } + + let memberBlock = MemberBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + members: MemberBlockItemListSyntax( + members.compactMap { member in + guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) + }), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + return ExtensionDeclSyntax( + extensionKeyword: extensionKeyword, + extendedType: IdentifierTypeSyntax(name: identifier), + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + } +} \ No newline at end of file diff --git a/Sources/SyntaxKit/Literal.swift b/Sources/SyntaxKit/Literal.swift index 7e0e1dd..46a07d2 100644 --- a/Sources/SyntaxKit/Literal.swift +++ b/Sources/SyntaxKit/Literal.swift @@ -7,7 +7,7 @@ // // 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -29,6 +29,15 @@ import SwiftSyntax +/// A protocol for types that can be represented as literal values in Swift code. +public protocol LiteralValue { + /// The Swift type name for this literal value. + var typeName: String { get } + + /// Renders this value as a Swift literal string. + var literalString: String { get } +} + /// A literal value. public enum Literal: CodeBlock { /// A string literal. @@ -64,3 +73,41 @@ public enum Literal: CodeBlock { } } } + +// MARK: - LiteralValue Implementations + +extension Array: LiteralValue where Element == String { + public var typeName: String { "[String]" } + + public var literalString: String { + let elements = self.map { element in + // Escape quotes and newlines + let escaped = element + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + return "\"\(escaped)\"" + }.joined(separator: ", ") + return "[\(elements)]" + } +} + +extension Dictionary: LiteralValue where Key == Int, Value == String { + public var typeName: String { "[Int: String]" } + + public var literalString: String { + let elements = self.map { key, value in + // Escape quotes and newlines + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\t", with: "\\t") + return "\(key): \"\(escaped)\"" + }.joined(separator: ", ") + return "[\(elements)]" + } +} diff --git a/Sources/SyntaxKit/Variable.swift b/Sources/SyntaxKit/Variable.swift index b37ec0d..0fa9a9d 100644 --- a/Sources/SyntaxKit/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -7,7 +7,7 @@ // // 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -35,6 +35,7 @@ public struct Variable: CodeBlock { private let name: String private let type: String private let defaultValue: String? + private var isStatic: Bool = false /// Creates a `let` or `var` declaration with an explicit type. /// - Parameters: @@ -49,6 +50,26 @@ public struct Variable: CodeBlock { self.type = type self.defaultValue = defaultValue } + + /// Creates a `let` or `var` declaration with a literal value. + /// - Parameters: + /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. + /// - name: The name of the variable. + /// - value: A literal value that conforms to ``LiteralValue``. + public init(_ kind: VariableKind, name: String, equals value: T) { + self.kind = kind + self.name = name + self.type = value.typeName + self.defaultValue = value.literalString + } + + /// Marks the variable as `static`. + /// - Returns: A copy of the variable marked as `static`. + public func `static`() -> Self { + var copy = self + copy.isStatic = true + return copy + } public var syntax: SyntaxProtocol { let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) @@ -64,8 +85,16 @@ public struct Variable: CodeBlock { value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) ) } + + var modifiers: DeclModifierListSyntax = [] + if isStatic { + modifiers = DeclModifierListSyntax([ + DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) + ]) + } return VariableDeclSyntax( + modifiers: modifiers, bindingSpecifier: bindingKeyword, bindings: PatternBindingListSyntax([ PatternBindingSyntax( diff --git a/Tests/SyntaxKitTests/ExtensionTests.swift b/Tests/SyntaxKitTests/ExtensionTests.swift new file mode 100644 index 0000000..5d30dc5 --- /dev/null +++ b/Tests/SyntaxKitTests/ExtensionTests.swift @@ -0,0 +1,179 @@ +// +// ExtensionTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import Testing + +@testable import SyntaxKit + +struct ExtensionTests { + + // MARK: - Basic Extension Tests + + @Test func testBasicExtension() { + let extensionDecl = Extension("String") { + Variable(.let, name: "test", type: "Int", equals: "42") + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension String")) + #expect(generated.contains("let test: Int = 42")) + } + + @Test func testExtensionWithMultipleMembers() { + let extensionDecl = Extension("Array") { + Variable(.let, name: "isEmpty", type: "Bool", equals: "true") + Variable(.let, name: "count", type: "Int", equals: "0") + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension Array")) + #expect(generated.contains("let isEmpty: Bool = true")) + #expect(generated.contains("let count: Int = 0")) + } + + // MARK: - Extension with Inheritance Tests + + @Test func testExtensionWithSingleInheritance() { + let extensionDecl = Extension("MyEnum") { + TypeAlias("MappedType", equals: "String") + }.inherits("MappedValueRepresentable") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) + #expect(generated.contains("typealias MappedType = String")) + } + + @Test func testExtensionWithMultipleInheritance() { + let extensionDecl = Extension("MyEnum") { + TypeAlias("MappedType", equals: "String") + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + } + + @Test func testExtensionWithoutInheritance() { + let extensionDecl = Extension("MyType") { + Variable(.let, name: "constant", type: "String", equals: "value") + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyType")) + #expect(!generated.contains("extension MyType:")) + #expect(generated.contains("let constant: String = value")) + } + + // MARK: - Extension with Complex Members Tests + + @Test func testExtensionWithStaticVariables() { + let array: [String] = ["a", "b", "c"] + let dict: [Int: String] = [1: "one", 2: "two"] + + let extensionDecl = Extension("TestEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: array).static() + Variable(.let, name: "lookup", equals: dict).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) + #expect(generated.contains("static let lookup: [Int: String]")) + #expect(generated.contains("1: \"one\"")) + #expect(generated.contains("2: \"two\"")) + } + + @Test func testExtensionWithFunctions() { + let extensionDecl = Extension("String") { + Function("uppercasedFirst", returns: "String") { + Return { + VariableExp("self.prefix(1).uppercased() + self.dropFirst()") + } + } + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension String")) + #expect(generated.contains("func uppercasedFirst() -> String")) + #expect(generated.contains("return self.prefix(1).uppercased() + self.dropFirst()")) + } + + // MARK: - Edge Cases + + @Test func testExtensionWithEmptyBody() { + let extensionDecl = Extension("EmptyType") { + // Empty body + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension EmptyType")) + #expect(generated.contains("{")) + #expect(generated.contains("}")) + } + + @Test func testExtensionWithSpecialCharactersInName() { + let extensionDecl = Extension("MyType") { + Variable(.let, name: "generic", type: "T", equals: "nil") + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyType")) + #expect(generated.contains("let generic: T = nil")) + } + + @Test func testInheritsMethodReturnsNewInstance() { + let original = Extension("Test") { + Variable(.let, name: "value", type: "Int", equals: "42") + } + + let withInheritance = original.inherits("Protocol1", "Protocol2") + + // Should be different instances + #expect(original.generateCode() != withInheritance.generateCode()) + + // Original should not have inheritance + let originalGenerated = original.generateCode().normalize() + #expect(!originalGenerated.contains("extension Test:")) + + // With inheritance should have inheritance + let inheritedGenerated = withInheritance.generateCode().normalize() + #expect(inheritedGenerated.contains(": Protocol1, Protocol2")) + } +} \ No newline at end of file diff --git a/Tests/SyntaxKitTests/LiteralValueTests.swift b/Tests/SyntaxKitTests/LiteralValueTests.swift new file mode 100644 index 0000000..ef217ae --- /dev/null +++ b/Tests/SyntaxKitTests/LiteralValueTests.swift @@ -0,0 +1,112 @@ +// +// LiteralValueTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import Testing + +@testable import SyntaxKit + +struct LiteralValueTests { + + // MARK: - Array LiteralValue Tests + + @Test func testArrayStringTypeName() { + let array: [String] = ["a", "b", "c"] + #expect(array.typeName == "[String]") + } + + @Test func testArrayStringLiteralString() { + let array: [String] = ["a", "b", "c"] + #expect(array.literalString == "[\"a\", \"b\", \"c\"]") + } + + @Test func testEmptyArrayStringLiteralString() { + let array: [String] = [] + #expect(array.literalString == "[]") + } + + @Test func testArrayStringWithSpecialCharacters() { + let array: [String] = ["hello world", "test\"quote", "line\nbreak"] + #expect(array.literalString == "[\"hello world\", \"test\\\"quote\", \"line\\nbreak\"]") + } + + // MARK: - Dictionary LiteralValue Tests + + @Test func testDictionaryIntStringTypeName() { + let dict: [Int: String] = [1: "a", 2: "b"] + #expect(dict.typeName == "[Int: String]") + } + + @Test func testDictionaryIntStringLiteralString() { + let dict: [Int: String] = [1: "a", 2: "b", 3: "c"] + let literal = dict.literalString + + // Dictionary order is not guaranteed, so check that all elements are present + #expect(literal.contains("1: \"a\"")) + #expect(literal.contains("2: \"b\"")) + #expect(literal.contains("3: \"c\"")) + #expect(literal.hasPrefix("[")) + #expect(literal.hasSuffix("]")) + } + + @Test func testEmptyDictionaryLiteralString() { + let dict: [Int: String] = [:] + #expect(dict.literalString == "[]") + } + + @Test func testDictionaryWithSpecialCharacters() { + let dict: [Int: String] = [1: "hello world", 2: "test\"quote"] + let literal = dict.literalString + + // Dictionary order is not guaranteed, so check that all elements are present + #expect(literal.contains("1: \"hello world\"")) + #expect(literal.contains("2: \"test\\\"quote\"")) + #expect(literal.hasPrefix("[")) + #expect(literal.hasSuffix("]")) + } + + // MARK: - Dictionary Ordering Tests + + @Test func testDictionaryOrderingIsConsistent() { + let dict1: [Int: String] = [2: "b", 1: "a", 3: "c"] + let dict2: [Int: String] = [1: "a", 2: "b", 3: "c"] + + // Both should produce the same literal string regardless of insertion order + let literal1 = dict1.literalString + let literal2 = dict2.literalString + + // The exact order depends on the dictionary's internal ordering, + // but both should be valid Swift dictionary literals + #expect(literal1.contains("1: \"a\"")) + #expect(literal1.contains("2: \"b\"")) + #expect(literal1.contains("3: \"c\"")) + #expect(literal2.contains("1: \"a\"")) + #expect(literal2.contains("2: \"b\"")) + #expect(literal2.contains("3: \"c\"")) + } +} \ No newline at end of file diff --git a/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift b/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift new file mode 100644 index 0000000..060c1d1 --- /dev/null +++ b/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift @@ -0,0 +1,220 @@ +// +// OptionsMacroIntegrationTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import Testing + +@testable import SyntaxKit + +struct OptionsMacroIntegrationTests { + + // MARK: - Enum with Raw Values (Dictionary) Tests + + @Test func testEnumWithRawValuesCreatesDictionary() { + // Simulate the Options macro expansion for an enum with raw values + let keyValues: [Int: String] = [2: "a", 5: "b", 6: "c", 12: "d"] + + let extensionDecl = Extension("MockDictionaryEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: keyValues).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MockDictionaryEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [Int: String]")) + #expect(generated.contains("2: \"a\"")) + #expect(generated.contains("5: \"b\"")) + #expect(generated.contains("6: \"c\"")) + #expect(generated.contains("12: \"d\"")) + } + + @Test func testEnumWithoutRawValuesCreatesArray() { + // Simulate the Options macro expansion for an enum without raw values + let caseNames: [String] = ["red", "green", "blue"] + + let extensionDecl = Extension("Color") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: caseNames).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension Color: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String] = [\"red\", \"green\", \"blue\"]")) + } + + // MARK: - Complex Integration Tests + + @Test func testCompleteOptionsMacroWorkflow() { + // This test demonstrates the complete workflow that the Options macro would use + + // Step 1: Determine if enum has raw values (simulated) + let hasRawValues = true + let enumName = "TestEnum" + + // Step 2: Create the appropriate mappedValues variable + let mappedValuesVariable: Variable + if hasRawValues { + let keyValues: [Int: String] = [1: "first", 2: "second", 3: "third"] + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: keyValues).static() + } else { + let caseNames: [String] = ["first", "second", "third"] + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: caseNames).static() + } + + // Step 3: Create the extension + let extensionDecl = Extension(enumName) { + TypeAlias("MappedType", equals: "String") + mappedValuesVariable + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + // Verify the complete extension + #expect(generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [Int: String]")) + #expect(generated.contains("1: \"first\"")) + #expect(generated.contains("2: \"second\"")) + #expect(generated.contains("3: \"third\"")) + } + + @Test func testOptionsMacroWorkflowWithoutRawValues() { + // Test the workflow for enums without raw values + + let hasRawValues = false + let enumName = "SimpleEnum" + + let mappedValuesVariable: Variable + if hasRawValues { + let keyValues: [Int: String] = [1: "first", 2: "second"] + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: keyValues).static() + } else { + let caseNames: [String] = ["first", "second"] + mappedValuesVariable = Variable(.let, name: "mappedValues", equals: caseNames).static() + } + + let extensionDecl = Extension(enumName) { + TypeAlias("MappedType", equals: "String") + mappedValuesVariable + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension SimpleEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String] = [\"first\", \"second\"]")) + } + + // MARK: - Edge Cases + + @Test func testEmptyEnumCases() { + let caseNames: [String] = [] + + let extensionDecl = Extension("EmptyEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: caseNames).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension EmptyEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String] = []")) + } + + @Test func testEmptyDictionary() { + let keyValues: [Int: String] = [:] + + let extensionDecl = Extension("EmptyDictEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: keyValues).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension EmptyDictEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [Int: String] = []")) + } + + @Test func testSpecialCharactersInCaseNames() { + let caseNames: [String] = ["case_with_underscore", "case-with-dash", "caseWithCamelCase"] + + let extensionDecl = Extension("SpecialEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: caseNames).static() + }.inherits("MappedValueRepresentable", "MappedValueRepresented") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension SpecialEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String]")) + #expect(generated.contains("\"case_with_underscore\"")) + #expect(generated.contains("\"case-with-dash\"")) + #expect(generated.contains("\"caseWithCamelCase\"")) + } + + // MARK: - API Validation Tests + + @Test func testNewSyntaxKitAPICompleteness() { + // Verify that all the new API components work together correctly + + // Test LiteralValue protocol + let array: [String] = ["a", "b", "c"] + #expect(array.typeName == "[String]") + #expect(array.literalString == "[\"a\", \"b\", \"c\"]") + + let dict: [Int: String] = [1: "a", 2: "b"] + #expect(dict.typeName == "[Int: String]") + #expect(dict.literalString.contains("1: \"a\"")) + #expect(dict.literalString.contains("2: \"b\"")) + + // Test Variable with static support + let staticVar = Variable(.let, name: "test", equals: array).static() + let staticGenerated = staticVar.generateCode().normalize() + #expect(staticGenerated.contains("static let test: [String] = [\"a\", \"b\", \"c\"]")) + + // Test Extension with inheritance + let ext = Extension("Test") { + // Empty content + }.inherits("Protocol1", "Protocol2") + + let extGenerated = ext.generateCode().normalize() + #expect(extGenerated.contains("extension Test: Protocol1, Protocol2")) + + // Test TypeAlias + let alias = TypeAlias("MyType", equals: "String") + let aliasGenerated = alias.generateCode().normalize() + #expect(aliasGenerated.contains("typealias MyType = String")) + } +} \ No newline at end of file diff --git a/Tests/SyntaxKitTests/TypeAliasTests.swift b/Tests/SyntaxKitTests/TypeAliasTests.swift new file mode 100644 index 0000000..e6b4f9f --- /dev/null +++ b/Tests/SyntaxKitTests/TypeAliasTests.swift @@ -0,0 +1,177 @@ +// +// TypeAliasTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import Testing + +@testable import SyntaxKit + +struct TypeAliasTests { + + // MARK: - Basic TypeAlias Tests + + @Test func testBasicTypeAlias() { + let typeAlias = TypeAlias("MappedType", equals: "String") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias MappedType = String")) + } + + @Test func testTypeAliasWithComplexType() { + let typeAlias = TypeAlias("ResultType", equals: "Result") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias ResultType = Result")) + } + + @Test func testTypeAliasWithGenericType() { + let typeAlias = TypeAlias("ArrayType", equals: "Array") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias ArrayType = Array")) + } + + @Test func testTypeAliasWithOptionalType() { + let typeAlias = TypeAlias("OptionalString", equals: "String?") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias OptionalString = String?")) + } + + // MARK: - TypeAlias in Context Tests + + @Test func testTypeAliasInExtension() { + let extensionDecl = Extension("MyEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "test", type: "MappedType", equals: "value") + } + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyEnum")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("let test: MappedType = value")) + } + + @Test func testTypeAliasInStruct() { + let structDecl = Struct("Container") { + TypeAlias("ElementType", equals: "String") + Variable(.let, name: "element", type: "ElementType") + } + + let generated = structDecl.generateCode().normalize() + + #expect(generated.contains("struct Container")) + #expect(generated.contains("typealias ElementType = String")) + #expect(generated.contains("let element: ElementType")) + } + + @Test func testTypeAliasInEnum() { + let enumDecl = Enum("Result") { + TypeAlias("SuccessType", equals: "String") + TypeAlias("FailureType", equals: "Error") + EnumCase("success") + EnumCase("failure") + } + + let generated = enumDecl.generateCode().normalize() + + #expect(generated.contains("enum Result")) + #expect(generated.contains("typealias SuccessType = String")) + #expect(generated.contains("typealias FailureType = Error")) + #expect(generated.contains("case success")) + #expect(generated.contains("case failure")) + } + + // MARK: - Edge Cases + + @Test func testTypeAliasWithSpecialCharacters() { + let typeAlias = TypeAlias("GenericType", equals: "Array") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias GenericType = Array")) + } + + @Test func testTypeAliasWithProtocolComposition() { + let typeAlias = TypeAlias("ProtocolType", equals: "Protocol1 & Protocol2") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias ProtocolType = Protocol1 & Protocol2")) + } + + @Test func testTypeAliasWithFunctionType() { + let typeAlias = TypeAlias("Handler", equals: "(String) -> Void") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias Handler = (String) -> Void")) + } + + @Test func testTypeAliasWithTupleType() { + let typeAlias = TypeAlias("Coordinate", equals: "(x: Double, y: Double)") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias Coordinate = (x: Double, y: Double)")) + } + + @Test func testTypeAliasWithClosureType() { + let typeAlias = TypeAlias("Callback", equals: "@escaping (Result) -> Void") + let generated = typeAlias.generateCode().normalize() + + #expect(generated.contains("typealias Callback = @escaping (Result) -> Void")) + } + + // MARK: - Integration Tests + + @Test func testTypeAliasWithStaticVariable() { + let extensionDecl = Extension("MyEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: ["a", "b", "c"]).static() + }.inherits("MappedValueRepresentable") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) + } + + @Test func testTypeAliasWithDictionaryVariable() { + let extensionDecl = Extension("MyEnum") { + TypeAlias("MappedType", equals: "String") + Variable(.let, name: "mappedValues", equals: [1: "a", 2: "b"]).static() + }.inherits("MappedValueRepresentable") + + let generated = extensionDecl.generateCode().normalize() + + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) + #expect(generated.contains("typealias MappedType = String")) + #expect(generated.contains("static let mappedValues: [Int: String]")) + #expect(generated.contains("1: \"a\"")) + #expect(generated.contains("2: \"b\"")) + } +} \ No newline at end of file diff --git a/Tests/SyntaxKitTests/VariableStaticTests.swift b/Tests/SyntaxKitTests/VariableStaticTests.swift new file mode 100644 index 0000000..767fdc5 --- /dev/null +++ b/Tests/SyntaxKitTests/VariableStaticTests.swift @@ -0,0 +1,155 @@ +// +// VariableStaticTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// 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. +// + +import Testing + +@testable import SyntaxKit + +struct VariableStaticTests { + + // MARK: - Static Variable Tests + + @Test func testStaticVariableWithStringLiteral() { + let variable = Variable(.let, name: "test", type: "String", equals: "hello").static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static let test: String = hello")) + } + + @Test func testStaticVariableWithArrayLiteral() { + let array: [String] = ["a", "b", "c"] + let variable = Variable(.let, name: "mappedValues", equals: array).static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) + } + + @Test func testStaticVariableWithDictionaryLiteral() { + let dict: [Int: String] = [1: "a", 2: "b", 3: "c"] + let variable = Variable(.let, name: "mappedValues", equals: dict).static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static let mappedValues: [Int: String]")) + #expect(generated.contains("1: \"a\"")) + #expect(generated.contains("2: \"b\"")) + #expect(generated.contains("3: \"c\"")) + } + + @Test func testStaticVariableWithVar() { + let variable = Variable(.var, name: "counter", type: "Int", equals: "0").static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static var counter: Int = 0")) + } + + // MARK: - Non-Static Variable Tests + + @Test func testNonStaticVariableWithLiteral() { + let array: [String] = ["x", "y", "z"] + let variable = Variable(.let, name: "values", equals: array) + let generated = variable.generateCode().normalize() + + #expect(generated.contains("let values: [String] = [\"x\", \"y\", \"z\"]")) + #expect(!generated.contains("static")) + } + + @Test func testNonStaticVariableWithDictionary() { + let dict: [Int: String] = [10: "ten", 20: "twenty"] + let variable = Variable(.let, name: "lookup", equals: dict) + let generated = variable.generateCode().normalize() + + #expect(generated.contains("let lookup: [Int: String]")) + #expect(generated.contains("10: \"ten\"")) + #expect(generated.contains("20: \"twenty\"")) + #expect(!generated.contains("static")) + } + + // MARK: - Static Method Tests + + @Test func testStaticMethodReturnsNewInstance() { + let original = Variable(.let, name: "test", type: "String", equals: "value") + let staticVersion = original.static() + + // Should be different instances + #expect(original.generateCode() != staticVersion.generateCode()) + + // Original should not be static + let originalGenerated = original.generateCode().normalize() + #expect(!originalGenerated.contains("static")) + + // Static version should be static + let staticGenerated = staticVersion.generateCode().normalize() + #expect(staticGenerated.contains("static")) + } + + @Test func testStaticMethodPreservesOtherProperties() { + let original = Variable(.var, name: "test", type: "String", equals: "value") + let staticVersion = original.static() + + let originalGenerated = original.generateCode().normalize() + let staticGenerated = staticVersion.generateCode().normalize() + + // Both should have the same name and value + #expect(originalGenerated.contains("test")) + #expect(staticGenerated.contains("test")) + #expect(originalGenerated.contains("value")) + #expect(staticGenerated.contains("value")) + + // Both should be var + #expect(originalGenerated.contains("var")) + #expect(staticGenerated.contains("var")) + } + + // MARK: - Edge Cases + + @Test func testEmptyArrayLiteral() { + let array: [String] = [] + let variable = Variable(.let, name: "empty", equals: array).static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static let empty: [String] = []")) + } + + @Test func testEmptyDictionaryLiteral() { + let dict: [Int: String] = [:] + let variable = Variable(.let, name: "empty", equals: dict).static() + let generated = variable.generateCode().normalize() + + #expect(generated.contains("static let empty: [Int: String] = []")) + } + + @Test func testMultipleStaticCalls() { + let variable = Variable(.let, name: "test", type: "String", equals: "value").static().static() + let generated = variable.generateCode().normalize() + + // Should still only have one "static" keyword + let staticCount = generated.components(separatedBy: "static").count - 1 + #expect(staticCount == 1) + } +} \ No newline at end of file From 00573df2f12f9ecf1c3a2129653f15b61a3da699 Mon Sep 17 00:00:00 2001 From: leogdion Date: Wed, 18 Jun 2025 10:09:05 -0400 Subject: [PATCH 8/9] lint fix --- .swiftlint.yml | 1 + Sources/SyntaxKit/CodeBlock+ExprSyntax.swift | 23 +++- Sources/SyntaxKit/Extension.swift | 6 +- Sources/SyntaxKit/Literal.swift | 16 +-- Sources/SyntaxKit/Tuple.swift | 29 +++-- Sources/SyntaxKit/Variable.swift | 8 +- Tests/SyntaxKitTests/ExtensionTests.swift | 81 ++++++------- Tests/SyntaxKitTests/LiteralValueTests.swift | 33 +++--- .../OptionsMacroIntegrationTests.swift | 110 ++++++++++-------- Tests/SyntaxKitTests/TypeAliasTests.swift | 75 ++++++------ .../SyntaxKitTests/VariableStaticTests.swift | 61 +++++----- 11 files changed, 242 insertions(+), 201 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index e4222bf..6236845 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -116,6 +116,7 @@ excluded: - .build - Mint - Examples + - Macros indentation_width: indentation_width: 2 file_name: diff --git a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift index 4cf3469..b8b9f30 100644 --- a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift +++ b/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift @@ -3,7 +3,28 @@ // SyntaxKit // // Created by Leo Dion. -// Provides convenience for converting a CodeBlock into ExprSyntax when appropriate. +// Copyright © 2025 BrightDigit. +// +// 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. // import SwiftSyntax diff --git a/Sources/SyntaxKit/Extension.swift b/Sources/SyntaxKit/Extension.swift index cb93a5b..522c5fc 100644 --- a/Sources/SyntaxKit/Extension.swift +++ b/Sources/SyntaxKit/Extension.swift @@ -7,7 +7,7 @@ // // 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -93,4 +93,4 @@ public struct Extension: CodeBlock { memberBlock: memberBlock ) } -} \ No newline at end of file +} diff --git a/Sources/SyntaxKit/Literal.swift b/Sources/SyntaxKit/Literal.swift index 46a07d2..0cec486 100644 --- a/Sources/SyntaxKit/Literal.swift +++ b/Sources/SyntaxKit/Literal.swift @@ -7,7 +7,7 @@ // // 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -33,7 +33,7 @@ import SwiftSyntax public protocol LiteralValue { /// The Swift type name for this literal value. var typeName: String { get } - + /// Renders this value as a Swift literal string. var literalString: String { get } } @@ -78,11 +78,12 @@ public enum Literal: CodeBlock { extension Array: LiteralValue where Element == String { public var typeName: String { "[String]" } - + public var literalString: String { let elements = self.map { element in // Escape quotes and newlines - let escaped = element + let escaped = + element .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "\n", with: "\\n") @@ -96,11 +97,12 @@ extension Array: LiteralValue where Element == String { extension Dictionary: LiteralValue where Key == Int, Value == String { public var typeName: String { "[Int: String]" } - + public var literalString: String { let elements = self.map { key, value in // Escape quotes and newlines - let escaped = value + let escaped = + value .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "\n", with: "\\n") diff --git a/Sources/SyntaxKit/Tuple.swift b/Sources/SyntaxKit/Tuple.swift index a294607..6179f7c 100644 --- a/Sources/SyntaxKit/Tuple.swift +++ b/Sources/SyntaxKit/Tuple.swift @@ -3,17 +3,28 @@ // SyntaxKit // // Created by Leo Dion. -// This file defines a `Tuple` code-block that generates a Swift tuple expression. -// It is primarily useful for macro expansions or DSL code that needs to group -// multiple expression `CodeBlock`s together, for example: +// Copyright © 2025 BrightDigit. // -// Tuple { -// VariableExp("value") -// Literal.string("debug") -// }.expr // -> ExprSyntax for `(value, "debug")` +// 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 result is represented as a `TupleExprSyntax`, which naturally conforms to -// `ExprSyntax` and therefore plays nicely with our `CodeBlock.expr` convenience. +// 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. // import SwiftSyntax diff --git a/Sources/SyntaxKit/Variable.swift b/Sources/SyntaxKit/Variable.swift index 0fa9a9d..40140ba 100644 --- a/Sources/SyntaxKit/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -7,7 +7,7 @@ // // 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 +// 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 @@ -17,7 +17,7 @@ // 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, +// 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 @@ -50,7 +50,7 @@ public struct Variable: CodeBlock { self.type = type self.defaultValue = defaultValue } - + /// Creates a `let` or `var` declaration with a literal value. /// - Parameters: /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. @@ -85,7 +85,7 @@ public struct Variable: CodeBlock { value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) ) } - + var modifiers: DeclModifierListSyntax = [] if isStatic { modifiers = DeclModifierListSyntax([ diff --git a/Tests/SyntaxKitTests/ExtensionTests.swift b/Tests/SyntaxKitTests/ExtensionTests.swift index 5d30dc5..b04190d 100644 --- a/Tests/SyntaxKitTests/ExtensionTests.swift +++ b/Tests/SyntaxKitTests/ExtensionTests.swift @@ -32,91 +32,92 @@ import Testing @testable import SyntaxKit struct ExtensionTests { - // MARK: - Basic Extension Tests - + @Test func testBasicExtension() { let extensionDecl = Extension("String") { Variable(.let, name: "test", type: "Int", equals: "42") } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension String")) #expect(generated.contains("let test: Int = 42")) } - + @Test func testExtensionWithMultipleMembers() { let extensionDecl = Extension("Array") { Variable(.let, name: "isEmpty", type: "Bool", equals: "true") Variable(.let, name: "count", type: "Int", equals: "0") } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension Array")) #expect(generated.contains("let isEmpty: Bool = true")) #expect(generated.contains("let count: Int = 0")) } - + // MARK: - Extension with Inheritance Tests - + @Test func testExtensionWithSingleInheritance() { let extensionDecl = Extension("MyEnum") { TypeAlias("MappedType", equals: "String") }.inherits("MappedValueRepresentable") - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) #expect(generated.contains("typealias MappedType = String")) } - + @Test func testExtensionWithMultipleInheritance() { let extensionDecl = Extension("MyEnum") { TypeAlias("MappedType", equals: "String") }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension MyEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains("extension MyEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) } - + @Test func testExtensionWithoutInheritance() { let extensionDecl = Extension("MyType") { Variable(.let, name: "constant", type: "String", equals: "value") } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyType")) #expect(!generated.contains("extension MyType:")) #expect(generated.contains("let constant: String = value")) } - + // MARK: - Extension with Complex Members Tests - + @Test func testExtensionWithStaticVariables() { let array: [String] = ["a", "b", "c"] let dict: [Int: String] = [1: "one", 2: "two"] - + let extensionDecl = Extension("TestEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: array).static() Variable(.let, name: "lookup", equals: dict).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) #expect(generated.contains("static let lookup: [Int: String]")) #expect(generated.contains("1: \"one\"")) #expect(generated.contains("2: \"two\"")) } - + @Test func testExtensionWithFunctions() { let extensionDecl = Extension("String") { Function("uppercasedFirst", returns: "String") { @@ -125,55 +126,55 @@ struct ExtensionTests { } } } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension String")) #expect(generated.contains("func uppercasedFirst() -> String")) #expect(generated.contains("return self.prefix(1).uppercased() + self.dropFirst()")) } - + // MARK: - Edge Cases - + @Test func testExtensionWithEmptyBody() { let extensionDecl = Extension("EmptyType") { // Empty body } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension EmptyType")) #expect(generated.contains("{")) #expect(generated.contains("}")) } - + @Test func testExtensionWithSpecialCharactersInName() { let extensionDecl = Extension("MyType") { Variable(.let, name: "generic", type: "T", equals: "nil") } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyType")) #expect(generated.contains("let generic: T = nil")) } - + @Test func testInheritsMethodReturnsNewInstance() { let original = Extension("Test") { Variable(.let, name: "value", type: "Int", equals: "42") } - + let withInheritance = original.inherits("Protocol1", "Protocol2") - + // Should be different instances #expect(original.generateCode() != withInheritance.generateCode()) - + // Original should not have inheritance let originalGenerated = original.generateCode().normalize() #expect(!originalGenerated.contains("extension Test:")) - + // With inheritance should have inheritance let inheritedGenerated = withInheritance.generateCode().normalize() #expect(inheritedGenerated.contains(": Protocol1, Protocol2")) } -} \ No newline at end of file +} diff --git a/Tests/SyntaxKitTests/LiteralValueTests.swift b/Tests/SyntaxKitTests/LiteralValueTests.swift index ef217ae..1517dcb 100644 --- a/Tests/SyntaxKitTests/LiteralValueTests.swift +++ b/Tests/SyntaxKitTests/LiteralValueTests.swift @@ -32,40 +32,39 @@ import Testing @testable import SyntaxKit struct LiteralValueTests { - // MARK: - Array LiteralValue Tests - + @Test func testArrayStringTypeName() { let array: [String] = ["a", "b", "c"] #expect(array.typeName == "[String]") } - + @Test func testArrayStringLiteralString() { let array: [String] = ["a", "b", "c"] #expect(array.literalString == "[\"a\", \"b\", \"c\"]") } - + @Test func testEmptyArrayStringLiteralString() { let array: [String] = [] #expect(array.literalString == "[]") } - + @Test func testArrayStringWithSpecialCharacters() { let array: [String] = ["hello world", "test\"quote", "line\nbreak"] #expect(array.literalString == "[\"hello world\", \"test\\\"quote\", \"line\\nbreak\"]") } - + // MARK: - Dictionary LiteralValue Tests - + @Test func testDictionaryIntStringTypeName() { let dict: [Int: String] = [1: "a", 2: "b"] #expect(dict.typeName == "[Int: String]") } - + @Test func testDictionaryIntStringLiteralString() { let dict: [Int: String] = [1: "a", 2: "b", 3: "c"] let literal = dict.literalString - + // Dictionary order is not guaranteed, so check that all elements are present #expect(literal.contains("1: \"a\"")) #expect(literal.contains("2: \"b\"")) @@ -73,33 +72,33 @@ struct LiteralValueTests { #expect(literal.hasPrefix("[")) #expect(literal.hasSuffix("]")) } - + @Test func testEmptyDictionaryLiteralString() { let dict: [Int: String] = [:] #expect(dict.literalString == "[]") } - + @Test func testDictionaryWithSpecialCharacters() { let dict: [Int: String] = [1: "hello world", 2: "test\"quote"] let literal = dict.literalString - + // Dictionary order is not guaranteed, so check that all elements are present #expect(literal.contains("1: \"hello world\"")) #expect(literal.contains("2: \"test\\\"quote\"")) #expect(literal.hasPrefix("[")) #expect(literal.hasSuffix("]")) } - + // MARK: - Dictionary Ordering Tests - + @Test func testDictionaryOrderingIsConsistent() { let dict1: [Int: String] = [2: "b", 1: "a", 3: "c"] let dict2: [Int: String] = [1: "a", 2: "b", 3: "c"] - + // Both should produce the same literal string regardless of insertion order let literal1 = dict1.literalString let literal2 = dict2.literalString - + // The exact order depends on the dictionary's internal ordering, // but both should be valid Swift dictionary literals #expect(literal1.contains("1: \"a\"")) @@ -109,4 +108,4 @@ struct LiteralValueTests { #expect(literal2.contains("2: \"b\"")) #expect(literal2.contains("3: \"c\"")) } -} \ No newline at end of file +} diff --git a/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift b/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift index 060c1d1..b013daa 100644 --- a/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift +++ b/Tests/SyntaxKitTests/OptionsMacroIntegrationTests.swift @@ -32,21 +32,22 @@ import Testing @testable import SyntaxKit struct OptionsMacroIntegrationTests { - // MARK: - Enum with Raw Values (Dictionary) Tests - + @Test func testEnumWithRawValuesCreatesDictionary() { // Simulate the Options macro expansion for an enum with raw values let keyValues: [Int: String] = [2: "a", 5: "b", 6: "c", 12: "d"] - + let extensionDecl = Extension("MockDictionaryEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: keyValues).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension MockDictionaryEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains( + "extension MockDictionaryEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [Int: String]")) #expect(generated.contains("2: \"a\"")) @@ -54,32 +55,33 @@ struct OptionsMacroIntegrationTests { #expect(generated.contains("6: \"c\"")) #expect(generated.contains("12: \"d\"")) } - + @Test func testEnumWithoutRawValuesCreatesArray() { // Simulate the Options macro expansion for an enum without raw values let caseNames: [String] = ["red", "green", "blue"] - + let extensionDecl = Extension("Color") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: caseNames).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension Color: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) - #expect(generated.contains("static let mappedValues: [String] = [\"red\", \"green\", \"blue\"]")) + #expect( + generated.contains("static let mappedValues: [String] = [\"red\", \"green\", \"blue\"]")) } - + // MARK: - Complex Integration Tests - + @Test func testCompleteOptionsMacroWorkflow() { // This test demonstrates the complete workflow that the Options macro would use - + // Step 1: Determine if enum has raw values (simulated) let hasRawValues = true let enumName = "TestEnum" - + // Step 2: Create the appropriate mappedValues variable let mappedValuesVariable: Variable if hasRawValues { @@ -89,30 +91,31 @@ struct OptionsMacroIntegrationTests { let caseNames: [String] = ["first", "second", "third"] mappedValuesVariable = Variable(.let, name: "mappedValues", equals: caseNames).static() } - + // Step 3: Create the extension let extensionDecl = Extension(enumName) { TypeAlias("MappedType", equals: "String") mappedValuesVariable }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - + // Verify the complete extension - #expect(generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) + #expect( + generated.contains("extension TestEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [Int: String]")) #expect(generated.contains("1: \"first\"")) #expect(generated.contains("2: \"second\"")) #expect(generated.contains("3: \"third\"")) } - + @Test func testOptionsMacroWorkflowWithoutRawValues() { // Test the workflow for enums without raw values - + let hasRawValues = false let enumName = "SimpleEnum" - + let mappedValuesVariable: Variable if hasRawValues { let keyValues: [Int: String] = [1: "first", 2: "second"] @@ -121,100 +124,105 @@ struct OptionsMacroIntegrationTests { let caseNames: [String] = ["first", "second"] mappedValuesVariable = Variable(.let, name: "mappedValues", equals: caseNames).static() } - + let extensionDecl = Extension(enumName) { TypeAlias("MappedType", equals: "String") mappedValuesVariable }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension SimpleEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains("extension SimpleEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [String] = [\"first\", \"second\"]")) } - + // MARK: - Edge Cases - + @Test func testEmptyEnumCases() { let caseNames: [String] = [] - + let extensionDecl = Extension("EmptyEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: caseNames).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension EmptyEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains("extension EmptyEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [String] = []")) } - + @Test func testEmptyDictionary() { let keyValues: [Int: String] = [:] - + let extensionDecl = Extension("EmptyDictEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: keyValues).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension EmptyDictEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains( + "extension EmptyDictEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [Int: String] = []")) } - + @Test func testSpecialCharactersInCaseNames() { let caseNames: [String] = ["case_with_underscore", "case-with-dash", "caseWithCamelCase"] - + let extensionDecl = Extension("SpecialEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: caseNames).static() }.inherits("MappedValueRepresentable", "MappedValueRepresented") - + let generated = extensionDecl.generateCode().normalize() - - #expect(generated.contains("extension SpecialEnum: MappedValueRepresentable, MappedValueRepresented")) + + #expect( + generated.contains("extension SpecialEnum: MappedValueRepresentable, MappedValueRepresented")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [String]")) #expect(generated.contains("\"case_with_underscore\"")) #expect(generated.contains("\"case-with-dash\"")) #expect(generated.contains("\"caseWithCamelCase\"")) } - + // MARK: - API Validation Tests - + @Test func testNewSyntaxKitAPICompleteness() { // Verify that all the new API components work together correctly - + // Test LiteralValue protocol let array: [String] = ["a", "b", "c"] #expect(array.typeName == "[String]") #expect(array.literalString == "[\"a\", \"b\", \"c\"]") - + let dict: [Int: String] = [1: "a", 2: "b"] #expect(dict.typeName == "[Int: String]") #expect(dict.literalString.contains("1: \"a\"")) #expect(dict.literalString.contains("2: \"b\"")) - + // Test Variable with static support let staticVar = Variable(.let, name: "test", equals: array).static() let staticGenerated = staticVar.generateCode().normalize() #expect(staticGenerated.contains("static let test: [String] = [\"a\", \"b\", \"c\"]")) - + // Test Extension with inheritance let ext = Extension("Test") { // Empty content }.inherits("Protocol1", "Protocol2") - + let extGenerated = ext.generateCode().normalize() #expect(extGenerated.contains("extension Test: Protocol1, Protocol2")) - + // Test TypeAlias let alias = TypeAlias("MyType", equals: "String") let aliasGenerated = alias.generateCode().normalize() #expect(aliasGenerated.contains("typealias MyType = String")) } -} \ No newline at end of file +} diff --git a/Tests/SyntaxKitTests/TypeAliasTests.swift b/Tests/SyntaxKitTests/TypeAliasTests.swift index e6b4f9f..c67c4eb 100644 --- a/Tests/SyntaxKitTests/TypeAliasTests.swift +++ b/Tests/SyntaxKitTests/TypeAliasTests.swift @@ -32,65 +32,64 @@ import Testing @testable import SyntaxKit struct TypeAliasTests { - // MARK: - Basic TypeAlias Tests - + @Test func testBasicTypeAlias() { let typeAlias = TypeAlias("MappedType", equals: "String") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias MappedType = String")) } - + @Test func testTypeAliasWithComplexType() { let typeAlias = TypeAlias("ResultType", equals: "Result") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias ResultType = Result")) } - + @Test func testTypeAliasWithGenericType() { let typeAlias = TypeAlias("ArrayType", equals: "Array") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias ArrayType = Array")) } - + @Test func testTypeAliasWithOptionalType() { let typeAlias = TypeAlias("OptionalString", equals: "String?") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias OptionalString = String?")) } - + // MARK: - TypeAlias in Context Tests - + @Test func testTypeAliasInExtension() { let extensionDecl = Extension("MyEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "test", type: "MappedType", equals: "value") } - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyEnum")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("let test: MappedType = value")) } - + @Test func testTypeAliasInStruct() { let structDecl = Struct("Container") { TypeAlias("ElementType", equals: "String") Variable(.let, name: "element", type: "ElementType") } - + let generated = structDecl.generateCode().normalize() - + #expect(generated.contains("struct Container")) #expect(generated.contains("typealias ElementType = String")) #expect(generated.contains("let element: ElementType")) } - + @Test func testTypeAliasInEnum() { let enumDecl = Enum("Result") { TypeAlias("SuccessType", equals: "String") @@ -98,80 +97,80 @@ struct TypeAliasTests { EnumCase("success") EnumCase("failure") } - + let generated = enumDecl.generateCode().normalize() - + #expect(generated.contains("enum Result")) #expect(generated.contains("typealias SuccessType = String")) #expect(generated.contains("typealias FailureType = Error")) #expect(generated.contains("case success")) #expect(generated.contains("case failure")) } - + // MARK: - Edge Cases - + @Test func testTypeAliasWithSpecialCharacters() { let typeAlias = TypeAlias("GenericType", equals: "Array") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias GenericType = Array")) } - + @Test func testTypeAliasWithProtocolComposition() { let typeAlias = TypeAlias("ProtocolType", equals: "Protocol1 & Protocol2") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias ProtocolType = Protocol1 & Protocol2")) } - + @Test func testTypeAliasWithFunctionType() { let typeAlias = TypeAlias("Handler", equals: "(String) -> Void") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias Handler = (String) -> Void")) } - + @Test func testTypeAliasWithTupleType() { let typeAlias = TypeAlias("Coordinate", equals: "(x: Double, y: Double)") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias Coordinate = (x: Double, y: Double)")) } - + @Test func testTypeAliasWithClosureType() { let typeAlias = TypeAlias("Callback", equals: "@escaping (Result) -> Void") let generated = typeAlias.generateCode().normalize() - + #expect(generated.contains("typealias Callback = @escaping (Result) -> Void")) } - + // MARK: - Integration Tests - + @Test func testTypeAliasWithStaticVariable() { let extensionDecl = Extension("MyEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: ["a", "b", "c"]).static() }.inherits("MappedValueRepresentable") - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) } - + @Test func testTypeAliasWithDictionaryVariable() { let extensionDecl = Extension("MyEnum") { TypeAlias("MappedType", equals: "String") Variable(.let, name: "mappedValues", equals: [1: "a", 2: "b"]).static() }.inherits("MappedValueRepresentable") - + let generated = extensionDecl.generateCode().normalize() - + #expect(generated.contains("extension MyEnum: MappedValueRepresentable")) #expect(generated.contains("typealias MappedType = String")) #expect(generated.contains("static let mappedValues: [Int: String]")) #expect(generated.contains("1: \"a\"")) #expect(generated.contains("2: \"b\"")) } -} \ No newline at end of file +} diff --git a/Tests/SyntaxKitTests/VariableStaticTests.swift b/Tests/SyntaxKitTests/VariableStaticTests.swift index 767fdc5..38e1936 100644 --- a/Tests/SyntaxKitTests/VariableStaticTests.swift +++ b/Tests/SyntaxKitTests/VariableStaticTests.swift @@ -32,124 +32,123 @@ import Testing @testable import SyntaxKit struct VariableStaticTests { - // MARK: - Static Variable Tests - + @Test func testStaticVariableWithStringLiteral() { let variable = Variable(.let, name: "test", type: "String", equals: "hello").static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static let test: String = hello")) } - + @Test func testStaticVariableWithArrayLiteral() { let array: [String] = ["a", "b", "c"] let variable = Variable(.let, name: "mappedValues", equals: array).static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static let mappedValues: [String] = [\"a\", \"b\", \"c\"]")) } - + @Test func testStaticVariableWithDictionaryLiteral() { let dict: [Int: String] = [1: "a", 2: "b", 3: "c"] let variable = Variable(.let, name: "mappedValues", equals: dict).static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static let mappedValues: [Int: String]")) #expect(generated.contains("1: \"a\"")) #expect(generated.contains("2: \"b\"")) #expect(generated.contains("3: \"c\"")) } - + @Test func testStaticVariableWithVar() { let variable = Variable(.var, name: "counter", type: "Int", equals: "0").static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static var counter: Int = 0")) } - + // MARK: - Non-Static Variable Tests - + @Test func testNonStaticVariableWithLiteral() { let array: [String] = ["x", "y", "z"] let variable = Variable(.let, name: "values", equals: array) let generated = variable.generateCode().normalize() - + #expect(generated.contains("let values: [String] = [\"x\", \"y\", \"z\"]")) #expect(!generated.contains("static")) } - + @Test func testNonStaticVariableWithDictionary() { let dict: [Int: String] = [10: "ten", 20: "twenty"] let variable = Variable(.let, name: "lookup", equals: dict) let generated = variable.generateCode().normalize() - + #expect(generated.contains("let lookup: [Int: String]")) #expect(generated.contains("10: \"ten\"")) #expect(generated.contains("20: \"twenty\"")) #expect(!generated.contains("static")) } - + // MARK: - Static Method Tests - + @Test func testStaticMethodReturnsNewInstance() { let original = Variable(.let, name: "test", type: "String", equals: "value") let staticVersion = original.static() - + // Should be different instances #expect(original.generateCode() != staticVersion.generateCode()) - + // Original should not be static let originalGenerated = original.generateCode().normalize() #expect(!originalGenerated.contains("static")) - + // Static version should be static let staticGenerated = staticVersion.generateCode().normalize() #expect(staticGenerated.contains("static")) } - + @Test func testStaticMethodPreservesOtherProperties() { let original = Variable(.var, name: "test", type: "String", equals: "value") let staticVersion = original.static() - + let originalGenerated = original.generateCode().normalize() let staticGenerated = staticVersion.generateCode().normalize() - + // Both should have the same name and value #expect(originalGenerated.contains("test")) #expect(staticGenerated.contains("test")) #expect(originalGenerated.contains("value")) #expect(staticGenerated.contains("value")) - + // Both should be var #expect(originalGenerated.contains("var")) #expect(staticGenerated.contains("var")) } - + // MARK: - Edge Cases - + @Test func testEmptyArrayLiteral() { let array: [String] = [] let variable = Variable(.let, name: "empty", equals: array).static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static let empty: [String] = []")) } - + @Test func testEmptyDictionaryLiteral() { let dict: [Int: String] = [:] let variable = Variable(.let, name: "empty", equals: dict).static() let generated = variable.generateCode().normalize() - + #expect(generated.contains("static let empty: [Int: String] = []")) } - + @Test func testMultipleStaticCalls() { let variable = Variable(.let, name: "test", type: "String", equals: "value").static().static() let generated = variable.generateCode().normalize() - + // Should still only have one "static" keyword let staticCount = generated.components(separatedBy: "static").count - 1 #expect(staticCount == 1) } -} \ No newline at end of file +} From c0f5b0ceead2555b8de302c3e2f3810898ec03e7 Mon Sep 17 00:00:00 2001 From: leogdion Date: Wed, 18 Jun 2025 10:09:11 -0400 Subject: [PATCH 9/9] Update SyntaxKit.yml --- .github/workflows/SyntaxKit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/SyntaxKit.yml b/.github/workflows/SyntaxKit.yml index dd72dfa..2c9de80 100644 --- a/.github/workflows/SyntaxKit.yml +++ b/.github/workflows/SyntaxKit.yml @@ -122,7 +122,7 @@ jobs: restore-keys: | ${{ runner.os }}-mint- - name: Install mint - if: steps.cache-mint.outputs.cache-hit != 'true' + if: steps.cache-mint.outputs.cache-hit == '' run: | git clone https://github.com/yonaskolb/Mint.git cd Mint