From 5b7c5734a7dc9be7c45bc42b6af98d6a1ac26b3e Mon Sep 17 00:00:00 2001 From: Mohammed Rokon Uddin Date: Fri, 9 Feb 2024 14:51:47 +0600 Subject: [PATCH 1/4] chore: refactor the repository by implementing generics --- {{cookiecutter.app_name}}/.swift-format | 56 + {{cookiecutter.app_name}}/.swiftformat | 5 - .../Common/Package.swift | 44 +- .../Common/Sources/Common/BaseAction.swift | 22 + .../Sources/Common/FeatureReducer.swift | 195 +- .../Common/Sources/Common/Heap.swift | 50 +- .../Domain/Package.swift | 29 +- .../Sources/Domain/Enitity/AppError.swift | 87 +- .../Sources/Domain/Enitity/Product.swift | 48 + .../TrendingRepository/BuiltByUser.swift | 25 - .../TrendingRepository.swift | 71 - .../TrendingRepository/TrendingUser.swift | 47 - .../Enitity/TrendingRepository/UserRepo.swift | 26 - .../Sources/Domain/Interface/Repository.swift | 30 + .../Domain/Interface/RepositoryProvider.swift | 14 - .../TrendingDeveloperRepository.swift | 14 - ...dingRepoRepository.swift => UseCase.swift} | 10 +- .../UseCases/PrepareCoreDataUseCase.swift | 21 + .../Domain/UseCases/ProductUseCase.swift | 24 + .../Domain/UseCases/SaveProductUseCase.swift | 24 + .../UseCases/TrendingDeveloperUseCase.swift | 24 - .../UseCases/TrendingRepositoryUseCase.swift | 24 - .../Domain/UseCases/UseCaseProtocol.swift | 28 - .../Features/Package.swift | 84 +- .../Features/Sources/App/AppClient.swift | 52 + .../Features/Sources/App/AppFeature.swift | 193 +- .../Features/Sources/App/AppView.swift | 133 +- .../Sources/Counter/CounterFeature.swift | 94 +- .../Sources/Counter/CounterView.swift | 87 +- .../Tests/FeaturesTests/FeaturesTests.swift | 13 +- .../xcschemes/NetworkPlatform.xcscheme | 66 + .../NetworkPlatform/Package.swift | 68 +- .../BuildConfiguration.swift | 73 +- .../Sources/NetworkPlatform/API/AppAPI.swift | 73 + .../Sources/NetworkPlatform/API/AuthAPI.swift | 64 +- .../API/TrendingGithubAPI.swift | 84 - .../Common/HTTPStatusCode.swift | 366 +-- .../Common/LocalNotification.swift | 24 +- .../NetworkPlatform/Common/Stubbable.swift | 23 +- .../Network/AppNetworking.swift | 74 + .../Network/NetworkProvider.swift | 19 - .../NetworkPlatform/Network/Networking.swift | 151 +- .../Network/NetworkingError.swift | 18 +- .../Network/NetworkingType.swift | 124 +- .../Network/TrendingGithubNetworking.swift | 86 - .../Repository/NetworkRepository.swift | 47 + .../Repository/RepositoryProvider.swift | 34 - .../TrendingDeveloperRepository.swift | 22 - .../Repository/TrendingRepoRepository.swift | 22 - .../Resources/StubbedResponse/Product.json | 12 + .../StubbedResponse/Languages.json | 1998 ----------------- .../StubbedResponse/RepositoryTrendings.json | 952 -------- .../StubbedResponse/UserTrendings.json | 302 --- .../Utility/AnyPublisher+Extensions.swift | 42 +- .../Utility/VerbosePlugin.swift | 63 +- .../NetworkPlatformTests.swift | 13 +- .../PersistentPlatform/.gitignore | 8 + .../xcschemes/PersistentPlatform.xcscheme | 66 + .../PersistentPlatform/Package.swift | 33 + .../Extensions/Constants+CoreData.swift | 19 + .../Extensions/DomainConvertible.swift | 38 + .../NSManagedObjectModel+Compatible.swift | 19 + .../NSManagedObjectModel+Resource.swift | 32 + .../NSPersistentStoreCoordinator+SQLite.swift | 63 + .../MODefinition/Product+CoreDataClass.swift | 28 + .../Product+CoreDataProperties.swift | 23 + .../Model.xcdatamodel/contents | 11 + .../PersistenceController.swift | 407 ++++ .../Repository/PersistentRepository.swift | 59 + .../Utilities/Package.swift | 28 +- .../Utilities/Sources/Utilities/Logger.swift | 95 +- .../project.pbxproj | 18 +- .../xcshareddata/swiftpm/Package.resolved | 36 +- .../Application/MainApp.swift | 17 +- .../Generated/BuildConfiguration.plist | 2 +- .../__cookiecutter_app_name__Tests.swift | 31 +- .../__cookiecutter_app_name__UITests.swift | 58 +- ...ecutter_app_name__UITestsLaunchTests.swift | 32 +- 78 files changed, 2643 insertions(+), 4874 deletions(-) create mode 100644 {{cookiecutter.app_name}}/.swift-format delete mode 100644 {{cookiecutter.app_name}}/.swiftformat create mode 100644 {{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift create mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/Product.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/BuiltByUser.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingRepository.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingUser.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/UserRepo.swift create mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/Repository.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/RepositoryProvider.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingDeveloperRepository.swift rename {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/{TrendingRepoRepository.swift => UseCase.swift} (52%) create mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/PrepareCoreDataUseCase.swift create mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/ProductUseCase.swift create mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/SaveProductUseCase.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingDeveloperUseCase.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingRepositoryUseCase.swift delete mode 100644 {{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/UseCaseProtocol.swift create mode 100644 {{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift create mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/.swiftpm/xcode/xcshareddata/xcschemes/NetworkPlatform.xcscheme create mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AppAPI.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/TrendingGithubAPI.swift create mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/AppNetworking.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkProvider.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/TrendingGithubNetworking.swift create mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/NetworkRepository.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/RepositoryProvider.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingDeveloperRepository.swift delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingRepoRepository.swift create mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Resources/StubbedResponse/Product.json delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/Languages.json delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/RepositoryTrendings.json delete mode 100644 {{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/UserTrendings.json create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/.gitignore create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/.swiftpm/xcode/xcshareddata/xcschemes/PersistentPlatform.xcscheme create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Package.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/Constants+CoreData.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/DomainConvertible.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Compatible.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Resource.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSPersistentStoreCoordinator/NSPersistentStoreCoordinator+SQLite.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataClass.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataProperties.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/PersistenceController.swift create mode 100644 {{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Repository/PersistentRepository.swift diff --git a/{{cookiecutter.app_name}}/.swift-format b/{{cookiecutter.app_name}}/.swift-format new file mode 100644 index 0000000..8fd09a1 --- /dev/null +++ b/{{cookiecutter.app_name}}/.swift-format @@ -0,0 +1,56 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 80, + "tabWidth" : 8, + "maximumBlankLines" : 1, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "version" : 1, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : false, + "OrderedImports" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + } +} diff --git a/{{cookiecutter.app_name}}/.swiftformat b/{{cookiecutter.app_name}}/.swiftformat deleted file mode 100644 index 2727d23..0000000 --- a/{{cookiecutter.app_name}}/.swiftformat +++ /dev/null @@ -1,5 +0,0 @@ -# format options ---allman false - -# file options ---exclude Pods, {{cookiecutter.app_name}}/Resources/Generated/R.generated.swift, Modules/Sources/Authentication/R.generated.swift \ No newline at end of file diff --git a/{{cookiecutter.app_name}}/Common/Package.swift b/{{cookiecutter.app_name}}/Common/Package.swift index 6122973..c50ef42 100644 --- a/{{cookiecutter.app_name}}/Common/Package.swift +++ b/{{cookiecutter.app_name}}/Common/Package.swift @@ -4,25 +4,27 @@ import PackageDescription let package = Package( - name: "Common", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library( - name: "Common", - targets: ["Common"]), - ], - dependencies: [ - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", - exact: "1.5.1" - ), - ], - targets: [ - .target( - name: "Common", - dependencies: [ - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), - ] + name: "Common", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library( + name: "Common", + targets: ["Common"]) + ], + dependencies: [ + .package( + url: "https://github.com/pointfreeco/swift-composable-architecture", + exact: "1.5.1" + ) + ], + targets: [ + .target( + name: "Common", + dependencies: [ + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture") + ] + ) + ] ) diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift new file mode 100644 index 0000000..3a60272 --- /dev/null +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift @@ -0,0 +1,22 @@ +// +// BaseAction.swift +// Common +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation + +// keep Actions organized and their intent explicit +// https://github.com/pointfreeco/swift-composable-architecture/discussions/1440 + +public protocol BaseAction { + associatedtype ViewAction + associatedtype DelegateAction + associatedtype InlyingAction + + static func view(_: ViewAction) -> Self + static func delegate(_: DelegateAction) -> Self + static func inlying(_: InlyingAction) -> Self +} diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift index 67688a2..c831ebb 100644 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift @@ -10,114 +10,137 @@ import ComposableArchitecture import SwiftUI // MARK: FeatureReducer -public protocol FeatureReducer: Reducer where State: Sendable & Hashable, Action == FeatureAction { - associatedtype ViewAction: Sendable & Equatable = Never - associatedtype InternalAction: Sendable & Equatable = Never - associatedtype ChildAction: Sendable & Equatable = Never - associatedtype DelegateAction: Sendable & Equatable = Never - - func reduce(into state: inout State, viewAction: ViewAction) -> Effect - func reduce(into state: inout State, internalAction: InternalAction) -> Effect - func reduce(into state: inout State, childAction: ChildAction) -> Effect - func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect - func reduceDismissDestination(into state: inout State) -> Effect - - associatedtype Destination: DestinationReducer = EmptyDestination - associatedtype ViewState: Equatable = Never +public protocol FeatureReducer: Reducer +where State: Sendable & Hashable, Action == FeatureAction { + associatedtype ViewAction: Sendable & Equatable = Never + associatedtype InternalAction: Sendable & Equatable = Never + associatedtype ChildAction: Sendable & Equatable = Never + associatedtype DelegateAction: Sendable & Equatable = Never + + func reduce(into state: inout State, viewAction: ViewAction) -> Effect + func reduce(into state: inout State, internalAction: InternalAction) + -> Effect + func reduce(into state: inout State, childAction: ChildAction) -> Effect< + Action + > + func reduce(into state: inout State, presentedAction: Destination.Action) + -> Effect + func reduceDismissDestination(into state: inout State) -> Effect + + associatedtype Destination: DestinationReducer = EmptyDestination + associatedtype ViewState: Equatable = Never } extension Reducer where Self: FeatureReducer { - public typealias Action = FeatureAction - - public var body: some ReducerOf { - Reduce(core) - } - - public func core(into state: inout State, action: Action) -> Effect { - switch action { - case .destination(.dismiss): - reduceDismissDestination(into: &state) - case let .destination(.presented(presentedAction)): - reduce(into: &state, presentedAction: presentedAction) - case let .view(viewAction): - reduce(into: &state, viewAction: viewAction) - case let .internal(internalAction): - reduce(into: &state, internalAction: internalAction) - case let .child(childAction): - reduce(into: &state, childAction: childAction) - case .delegate: - .none - } - } - - public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { - .none - } - - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - .none - } - - public func reduce(into state: inout State, childAction: ChildAction) -> Effect { - .none - } - - public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { - .none - } - - public func reduceDismissDestination(into state: inout State) -> Effect { - .none + public typealias Action = FeatureAction + + public var body: some ReducerOf { + Reduce(core) + } + + public func core(into state: inout State, action: Action) -> Effect { + switch action { + case .destination(.dismiss): + reduceDismissDestination(into: &state) + case let .destination(.presented(presentedAction)): + reduce(into: &state, presentedAction: presentedAction) + case let .view(viewAction): + reduce(into: &state, viewAction: viewAction) + case let .internal(internalAction): + reduce(into: &state, internalAction: internalAction) + case let .child(childAction): + reduce(into: &state, childAction: childAction) + case .delegate: + .none } - + } + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect< + Action + > { + .none + } + + public func reduce(into state: inout State, internalAction: InternalAction) + -> Effect + { + .none + } + + public func reduce(into state: inout State, childAction: ChildAction) + -> Effect + { + .none + } + + public func reduce( + into state: inout State, presentedAction: Destination.Action + ) -> Effect { + .none + } + + public func reduceDismissDestination(into state: inout State) -> Effect< + Action + > { + .none + } + } -public typealias PresentationStoreOf = Store, PresentationAction> +public typealias PresentationStoreOf = Store< + PresentationState, PresentationAction +> // MARK: FeatureAction @CasePathable public enum FeatureAction: Sendable, Equatable { - case destination(PresentationAction) - case view(Feature.ViewAction) - case `internal`(Feature.InternalAction) - case child(Feature.ChildAction) - case delegate(Feature.DelegateAction) + case destination(PresentationAction) + case view(Feature.ViewAction) + case `internal`(Feature.InternalAction) + case child(Feature.ChildAction) + case delegate(Feature.DelegateAction) } // MARK: DestinationReducer -public protocol DestinationReducer: Reducer where State: Sendable & Hashable, Action: Sendable & Equatable & CasePathable { } +public protocol DestinationReducer: Reducer +where State: Sendable & Hashable, Action: Sendable & Equatable & CasePathable {} // MARK: EmptyDestination public enum EmptyDestination: DestinationReducer { - public struct State: Sendable, Hashable {} - public typealias Action = Never - public func reduce(into state: inout State, action: Never) -> Effect {} - public func reduceDismissDestination(into state: inout State) -> Effect { .none } + public struct State: Sendable, Hashable {} + public typealias Action = Never + public func reduce(into state: inout State, action: Never) -> Effect { + } + public func reduceDismissDestination(into state: inout State) -> Effect< + Action + > { .none } } //MARK: FeatureAction + Hashable -extension FeatureAction: Hashable where Feature.Destination.Action: Hashable, - Feature.ViewAction: Hashable, - Feature.ChildAction: Hashable, - Feature.InternalAction: Hashable, - Feature.DelegateAction: Hashable { - public func hash(into hasher: inout Hasher) { - switch self { - case let .destination(action): - hasher.combine(action) - case let .view(action): - hasher.combine(action) - case let .internal(action): - hasher.combine(action) - case let .child(action): - hasher.combine(action) - case let .delegate(action): - hasher.combine(action) - } +extension FeatureAction: Hashable +where + Feature.Destination.Action: Hashable, + Feature.ViewAction: Hashable, + Feature.ChildAction: Hashable, + Feature.InternalAction: Hashable, + Feature.DelegateAction: Hashable +{ + public func hash(into hasher: inout Hasher) { + switch self { + case let .destination(action): + hasher.combine(action) + case let .view(action): + hasher.combine(action) + case let .internal(action): + hasher.combine(action) + case let .child(action): + hasher.combine(action) + case let .delegate(action): + hasher.combine(action) } + } } /// For scoping to an actionless childstore public func actionless(never: Never) -> T {} - diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/Heap.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/Heap.swift index 8c3b70e..a8bc9f5 100644 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/Heap.swift +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/Heap.swift @@ -7,35 +7,35 @@ // private final class Reference: Equatable { - var value: T - init(_ value: T) { - self.value = value - } - static func == (lhs: Reference, rhs: Reference) -> Bool { - lhs.value == rhs.value - } + var value: T + init(_ value: T) { + self.value = value + } + static func == (lhs: Reference, rhs: Reference) -> Bool { + lhs.value == rhs.value + } } @propertyWrapper public struct Heap: Equatable { - private var reference: Reference - - public init(_ value: T) { - reference = .init(value) - } - - public var wrappedValue: T { - get { reference.value } - set { - if !isKnownUniquelyReferenced(&reference) { - reference = .init(newValue) - return - } - reference.value = newValue - } - } - public var projectedValue: Heap { - self + private var reference: Reference + + public init(_ value: T) { + reference = .init(value) + } + + public var wrappedValue: T { + get { reference.value } + set { + if !isKnownUniquelyReferenced(&reference) { + reference = .init(newValue) + return + } + reference.value = newValue } + } + public var projectedValue: Heap { + self + } } extension Heap: Hashable where T: Hashable { diff --git a/{{cookiecutter.app_name}}/Domain/Package.swift b/{{cookiecutter.app_name}}/Domain/Package.swift index 585e296..6d24114 100644 --- a/{{cookiecutter.app_name}}/Domain/Package.swift +++ b/{{cookiecutter.app_name}}/Domain/Package.swift @@ -1,21 +1,20 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "Domain", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library( - name: "Domain", - targets: ["Domain"]), - ], - dependencies: [ - ], - targets: [ - .target( - name: "Domain", - dependencies: []) - ] + name: "Domain", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library( + name: "Domain", + targets: ["Domain"]) + ], + dependencies: [], + targets: [ + .target( + name: "Domain", + dependencies: []) + ] ) diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/AppError.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/AppError.swift index 85b4090..1431870 100644 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/AppError.swift +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/AppError.swift @@ -8,14 +8,83 @@ import Foundation -public struct AppError: Error, Equatable, Sendable { - public let code: Int? - public let title: String? - public let message: String? - - public init(code: Int?, title: String?, message: String?) { - self.code = code - self.title = title - self.message = message +public struct ResponseWrapper: Decodable { + let response: Value + + public var value: Value { + response + } +} + +public enum AppError: Error, Identifiable { + + public struct MiddlewareAPIError: Decodable, LocalizedError { + public struct Header: Decodable { + public let system_timestamp: Date + public let api_ver: String + } + + public let success: Bool + public let message: String + public let status_code: Int + public let phoneID: String + public let header: Header + + public var errorDescription: String? { + message + } + } + + public struct WebsiteAPIError: Decodable, LocalizedError { + let error_code: String + let status_code: Int + let message: String + public var errorDescription: String? { + message + } + } + + public var id: String { localizedDescription } + + case databaseCorrupted(String?) + case copyrightClaim(String) + case middleWareAPIError(MiddlewareAPIError) + case websiteAPIError(WebsiteAPIError) + case networkingFailed( + underlyingError: Error, context: String, file: StaticString = #file, + line: Int = #line) + case parseFailed + case noUpdates + case notFound + case unknown +} + +extension AppError: LocalizedError { + var isRetryable: Bool { + return false + } + + public var errorDescription: String? { + switch self { + case .databaseCorrupted: + return "Database Corrupted" + case .copyrightClaim: + return "Copyright Claim" + case let .networkingFailed(underlying, context, file, line): + return + "Network Error:\nError Description: \(underlying)\nFile: \(file)\nLine: \(line)\nContext: \(context)" + case let .middleWareAPIError(error): + return error.localizedDescription + case let .websiteAPIError(error): + return error.localizedDescription + case .parseFailed: + return "Parse Error" + case .noUpdates: + return "No updates available" + case .notFound: + return "Not found" + case .unknown: + return "Unknown Error" } + } } diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/Product.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/Product.swift new file mode 100644 index 0000000..a8a8c2a --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/Product.swift @@ -0,0 +1,48 @@ +// +// Product.swift +// Domain +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation + +public struct Product: Codable, Equatable, Hashable { + public let id: Int + public let price: Double + public let title: String + public let image: String + // public let rating: Rating + public let category: String + public let description: String + + public init( + id: Int, + price: Double, + title: String, + image: String, + // rating: Rating, + category: String, + description: String + ) { + self.id = id + self.price = price + self.title = title + self.image = image + // self.rating = rating + self.category = category + self.description = description + } +} +// +//// MARK: - Rating +//public struct Rating: Codable, Equatable, Hashable { +// public let rate: Double +// public let count: Int +// +// public init(rate: Double, count: Int) { +// self.rate = rate +// self.count = count +// } +//} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/BuiltByUser.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/BuiltByUser.swift deleted file mode 100644 index 829adeb..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/BuiltByUser.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BuiltByUser.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public struct BuiltByUser: Codable, Sendable { - public var username: String? - public var href: String? - public var avatar: String? - - public init(username: String?, href: String?, avatar: String?) { - self.username = username - self.href = href - self.avatar = avatar - } - - enum CodingKeys: String, CodingKey { - case username, href, avatar - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingRepository.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingRepository.swift deleted file mode 100644 index c143a55..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingRepository.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// TrendingRepository.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public struct TrendingRepository: Codable, Equatable, Sendable { - public var author: String? - public var name: String? - public var avatar: String? - public var descriptionField: String? - public var url: String? - public var language: String? - public var languageColor: String? - public var stars: Int? - public var forks: Int? - public var currentPeriodStars: Int? - public var builtBy: [BuiltByUser]? - - public var fullname: String? { - return "\(author ?? "")/\(name ?? "")" - } - - public var avatarUrl: String? { - return builtBy?.first?.avatar - } - - public init(author: String?, - name: String?, - avatar: String?, - url: String?, - descriptionField: String?, - language: String?, - languageColor: String?, - stars: Int?, - forks: Int?, - currentPeriodStars: Int?, - builtBy: [BuiltByUser]?) - { - self.author = author - self.name = name - self.avatar = avatar - self.url = url - self.descriptionField = descriptionField - self.language = language - self.languageColor = languageColor - self.stars = stars - self.forks = forks - self.currentPeriodStars = currentPeriodStars - self.builtBy = builtBy - } - - enum CodingKeys: String, CodingKey { - case author, name, avatar, url, language, languageColor, stars, forks, currentPeriodStars, builtBy - case descriptionField = "description" - } -} - -extension TrendingRepository: Hashable { - public static func == (lhs: TrendingRepository, rhs: TrendingRepository) -> Bool { - return lhs.fullname == rhs.fullname - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(fullname) - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingUser.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingUser.swift deleted file mode 100644 index ee0fbc7..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/TrendingUser.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// TrendingUser.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public struct TrendingUser: Codable, Sendable { - public var username: String? - public var name: String? - public var url: String? - public var sponsorUrl: String? - public var avatar: String? - public var repo: UserRepo? - - public init(username: String?, - name: String?, - sponsorUrl: String?, - url: String?, - avatar: String?, - repo: UserRepo?) - { - self.username = username - self.name = name - self.url = url - self.avatar = avatar - self.repo = repo - self.sponsorUrl = sponsorUrl - } - - enum CodingKeys: String, CodingKey { - case username, name, url, sponsorUrl, avatar, repo - } -} - -extension TrendingUser: Hashable { - public static func == (lhs: TrendingUser, rhs: TrendingUser) -> Bool { - return lhs.username == rhs.username - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(username) - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/UserRepo.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/UserRepo.swift deleted file mode 100644 index e040997..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Enitity/TrendingRepository/UserRepo.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UserRepo.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public struct UserRepo: Codable, Sendable { - public var name: String? - public var descriptionField: String? - public var url: String? - - public init(name: String?, descriptionField: String?, url: String?) { - self.name = name - self.descriptionField = descriptionField - self.url = url - } - - enum CodingKeys: String, CodingKey { - case name, url - case descriptionField = "description" - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/Repository.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/Repository.swift new file mode 100644 index 0000000..b883f09 --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/Repository.swift @@ -0,0 +1,30 @@ +// +// Repository.swift +// Domain +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation + +public protocol Repository { + associatedtype CreateInput = Never + associatedtype CreateOutput = Never + associatedtype ReadInput = Never + associatedtype ReadOutput = Never + associatedtype UpdateInput = Never + associatedtype UpdateOutput = Never + associatedtype DeleteInput = Never + associatedtype DeleteOutput = Never + associatedtype RepositoryType + + func prepare() async throws + func create(input: CreateInput) async throws -> CreateOutput + func read(input: ReadInput) async throws -> ReadOutput + func update(input: UpdateInput) async throws -> UpdateOutput + func delete(input: DeleteInput) async throws -> DeleteOutput + + static var live: RepositoryType { get } + static var stubbed: RepositoryType { get } +} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/RepositoryProvider.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/RepositoryProvider.swift deleted file mode 100644 index fb30c54..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/RepositoryProvider.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RepositoryProvider.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public protocol RepositoryProvider { - func makeTrendingRepoRepository() -> TrendingRepoRepository - func makeTrendingDeveloperRepository() -> TrendingDeveloperRepository -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingDeveloperRepository.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingDeveloperRepository.swift deleted file mode 100644 index 4c959fc..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingDeveloperRepository.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// TrendingDeveloperRepository.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Foundation - -public protocol TrendingDeveloperRepository { - func trendingDeveloper(language: String, since: String) async throws -> [TrendingUser] -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingRepoRepository.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/UseCase.swift similarity index 52% rename from {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingRepoRepository.swift rename to {{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/UseCase.swift index a66a90f..6f98f5d 100644 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/TrendingRepoRepository.swift +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/Interface/UseCase.swift @@ -1,14 +1,16 @@ // -// TrendingRepoRepository.swift +// UseCase.swift // Domain // // Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // -import Combine import Foundation -public protocol TrendingRepoRepository { - func trendingRepositories(language: String, since: String) async throws -> [TrendingRepository] +public protocol UseCase { + associatedtype Input + associatedtype Output + + func execute(input: Input) async throws -> Output } diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/PrepareCoreDataUseCase.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/PrepareCoreDataUseCase.swift new file mode 100644 index 0000000..274d1d8 --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/PrepareCoreDataUseCase.swift @@ -0,0 +1,21 @@ +// +// PrepareCoreDataUseCase.swift +// Domain +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation + +public final class PrepareCoreDataUseCase: UseCase { + var prepare: () async throws -> Void + public init(repository: R) { + self.prepare = + repository.prepare + } + + public func execute(input: Void) async { + try? await prepare() + } +} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/ProductUseCase.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/ProductUseCase.swift new file mode 100644 index 0000000..4d035ca --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/ProductUseCase.swift @@ -0,0 +1,24 @@ +// +// ProductUseCase.swift +// Domain +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation + +public final class ProductUseCase: UseCase { + + var getProduct: (_ input: Int) async throws -> Product? + + public init(repository: R) + where R.ReadInput == Input, R.ReadOutput == Output { + self.getProduct = + repository.read(input:) + } + + @Sendable public func execute(input: Int) async throws -> Product? { + return try await getProduct(input) + } +} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/SaveProductUseCase.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/SaveProductUseCase.swift new file mode 100644 index 0000000..914e09c --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/SaveProductUseCase.swift @@ -0,0 +1,24 @@ +// +// SaveProductUseCase.swift +// Domain +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Combine +import Foundation + +public final class SaveProductUseCase: UseCase { + var saveProduct: (_ input: Product) async throws -> Void + + public init(repository: R) + where R.CreateInput == Input, R.CreateOutput == Output { + self.saveProduct = + repository.create(input:) + } + + @Sendable public func execute(input: Product) async throws { + return try await saveProduct(input) + } +} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingDeveloperUseCase.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingDeveloperUseCase.swift deleted file mode 100644 index e5f0df6..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingDeveloperUseCase.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TrendingDeveloperUseCase.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Foundation - -public typealias AnyDeveloperUseCase = AnyUsecase<(language: String, since: String), [TrendingUser]> - -public final class DefaultTrendingDeveloperUseCase: UseCase { - private let repository: TrendingDeveloperRepository - - public init(repository: TrendingDeveloperRepository) { - self.repository = repository - } - - public func execute(input: (language: String, since: String)) async throws -> [TrendingUser] { - return try await repository.trendingDeveloper(language: input.language, since: input.since) - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingRepositoryUseCase.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingRepositoryUseCase.swift deleted file mode 100644 index 587983f..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/TrendingRepositoryUseCase.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TrendingRepositoryUseCase.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Foundation - -public typealias AnyRepositoryUseCase = AnyUsecase<(language: String, since: String), [TrendingRepository]> - -public final class DefaultTrendingRepositoryUseCase: UseCase { - private let repository: TrendingRepoRepository - - public init(repository: TrendingRepoRepository) { - self.repository = repository - } - - public func execute(input: (language: String, since: String)) async throws -> [TrendingRepository] { - return try await repository.trendingRepositories(language: input.language, since: input.since) - } -} diff --git a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/UseCaseProtocol.swift b/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/UseCaseProtocol.swift deleted file mode 100644 index 0a8fa9b..0000000 --- a/{{cookiecutter.app_name}}/Domain/Sources/Domain/UseCases/UseCaseProtocol.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UseCaseProtocol.swift -// Domain -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -public protocol UseCase { - associatedtype Input - associatedtype Output - - func execute(input: Input) async throws -> Output -} - -public struct AnyUsecase { - private let execute: (_ input: Input) async throws -> Output - - public init(with usecase: Usecase) where Usecase.Input == Input, Usecase.Output == Output { - self.execute = usecase.execute - } - - public func execute(input: Input) async throws -> Output { - return try await execute(input) - } -} diff --git a/{{cookiecutter.app_name}}/Features/Package.swift b/{{cookiecutter.app_name}}/Features/Package.swift index aa29676..6b7c8cf 100644 --- a/{{cookiecutter.app_name}}/Features/Package.swift +++ b/{{cookiecutter.app_name}}/Features/Package.swift @@ -4,42 +4,50 @@ import PackageDescription let package = Package( - name: "Features", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library( - name: "App", - targets: ["App"] - ), - - .library( - name: "Counter", - targets: ["Counter"] - ) - ], - dependencies: [ - .package(path: "../Common"), - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", - exact: "1.5.1" - ), - ], - targets: [ - .target( - name: "App", - dependencies: [ - "Counter", - .product(name: "Common", package: "Common"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), - - .target( - name: "Counter", - dependencies: [ - .product(name: "Common", package: "Common"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ) - ] + name: "Features", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library( + name: "App", + targets: ["App"] + ), + + .library( + name: "Counter", + targets: ["Counter"] + ), + ], + dependencies: [ + .package(path: "../Common"), + .package(path: "../NetworkPlatform"), + .package(path: "../PersistentPlatform"), + .package( + url: "https://github.com/pointfreeco/swift-composable-architecture", + exact: "1.5.1" + ), + ], + targets: [ + .target( + name: "App", + dependencies: [ + "Counter", + .product(name: "Common", package: "Common"), + .product(name: "NetworkPlatform", package: "NetworkPlatform"), + .product(name: "PersistentPlatform", package: "PersistentPlatform"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture"), + ] + ), + + .target( + name: "Counter", + dependencies: [ + .product(name: "Common", package: "Common"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture"), + ] + ), + ] ) diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift new file mode 100644 index 0000000..8919e67 --- /dev/null +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift @@ -0,0 +1,52 @@ +// +// AppClient.swift +// Features +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Dependencies +import Domain +import Foundation +import NetworkPlatform +import PersistentPlatform + +public struct AppClient { + public var prepare: (()) async -> Void + public var save: @Sendable (_ request: Domain.Product) async throws -> Void + public var product: @Sendable (_ request: Int) async throws -> Domain.Product? +} + +extension AppClient { + private init( + _ prepare: PrepareCoreDataUseCase, productUseCase: ProductUseCase, + saveProduct: SaveProductUseCase + ) { + self.prepare = prepare.execute(input:) + self.save = saveProduct.execute(input:) + self.product = productUseCase.execute(input:) + } +} + +extension DependencyValues { + public var productClient: AppClient { + get { self[AppClient.self] } + set { self[AppClient.self] = newValue } + } +} + +extension AppClient: DependencyKey { + public static var liveValue = AppClient( + PrepareCoreDataUseCase(repository: PersistentRepository.live), + productUseCase: ProductUseCase(repository: NetworkRepository.live), + saveProduct: SaveProductUseCase(repository: PersistentRepository.live)) + public static var testValue = AppClient( + PrepareCoreDataUseCase(repository: PersistentRepository.live), + productUseCase: ProductUseCase(repository: NetworkRepository.stubbed), + saveProduct: SaveProductUseCase(repository: PersistentRepository.live)) + public static var previewValue = AppClient( + PrepareCoreDataUseCase(repository: PersistentRepository.live), + productUseCase: ProductUseCase(repository: PersistentRepository.stubbed), + saveProduct: SaveProductUseCase(repository: PersistentRepository.live)) +} diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift index 79b266f..3dcc0af 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift @@ -7,91 +7,128 @@ // import Common -import Counter import ComposableArchitecture +import Counter +import Domain public struct AppFeature: FeatureReducer { - public init() { } - - public struct State: Equatable, Hashable { - public init() { } - - @PresentationState var destination: Destination.State? - } - - public enum ViewAction: Equatable { - case showSheet - case showFullScreenCover - } - - public enum InternalAction: Equatable { - case dismissDestination + + @Dependency(\.productClient) var productClient + public init() {} + + public struct State: Equatable, Hashable { + public init() {} + + @PresentationState var destination: Destination.State? + var product: Product? + } + + public enum ViewAction: Equatable { + case onAppear + case showSheet + case showFullScreenCover + case save + } + + public enum InternalAction: Equatable { + case dismissDestination + case productResponse(TaskResult) + } + + public var body: some ReducerOf { + Reduce(core) + .ifLet(\.$destination, action: \.destination) { + Destination() + } + } + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect< + Action + > { + switch viewAction { + case .onAppear: + return .run { send in + await productClient.prepare(Void()) + await send( + .internal( + .productResponse( + TaskResult { + try await productClient.product(1) + }))) + } + case .showSheet: + state.destination = .sheet(.init()) + return .none + + case .showFullScreenCover: + state.destination = .fullScreenCover(.init()) + return .none + + case .save: + return .run { [product = state.product] send in + do { + try await productClient.save(product!) + } catch { + + } + } } - - public var body: some ReducerOf { - Reduce(core) - .ifLet(\.$destination, action: \.destination) { - Destination() - } + } + + public func reduce( + into state: inout State, presentedAction: Destination.Action + ) -> Effect { + switch presentedAction { + case .sheet(.delegate(.close)): + return .send(.internal(.dismissDestination)) + + case .fullScreenCover(.delegate(.close)): + return .send(.internal(.dismissDestination)) + + default: + return .none } - - public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { - switch viewAction { - case .showSheet: - state.destination = .sheet(.init()) - return .none - - case .showFullScreenCover: - state.destination = .fullScreenCover(.init()) - return .none - } + } + + public func reduce(into state: inout State, internalAction: InternalAction) + -> Effect + { + switch internalAction { + case let .productResponse(.success(product)): + state.product = product + return .none + case let .productResponse(.failure(error)): + print(error) + return .none + case .dismissDestination: + state.destination = nil + return .none } - - public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { - switch presentedAction { - case .sheet(.delegate(.close)): - return .send(.internal(.dismissDestination)) - - case .fullScreenCover(.delegate(.close)): - return .send(.internal(.dismissDestination)) - - default: - return .none - } + } + + public struct Destination: DestinationReducer { + + public init() {} + + @CasePathable + public enum State: Hashable { + case sheet(Counter.State) + case fullScreenCover(Counter.State) } - - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - switch internalAction { - case .dismissDestination: - state.destination = nil - return .none - } + + @CasePathable + public enum Action: Equatable { + case sheet(Counter.Action) + case fullScreenCover(Counter.Action) } - - public struct Destination: DestinationReducer { - - public init() { } - - @CasePathable - public enum State: Hashable { - case sheet(Counter.State) - case fullScreenCover(Counter.State) - } - - @CasePathable - public enum Action: Equatable { - case sheet(Counter.Action) - case fullScreenCover(Counter.Action) - } - - public var body: some ReducerOf { - Scope(state: \.sheet, action: \.sheet) { - Counter() - } - Scope(state: \.fullScreenCover, action: \.fullScreenCover) { - Counter() - } - } + + public var body: some ReducerOf { + Scope(state: \.sheet, action: \.sheet) { + Counter() + } + Scope(state: \.fullScreenCover, action: \.fullScreenCover) { + Counter() + } } + } } - diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift index b35029e..5410a47 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift @@ -7,80 +7,95 @@ // import Common -import Counter import ComposableArchitecture +import Counter import SwiftUI @MainActor public struct AppView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewstore in - Form { - Button { - viewstore.send(.view(.showSheet)) - } label: { - Text("Sheet") - } - - Button { - viewstore.send(.view(.showFullScreenCover)) - } label: { - Text("Full Screen Cover") - } - } - .onAppear() - .destinations(with: store) + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewstore in + Text("\(viewstore.product?.title ?? "Unknown")") + Text("\(viewstore.product?.description ?? "Unknown")") + + Button("Save") { + viewstore.send(.view(.save)) + } + + Form { + Button { + viewstore.send(.view(.showSheet)) + } label: { + Text("Sheet") + } + + Button { + viewstore.send(.view(.showFullScreenCover)) + } label: { + Text("Full Screen Cover") } + } + .onAppear { + viewstore.send(.view(.onAppear)) + } + .destinations(with: store) } + } } -private extension StoreOf { - var destination: PresentationStoreOf { - scope(state: \.$destination, action: \.destination) - } +extension StoreOf { + fileprivate var destination: PresentationStoreOf { + scope(state: \.$destination, action: \.destination) + } } @MainActor -private extension View { - func destinations(with store: StoreOf) -> some View { - let destinationStore = store.destination - return showSheet(with: destinationStore) - .showFulllScreenCover(with: destinationStore) - } - - private func showSheet(with destinationStore: PresentationStoreOf) -> some View { - sheet(store: - destinationStore.scope( - state: \.sheet, - action: \.sheet) - ) { store in - CounterView(store: store) - } +extension View { + fileprivate func destinations(with store: StoreOf) -> some View { + let destinationStore = store.destination + return showSheet(with: destinationStore) + .showFulllScreenCover(with: destinationStore) + } + + private func showSheet( + with destinationStore: PresentationStoreOf + ) -> some View { + sheet( + store: + destinationStore.scope( + state: \.sheet, + action: \.sheet) + ) { store in + CounterView(store: store) } - - private func showFulllScreenCover(with destinationStore: PresentationStoreOf) -> some View { - fullScreenCover(store: - destinationStore.scope( - state: \.fullScreenCover, - action: \.fullScreenCover) - ) { store in - CounterView(store: store) - } + } + + private func showFulllScreenCover( + with destinationStore: PresentationStoreOf + ) -> some View { + fullScreenCover( + store: + destinationStore.scope( + state: \.fullScreenCover, + action: \.fullScreenCover) + ) { store in + CounterView(store: store) } + } } - #Preview { - AppView(store: - .init( - initialState: AppFeature.State(), - reducer: { AppFeature() } - ) - ) + AppView( + store: + .init( + initialState: AppFeature.State(), + reducer: { AppFeature() } + ) + ) } diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift index a4a84f9..b0c0102 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift @@ -2,57 +2,61 @@ // CounterFeature.swift // Features // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // -import Foundation import Common import ComposableArchitecture +import Foundation public struct Counter: FeatureReducer { - - public init() { } - - public struct State: Equatable, Hashable { - public init() { } - - var count = 0 - } - - public enum ViewAction: Equatable { - case decrementButtonTapped - case incrementButtonTapped - case closeButtonTapped - } - - public enum InternalAction: Equatable { - case close - } - - public enum DelegateAction: Equatable { - case close - } - - public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { - switch viewAction { - case .decrementButtonTapped: - state.count -= 1 - return .none - - case .incrementButtonTapped: - state.count += 1 - return .none - - case .closeButtonTapped: - return .send(.internal(.close)) - } + + public init() {} + + public struct State: Equatable, Hashable { + public init() {} + + var count = 0 + } + + public enum ViewAction: Equatable { + case decrementButtonTapped + case incrementButtonTapped + case closeButtonTapped + } + + public enum InternalAction: Equatable { + case close + } + + public enum DelegateAction: Equatable { + case close + } + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect< + Action + > { + switch viewAction { + case .decrementButtonTapped: + state.count -= 1 + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + + case .closeButtonTapped: + return .send(.internal(.close)) } - - public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { - switch internalAction { - case .close: - return .send(.delegate(.close)) - } + } + + public func reduce(into state: inout State, internalAction: InternalAction) + -> Effect + { + switch internalAction { + case .close: + return .send(.delegate(.close)) } + } } diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift index 63fa1fd..5871b6d 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift @@ -2,8 +2,8 @@ // CounterView.swift // Features // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // import Common @@ -12,50 +12,51 @@ import SwiftUI @MainActor public struct CounterView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewstore in - VStack(spacing: 16) { - HStack { - Button { - viewstore.send(.view(.decrementButtonTapped)) - } label: { - Image(systemName: "minus") - } - - Text("\(viewstore.count)") - .monospacedDigit() - - Button { - viewstore.send(.view(.incrementButtonTapped)) - } label: { - Image(systemName: "plus") - } - } - - Button { - viewstore.send(.view(.closeButtonTapped)) - } label: { - Text("Dismiss") - .foregroundStyle(.white) - .frame(width: 120, height: 40) - .background(.blue) - } - } + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewstore in + VStack(spacing: 16) { + HStack { + Button { + viewstore.send(.view(.decrementButtonTapped)) + } label: { + Image(systemName: "minus") + } + + Text("\(viewstore.count)") + .monospacedDigit() + + Button { + viewstore.send(.view(.incrementButtonTapped)) + } label: { + Image(systemName: "plus") + } + } + + Button { + viewstore.send(.view(.closeButtonTapped)) + } label: { + Text("Dismiss") + .foregroundStyle(.white) + .frame(width: 120, height: 40) + .background(.blue) } + } } + } } #Preview { - CounterView(store: - .init( - initialState: Counter.State(), - reducer: { Counter() } - ) - ) + CounterView( + store: + .init( + initialState: Counter.State(), + reducer: { Counter() } + ) + ) } diff --git a/{{cookiecutter.app_name}}/Features/Tests/FeaturesTests/FeaturesTests.swift b/{{cookiecutter.app_name}}/Features/Tests/FeaturesTests/FeaturesTests.swift index 242654a..f909c90 100644 --- a/{{cookiecutter.app_name}}/Features/Tests/FeaturesTests/FeaturesTests.swift +++ b/{{cookiecutter.app_name}}/Features/Tests/FeaturesTests/FeaturesTests.swift @@ -1,11 +1,12 @@ import XCTest + @testable import Features final class FeaturesTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Features().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Features().text, "Hello, World!") + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/.swiftpm/xcode/xcshareddata/xcschemes/NetworkPlatform.xcscheme b/{{cookiecutter.app_name}}/NetworkPlatform/.swiftpm/xcode/xcshareddata/xcschemes/NetworkPlatform.xcscheme new file mode 100644 index 0000000..5f28b0e --- /dev/null +++ b/{{cookiecutter.app_name}}/NetworkPlatform/.swiftpm/xcode/xcshareddata/xcschemes/NetworkPlatform.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift index f037386..2ced42d 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift @@ -4,36 +4,40 @@ import PackageDescription let package = Package( - name: "NetworkPlatform", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library(name: "NetworkPlatform", - targets: ["NetworkPlatform"]) - ], - - dependencies: [ - .package(path: "../Domain"), - .package(path: "../Utilities"), - .package( - url: "https://github.com/Moya/Moya.git", - from: "15.0.3" - ), - ], - - targets: [ - .target( - name: "BuildConfiguration", - dependencies: []), - - .target( - name: "NetworkPlatform", - dependencies: [ - "BuildConfiguration", - .product(name: "Domain", package: "Domain"), - .product(name: "Utilities", package: "Utilities"), - .product(name: "Moya", package: "Moya"), - .product(name: "CombineMoya", package: "Moya") - ] - ) - ] + name: "NetworkPlatform", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library( + name: "NetworkPlatform", + targets: ["NetworkPlatform"]) + ], + + dependencies: [ + .package(path: "../Domain"), + .package(path: "../Utilities"), + .package( + url: "https://github.com/Moya/Moya.git", + from: "15.0.3" + ), + ], + + targets: [ + .target( + name: "BuildConfiguration", + dependencies: []), + + .target( + name: "NetworkPlatform", + dependencies: [ + "BuildConfiguration", + .product(name: "Domain", package: "Domain"), + .product(name: "Utilities", package: "Utilities"), + .product(name: "Moya", package: "Moya"), + .product(name: "CombineMoya", package: "Moya"), + ], + resources: [ + .copy("Resources/StubbedResponse/Product.json") + ] + ), + ] ) diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/BuildConfiguration/BuildConfiguration.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/BuildConfiguration/BuildConfiguration.swift index ea9d587..6c5bc03 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/BuildConfiguration/BuildConfiguration.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/BuildConfiguration/BuildConfiguration.swift @@ -9,46 +9,47 @@ import Foundation public class BuildConfiguration { - struct Configuration: Decodable { - let name: String - let baseURL: String - let testFlags: TestFlags? + struct Configuration: Decodable { + let name: String + let baseURL: String + let testFlags: TestFlags? + } + + struct TestFlags: Decodable { + let resetData: Bool + let noSplash: Bool + let applyTestData: Bool + } + + // MARK: Shared instance + public static let shared = BuildConfiguration() + + // MARK: Properties + private let configuration: Configuration + + // MARK: Lifecycle + + private init(name: String = "BuildConfiguration.plist") { + guard let filePath = Bundle.main.path(forResource: name, ofType: nil), + let fileData = FileManager.default.contents(atPath: filePath) + else { + fatalError("Configuration file '\(name)' not loadable!") } - struct TestFlags: Decodable { - let resetData: Bool - let noSplash: Bool - let applyTestData: Bool - } - - // MARK: Shared instance - public static let shared = BuildConfiguration() - - // MARK: Properties - private let configuration: Configuration - - // MARK: Lifecycle + do { + configuration = try PropertyListDecoder().decode( + Configuration.self, from: fileData) - private init (name: String = "BuildConfiguration.plist") { - guard let filePath = Bundle.main.path(forResource: name, ofType: nil), - let fileData = FileManager.default.contents(atPath: filePath) - else { - fatalError("Configuration file '\(name)' not loadable!") - } - - do { - configuration = try PropertyListDecoder().decode(Configuration.self, from: fileData) - - } catch { - fatalError("Configuration not decodable from '\(name)': \(error)") - } + } catch { + fatalError("Configuration not decodable from '\(name)': \(error)") } + } - public var baseURLString: String { - return configuration.baseURL - } + public var baseURLString: String { + return configuration.baseURL + } - public var configName: String { - return configuration.name - } + public var configName: String { + return configuration.name + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AppAPI.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AppAPI.swift new file mode 100644 index 0000000..720f532 --- /dev/null +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AppAPI.swift @@ -0,0 +1,73 @@ +// +// AppAPI.swift +// NetworkPlatform +// +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. +// + +import BuildConfiguration +import Foundation +import Moya + +enum AppAPI { + case product(id: Int) +} + +extension AppAPI: TargetType, ProductAPIType, Stubble { + var baseURL: URL { + return URL(string: "https://fakestoreapi.com")! + } + + var path: String { + switch self { + case let .product(id): return "/products/\(id)" + } + } + + var method: Moya.Method { + switch self { + default: + return .get + } + } + + var headers: [String: String]? { + return nil + } + + var parameters: [String: Any]? { + var params: [String: Any] = [:] + switch self { + default: break + } + return params + } + + var parameterEncoding: ParameterEncoding { + return URLEncoding.default + } + + var sampleData: Data { + var fileName = "" + switch self { + case .product: + fileName = "Product" + } + return stubbedResponse(fileName) + } + + var task: Task { + if let parameters = parameters { + return .requestParameters( + parameters: parameters, encoding: parameterEncoding) + } + return .requestPlain + } + + var addXAuth: Bool { + switch self { + default: return false + } + } +} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AuthAPI.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AuthAPI.swift index a4dc07d..9dc73f0 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AuthAPI.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/AuthAPI.swift @@ -2,54 +2,54 @@ // AuthAPI.swift // NetworkPlatform // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // import Foundation import Moya enum AuthAPI { - case accessToken + case accessToken } -extension AuthAPI: TargetType, ProductAPIType, Stubbable { - var baseURL: URL { - return URL(string: "https://api.punkapi.com/v2/token")! - } +extension AuthAPI: TargetType, ProductAPIType, Stubble { + var baseURL: URL { + return URL(string: "https://api.punkapi.com/v2/token")! + } - var path: String { - switch self { - case .accessToken: - return "/access_token" - } + var path: String { + switch self { + case .accessToken: + return "/access_token" } + } - var method: Moya.Method { - return .get - } + var method: Moya.Method { + return .get + } - var task: Task { - switch self { - default: - return .requestPlain - } + var task: Task { + switch self { + default: + return .requestPlain } + } - var sampleData: Data { - switch self { - case .accessToken: - return stubbedResponse("AccessToken") - } + var sampleData: Data { + switch self { + case .accessToken: + return stubbedResponse("AccessToken") } + } - var headers: [String: String]? { - return nil - } + var headers: [String: String]? { + return nil + } - var addXAuth: Bool { - switch self { - default: return true - } + var addXAuth: Bool { + switch self { + default: return true } + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/TrendingGithubAPI.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/TrendingGithubAPI.swift deleted file mode 100644 index f7ab046..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/API/TrendingGithubAPI.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// TrendingGithubAPI.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation -import BuildConfiguration -import Moya - -enum TrendingGithubAPI { - case trendingRepositories(language: String, since: String) - case trendingDevelopers(language: String, since: String) - case languages -} - -extension TrendingGithubAPI: TargetType, ProductAPIType, Stubbable { - var baseURL: URL { - return URL(string: BuildConfiguration.shared.baseURLString)! - } - - var path: String { - switch self { - case .trendingRepositories: return "/repositories" - case .trendingDevelopers: return "/developers" - case .languages: return "/languages" - } - } - - var method: Moya.Method { - switch self { - default: - return .get - } - } - - var headers: [String: String]? { - return nil - } - - var parameters: [String: Any]? { - var params: [String: Any] = [:] - switch self { - case let .trendingRepositories(language, since), - let .trendingDevelopers(language, since): - params["language"] = language - params["since"] = since - default: break - } - return params - } - - var parameterEncoding: ParameterEncoding { - return URLEncoding.default - } - - var sampleData: Data { - var fileName = "" - switch self { - case .trendingDevelopers: - fileName = "UserTrendings" - case .trendingRepositories: - fileName = "RepositoryTrendings" - case .languages: - fileName = "Languages" - } - return stubbedResponse(fileName) - } - - var task: Task { - if let parameters = parameters { - return .requestParameters(parameters: parameters, encoding: parameterEncoding) - } - return .requestPlain - } - - var addXAuth: Bool { - switch self { - default: return false - } - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/HTTPStatusCode.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/HTTPStatusCode.swift index e7941cb..6ca0d06 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/HTTPStatusCode.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/HTTPStatusCode.swift @@ -2,278 +2,278 @@ // HTTPStatusCode.swift // NetworkPlatform // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // import Foundation enum HTTPStatusCode: Int, Error { - /// The response class representation of status codes, these get grouped by their first digit. - enum ResponseType { - /// - informational: This class of status code indicates a provisional response, consisting only of the Status-Line and optional headers, and is terminated by an empty line. - case informational + /// The response class representation of status codes, these get grouped by their first digit. + enum ResponseType { + /// - informational: This class of status code indicates a provisional response, consisting only of the Status-Line and optional headers, and is terminated by an empty line. + case informational - /// - success: This class of status codes indicates the action requested by the client was received, understood, accepted, and processed successfully. - case success + /// - success: This class of status codes indicates the action requested by the client was received, understood, accepted, and processed successfully. + case success - /// - redirection: This class of status code indicates the client must take additional action to complete the request. - case redirection + /// - redirection: This class of status code indicates the client must take additional action to complete the request. + case redirection - /// - clientError: This class of status code is intended for situations in which the client seems to have erred. - case clientError + /// - clientError: This class of status code is intended for situations in which the client seems to have erred. + case clientError - /// - serverError: This class of status code indicates the server failed to fulfill an apparently valid request. - case serverError + /// - serverError: This class of status code indicates the server failed to fulfill an apparently valid request. + case serverError - /// - undefined: The class of the status code cannot be resolved. - case undefined - } + /// - undefined: The class of the status code cannot be resolved. + case undefined + } - // - // Informational - 1xx - // + // + // Informational - 1xx + // - /// - continue: The server has received the request headers and the client should proceed to send the request body. - case `continue` = 100 + /// - continue: The server has received the request headers and the client should proceed to send the request body. + case `continue` = 100 - /// - switchingProtocols: The requester has asked the server to switch protocols and the server has agreed to do so. - case switchingProtocols = 101 + /// - switchingProtocols: The requester has asked the server to switch protocols and the server has agreed to do so. + case switchingProtocols = 101 - /// - processing: This code indicates that the server has received and is processing the request, but no response is available yet. - case processing = 102 + /// - processing: This code indicates that the server has received and is processing the request, but no response is available yet. + case processing = 102 - // - // Success - 2xx - // + // + // Success - 2xx + // - /// - ok: Standard response for successful HTTP requests. - case ok = 200 + /// - ok: Standard response for successful HTTP requests. + case ok = 200 - /// - created: The request has been fulfilled, resulting in the creation of a new resource. - case created = 201 + /// - created: The request has been fulfilled, resulting in the creation of a new resource. + case created = 201 - /// - accepted: The request has been accepted for processing, but the processing has not been completed. - case accepted = 202 + /// - accepted: The request has been accepted for processing, but the processing has not been completed. + case accepted = 202 - /// - nonAuthoritativeInformation: The server is a transforming proxy (e.g. a Web accelerator) that received a 200 OK from its origin, but is returning a modified version of the origin's response. - case nonAuthoritativeInformation = 203 + /// - nonAuthoritativeInformation: The server is a transforming proxy (e.g. a Web accelerator) that received a 200 OK from its origin, but is returning a modified version of the origin's response. + case nonAuthoritativeInformation = 203 - /// - noContent: The server successfully processed the request and is not returning any content. - case noContent = 204 + /// - noContent: The server successfully processed the request and is not returning any content. + case noContent = 204 - /// - resetContent: The server successfully processed the request, but is not returning any content. - case resetContent = 205 + /// - resetContent: The server successfully processed the request, but is not returning any content. + case resetContent = 205 - /// - partialContent: The server is delivering only part of the resource (byte serving) due to a range header sent by the client. - case partialContent = 206 + /// - partialContent: The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + case partialContent = 206 - /// - multiStatus: The message body that follows is an XML message and can contain a number of separate response codes, depending on how many sub-requests were made. - case multiStatus = 207 + /// - multiStatus: The message body that follows is an XML message and can contain a number of separate response codes, depending on how many sub-requests were made. + case multiStatus = 207 - /// - alreadyReported: The members of a DAV binding have already been enumerated in a previous reply to this request, and are not being included again. - case alreadyReported = 208 + /// - alreadyReported: The members of a DAV binding have already been enumerated in a previous reply to this request, and are not being included again. + case alreadyReported = 208 - /// - IMUsed: The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance. - case IMUsed = 226 + /// - IMUsed: The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + case IMUsed = 226 - // - // Redirection - 3xx - // + // + // Redirection - 3xx + // - /// - multipleChoices: Indicates multiple options for the resource from which the client may choose - case multipleChoices = 300 + /// - multipleChoices: Indicates multiple options for the resource from which the client may choose + case multipleChoices = 300 - /// - movedPermanently: This and all future requests should be directed to the given URI. - case movedPermanently = 301 + /// - movedPermanently: This and all future requests should be directed to the given URI. + case movedPermanently = 301 - /// - found: The resource was found. - case found = 302 + /// - found: The resource was found. + case found = 302 - /// - seeOther: The response to the request can be found under another URI using a GET method. - case seeOther = 303 + /// - seeOther: The response to the request can be found under another URI using a GET method. + case seeOther = 303 - /// - notModified: Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. - case notModified = 304 + /// - notModified: Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + case notModified = 304 - /// - useProxy: The requested resource is available only through a proxy, the address for which is provided in the response. - case useProxy = 305 + /// - useProxy: The requested resource is available only through a proxy, the address for which is provided in the response. + case useProxy = 305 - /// - switchProxy: No longer used. Originally meant "Subsequent requests should use the specified proxy. - case switchProxy = 306 + /// - switchProxy: No longer used. Originally meant "Subsequent requests should use the specified proxy. + case switchProxy = 306 - /// - temporaryRedirect: The request should be repeated with another URI. - case temporaryRedirect = 307 + /// - temporaryRedirect: The request should be repeated with another URI. + case temporaryRedirect = 307 - /// - permenantRedirect: The request and all future requests should be repeated using another URI. - case permenantRedirect = 308 + /// - permenantRedirect: The request and all future requests should be repeated using another URI. + case permenantRedirect = 308 - // - // Client Error - 4xx - // + // + // Client Error - 4xx + // - /// - badRequest: The server cannot or will not process the request due to an apparent client error. - case badRequest = 400 + /// - badRequest: The server cannot or will not process the request due to an apparent client error. + case badRequest = 400 - /// - unauthorized: Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. - case unauthorized = 401 + /// - unauthorized: Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. + case unauthorized = 401 - /// - paymentRequired: The content available on the server requires payment. - case paymentRequired = 402 + /// - paymentRequired: The content available on the server requires payment. + case paymentRequired = 402 - /// - forbidden: The request was a valid request, but the server is refusing to respond to it. - case forbidden = 403 + /// - forbidden: The request was a valid request, but the server is refusing to respond to it. + case forbidden = 403 - /// - notFound: The requested resource could not be found but may be available in the future. - case notFound = 404 + /// - notFound: The requested resource could not be found but may be available in the future. + case notFound = 404 - /// - methodNotAllowed: A request method is not supported for the requested resource. e.g. a GET request on a form which requires data to be presented via POST - case methodNotAllowed = 405 + /// - methodNotAllowed: A request method is not supported for the requested resource. e.g. a GET request on a form which requires data to be presented via POST + case methodNotAllowed = 405 - /// - notAcceptable: The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. - case notAcceptable = 406 + /// - notAcceptable: The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + case notAcceptable = 406 - /// - proxyAuthenticationRequired: The client must first authenticate itself with the proxy. - case proxyAuthenticationRequired = 407 + /// - proxyAuthenticationRequired: The client must first authenticate itself with the proxy. + case proxyAuthenticationRequired = 407 - /// - requestTimeout: The server timed out waiting for the request. - case requestTimeout = 408 + /// - requestTimeout: The server timed out waiting for the request. + case requestTimeout = 408 - /// - conflict: Indicates that the request could not be processed because of conflict in the request, such as an edit conflict between multiple simultaneous updates. - case conflict = 409 + /// - conflict: Indicates that the request could not be processed because of conflict in the request, such as an edit conflict between multiple simultaneous updates. + case conflict = 409 - /// - gone: Indicates that the resource requested is no longer available and will not be available again. - case gone = 410 + /// - gone: Indicates that the resource requested is no longer available and will not be available again. + case gone = 410 - /// - lengthRequired: The request did not specify the length of its content, which is required by the requested resource. - case lengthRequired = 411 + /// - lengthRequired: The request did not specify the length of its content, which is required by the requested resource. + case lengthRequired = 411 - /// - preconditionFailed: The server does not meet one of the preconditions that the requester put on the request. - case preconditionFailed = 412 + /// - preconditionFailed: The server does not meet one of the preconditions that the requester put on the request. + case preconditionFailed = 412 - /// - payloadTooLarge: The request is larger than the server is willing or able to process. - case payloadTooLarge = 413 + /// - payloadTooLarge: The request is larger than the server is willing or able to process. + case payloadTooLarge = 413 - /// - URITooLong: The URI provided was too long for the server to process. - case URITooLong = 414 + /// - URITooLong: The URI provided was too long for the server to process. + case URITooLong = 414 - /// - unsupportedMediaType: The request entity has a media type which the server or resource does not support. - case unsupportedMediaType = 415 + /// - unsupportedMediaType: The request entity has a media type which the server or resource does not support. + case unsupportedMediaType = 415 - /// - rangeNotSatisfiable: The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. - case rangeNotSatisfiable = 416 + /// - rangeNotSatisfiable: The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + case rangeNotSatisfiable = 416 - /// - expectationFailed: The server cannot meet the requirements of the Expect request-header field. - case expectationFailed = 417 + /// - expectationFailed: The server cannot meet the requirements of the Expect request-header field. + case expectationFailed = 417 - /// - teapot: This HTTP status is used as an Easter egg in some websites. - case teapot = 418 + /// - teapot: This HTTP status is used as an Easter egg in some websites. + case teapot = 418 - /// - misdirectedRequest: The request was directed at a server that is not able to produce a response. - case misdirectedRequest = 421 + /// - misdirectedRequest: The request was directed at a server that is not able to produce a response. + case misdirectedRequest = 421 - /// - unprocessableEntity: The request was well-formed but was unable to be followed due to semantic errors. - case unprocessableEntity = 422 + /// - unprocessableEntity: The request was well-formed but was unable to be followed due to semantic errors. + case unprocessableEntity = 422 - /// - locked: The resource that is being accessed is locked. - case locked = 423 + /// - locked: The resource that is being accessed is locked. + case locked = 423 - /// - failedDependency: The request failed due to failure of a previous request (e.g., a PROPPATCH). - case failedDependency = 424 + /// - failedDependency: The request failed due to failure of a previous request (e.g., a PROPPATCH). + case failedDependency = 424 - /// - upgradeRequired: The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. - case upgradeRequired = 426 + /// - upgradeRequired: The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + case upgradeRequired = 426 - /// - preconditionRequired: The origin server requires the request to be conditional. - case preconditionRequired = 428 + /// - preconditionRequired: The origin server requires the request to be conditional. + case preconditionRequired = 428 - /// - tooManyRequests: The user has sent too many requests in a given amount of time. - case tooManyRequests = 429 + /// - tooManyRequests: The user has sent too many requests in a given amount of time. + case tooManyRequests = 429 - /// - requestHeaderFieldsTooLarge: The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large. - case requestHeaderFieldsTooLarge = 431 + /// - requestHeaderFieldsTooLarge: The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large. + case requestHeaderFieldsTooLarge = 431 - /// - noResponse: Used to indicate that the server has returned no information to the client and closed the connection. - case noResponse = 444 + /// - noResponse: Used to indicate that the server has returned no information to the client and closed the connection. + case noResponse = 444 - /// - unavailableForLegalReasons: A server operator has received a legal demand to deny access to a resource or to a set of resources that includes the requested resource. - case unavailableForLegalReasons = 451 + /// - unavailableForLegalReasons: A server operator has received a legal demand to deny access to a resource or to a set of resources that includes the requested resource. + case unavailableForLegalReasons = 451 - /// - SSLCertificateError: An expansion of the 400 Bad Request response code, used when the client has provided an invalid client certificate. - case SSLCertificateError = 495 + /// - SSLCertificateError: An expansion of the 400 Bad Request response code, used when the client has provided an invalid client certificate. + case SSLCertificateError = 495 - /// - SSLCertificateRequired: An expansion of the 400 Bad Request response code, used when a client certificate is required but not provided. - case SSLCertificateRequired = 496 + /// - SSLCertificateRequired: An expansion of the 400 Bad Request response code, used when a client certificate is required but not provided. + case SSLCertificateRequired = 496 - /// - HTTPRequestSentToHTTPSPort: An expansion of the 400 Bad Request response code, used when the client has made a HTTP request to a port listening for HTTPS requests. - case HTTPRequestSentToHTTPSPort = 497 + /// - HTTPRequestSentToHTTPSPort: An expansion of the 400 Bad Request response code, used when the client has made a HTTP request to a port listening for HTTPS requests. + case HTTPRequestSentToHTTPSPort = 497 - /// - clientClosedRequest: Used when the client has closed the request before the server could send a response. - case clientClosedRequest = 499 + /// - clientClosedRequest: Used when the client has closed the request before the server could send a response. + case clientClosedRequest = 499 - // - // Server Error - 5xx - // + // + // Server Error - 5xx + // - /// - internalServerError: A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. - case internalServerError = 500 + /// - internalServerError: A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + case internalServerError = 500 - /// - notImplemented: The server either does not recognize the request method, or it lacks the ability to fulfill the request. - case notImplemented = 501 + /// - notImplemented: The server either does not recognize the request method, or it lacks the ability to fulfill the request. + case notImplemented = 501 - /// - badGateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server. - case badGateway = 502 + /// - badGateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server. + case badGateway = 502 - /// - serviceUnavailable: The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state. - case serviceUnavailable = 503 + /// - serviceUnavailable: The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state. + case serviceUnavailable = 503 - /// - gatewayTimeout: The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. - case gatewayTimeout = 504 + /// - gatewayTimeout: The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + case gatewayTimeout = 504 - /// - HTTPVersionNotSupported: The server does not support the HTTP protocol version used in the request. - case HTTPVersionNotSupported = 505 + /// - HTTPVersionNotSupported: The server does not support the HTTP protocol version used in the request. + case HTTPVersionNotSupported = 505 - /// - variantAlsoNegotiates: Transparent content negotiation for the request results in a circular reference. - case variantAlsoNegotiates = 506 + /// - variantAlsoNegotiates: Transparent content negotiation for the request results in a circular reference. + case variantAlsoNegotiates = 506 - /// - insufficientStorage: The server is unable to store the representation needed to complete the request. - case insufficientStorage = 507 + /// - insufficientStorage: The server is unable to store the representation needed to complete the request. + case insufficientStorage = 507 - /// - loopDetected: The server detected an infinite loop while processing the request. - case loopDetected = 508 + /// - loopDetected: The server detected an infinite loop while processing the request. + case loopDetected = 508 - /// - notExtended: Further extensions to the request are required for the server to fulfill it. - case notExtended = 510 + /// - notExtended: Further extensions to the request are required for the server to fulfill it. + case notExtended = 510 - /// - networkAuthenticationRequired: The client needs to authenticate to gain network access. - case networkAuthenticationRequired = 511 + /// - networkAuthenticationRequired: The client needs to authenticate to gain network access. + case networkAuthenticationRequired = 511 - /// The class (or group) which the status code belongs to. - var responseType: ResponseType { - switch rawValue { - case 100 ..< 200: - return .informational + /// The class (or group) which the status code belongs to. + var responseType: ResponseType { + switch rawValue { + case 100..<200: + return .informational - case 200 ..< 300: - return .success + case 200..<300: + return .success - case 300 ..< 400: - return .redirection + case 300..<400: + return .redirection - case 400 ..< 500: - return .clientError + case 400..<500: + return .clientError - case 500 ..< 600: - return .serverError + case 500..<600: + return .serverError - default: - return .undefined - } + default: + return .undefined } + } } extension HTTPURLResponse { - var status: HTTPStatusCode? { - return HTTPStatusCode(rawValue: statusCode) - } + var status: HTTPStatusCode? { + return HTTPStatusCode(rawValue: statusCode) + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/LocalNotification.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/LocalNotification.swift index e1299fc..679a3fe 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/LocalNotification.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/LocalNotification.swift @@ -2,8 +2,8 @@ // LocalNotification.swift // NetworkPlatform // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // import Combine @@ -12,21 +12,21 @@ import Foundation // https://stackoverflow.com/questions/58559908/combine-going-from-notification-center-addobserver-with-selector-to-notificatio class NetworkLoadingNotificationSender { - var loading: Bool + var loading: Bool - init(_ loadingToSend: Bool) { - loading = loadingToSend - } + init(_ loadingToSend: Bool) { + loading = loadingToSend + } - static let notification = Notification.Name("NetworkLoadingNotification") + static let notification = Notification.Name("NetworkLoadingNotification") } class NetworkInfoNotificationSender { - var message: String + var message: String - init(_ messageToSend: String) { - message = messageToSend - } + init(_ messageToSend: String) { + message = messageToSend + } - static let notification = Notification.Name("NetworkPopupNotification") + static let notification = Notification.Name("NetworkPopupNotification") } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/Stubbable.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/Stubbable.swift index deb60ee..ffc56d1 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/Stubbable.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Common/Stubbable.swift @@ -2,23 +2,20 @@ // Stubbable.swift // NetworkPlatform // -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// Created by Rokon on 24/01/2024. +// Copyright © 2024 MLBD. All rights reserved. // import Foundation -protocol Stubbable { - +protocol Stubble { + } -extension Stubbable { - - func stubbedResponse(_ filename: String) -> Data! { - let bundlePath = Bundle.main.path(forResource: "Stub", ofType: "bundle") - let bundle = Bundle(path: bundlePath!) - let path = bundle?.path(forResource: filename, ofType: "json") - return (try? Data(contentsOf: URL(fileURLWithPath: path!))) - } - +extension Stubble { + func stubbedResponse(_ filename: String) -> Data! { + let path = Bundle.module.path( + forResource: filename, ofType: "json") + return (try? Data(contentsOf: URL(fileURLWithPath: path!))) + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/AppNetworking.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/AppNetworking.swift new file mode 100644 index 0000000..dad7a5b --- /dev/null +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/AppNetworking.swift @@ -0,0 +1,74 @@ +// +// AppNetworking.swift +// NetworkPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Combine +import Domain +import Foundation +import Moya + +struct AppNetworking: NetworkingType { + typealias T = AppAPI + let provider: OnlineProvider + + static func defaultNetworking() -> Self { + return AppNetworking( + provider: + OnlineProvider( + endpointClosure: AppNetworking.endpointsClosure(), + requestClosure: AppNetworking.endpointResolver(), + stubClosure: AppNetworking.APIKeysBasedStubBehaviour, + online: Just(true).setFailureType(to: Never.self) + .eraseToAnyPublisher())) + } + + static func stubbingNetworking() -> Self { + return AppNetworking( + provider: + OnlineProvider( + endpointClosure: endpointsClosure(), + requestClosure: AppNetworking.endpointResolver(), + stubClosure: MoyaProvider.immediatelyStub, + online: Just(true).setFailureType(to: Never.self) + .eraseToAnyPublisher())) + } + + func request(_ target: T) -> AnyPublisher { + let actualRequest = provider.request(target) + return actualRequest + } + + func requestObject( + _ target: AppAPI, + type _: T.Type + ) async throws -> T { + return try await request(target) + .filterSuccessfulStatusCodes() + .map(T.self) + .mapError { error -> AppError in + AppError.networkingFailed(underlyingError: error, context: "Network") + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + .async() + } + + func requestArray( + _ target: AppAPI, + type _: T.Type + ) async throws -> [T] { + return try await request(target) + .filterSuccessfulStatusCodes() + .map([T].self) + .mapError { error -> AppError in + AppError.networkingFailed(underlyingError: error, context: "Network") + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + .async() + } +} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkProvider.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkProvider.swift deleted file mode 100644 index 53bcd02..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NetworkProvider.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -final class NetworkProvider { - public func makeTrendingGithubNetworking() -> TrendingGithubNetworking { - return TrendingGithubNetworking.defaultNetworking() - } - - public func makeTrendingGithubNetworkingStubbed() -> TrendingGithubNetworking { - return TrendingGithubNetworking.stubbingNetworking() - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/Networking.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/Networking.swift index 9c44289..75974dd 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/Networking.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/Networking.swift @@ -14,77 +14,92 @@ import Moya import Utilities class OnlineProvider where Target: Moya.TargetType { - private let online: AnyPublisher - private let provider: MoyaProvider - private var authprovider = MoyaProvider() + private let online: AnyPublisher + private let provider: MoyaProvider + private var authprovider = MoyaProvider() - init(endpointClosure: @escaping MoyaProvider.EndpointClosure = MoyaProvider.defaultEndpointMapping, - requestClosure: @escaping MoyaProvider.RequestClosure = MoyaProvider.defaultRequestMapping, - stubClosure: @escaping MoyaProvider.StubClosure = MoyaProvider.neverStub, - session: Session = MoyaProvider.defaultAlamofireSession(), - plugins: [PluginType] = [VerbosePlugin(verbose: true)], - trackInflights: Bool = false, - online: AnyPublisher) - { - self.online = online - provider = MoyaProvider(endpointClosure: endpointClosure, - requestClosure: requestClosure, - stubClosure: stubClosure, - session: session, - plugins: plugins, - trackInflights: trackInflights) - } + init( + endpointClosure: @escaping MoyaProvider.EndpointClosure = + MoyaProvider.defaultEndpointMapping, + requestClosure: @escaping MoyaProvider.RequestClosure = + MoyaProvider.defaultRequestMapping, + stubClosure: @escaping MoyaProvider.StubClosure = MoyaProvider< + Target + >.neverStub, + session: Session = MoyaProvider.defaultAlamofireSession(), + plugins: [PluginType] = [VerbosePlugin(verbose: true)], + trackInflights: Bool = false, + online: AnyPublisher + ) { + self.online = online + provider = MoyaProvider( + endpointClosure: endpointClosure, + requestClosure: requestClosure, + stubClosure: stubClosure, + session: session, + plugins: plugins, + trackInflights: trackInflights) + } - func request(_ target: Target) -> AnyPublisher { - return provider.requestPublisher(target) - .tryCatch { [weak self] error -> AnyPublisher in - guard let `self` = self else { throw error } - // 401 Error, update access token - if let response = error.response, - let statusCode = HTTPStatusCode(rawValue: response.statusCode), - statusCode == .unauthorized - { - // TODO: handle network retry - return self.fetchAccessToken(target: target) - } else { - throw error - } - } - // TODO: investigate - .mapError { $0 as! MoyaError } - .handleEvents(receiveOutput: { response in - Logger.info("Status Code: \(response.statusCode)") - }, receiveCompletion: { completion in - switch completion { - case .finished: - Logger.success("Request Finished") - case let .failure(error): - // TODO: handle network error - Logger.error(error.localizedDescription) - } - }) - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } + func request(_ target: Target) -> AnyPublisher { + return provider.requestPublisher(target) + .tryCatch { [weak self] error -> AnyPublisher in + guard let `self` = self else { throw error } + // 401 Error, update access token + if let response = error.response, + let statusCode = HTTPStatusCode(rawValue: response.statusCode), + statusCode == .unauthorized + { + // TODO: handle network retry + return self.fetchAccessToken(target: target) + } else { + throw error + } + } + // TODO: investigate + .mapError { $0 as! MoyaError } + .handleEvents( + receiveOutput: { response in + Logger.info("Status Code: \(response.statusCode)") + }, + receiveCompletion: { completion in + switch completion { + case .finished: + Logger.success("Request Finished") + case let .failure(error): + // TODO: handle network error + Logger.error(error.localizedDescription) + } + } + ) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } - func fetchAccessToken(target: Target) -> AnyPublisher { - return authprovider - .requestPublisher(.accessToken) - .handleEvents(receiveOutput: { _ in + func fetchAccessToken(target: Target) -> AnyPublisher< + Moya.Response, MoyaError + > { + return + authprovider + .requestPublisher(.accessToken) + .handleEvents( + receiveOutput: { _ in - }, receiveCompletion: { completion in - switch completion { - case .finished: - // TODO: Update new tokens - break - case let .failure(error): - Logger.error(error.localizedDescription) - // TODO: delete existing token and logout from app - } + }, + receiveCompletion: { completion in + switch completion { + case .finished: + // TODO: Update new tokens + break + case let .failure(error): + Logger.error(error.localizedDescription) + // TODO: delete existing token and logout from app + } - }) - .flatMap { _ in // TODO: fix use weak self - self.request(target) - }.eraseToAnyPublisher() - } + } + ) + .flatMap { _ in // TODO: fix use weak self + self.request(target) + }.eraseToAnyPublisher() + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingError.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingError.swift index 661b831..13e7202 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingError.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingError.swift @@ -9,15 +9,15 @@ import Foundation enum NetworkingError: Error { - case error(String) - case defaultError + case error(String) + case defaultError - var message: String { - switch self { - case let .error(msg): - return msg - case .defaultError: - return "Please try again later." - } + var message: String { + switch self { + case let .error(msg): + return msg + case .defaultError: + return "Please try again later." } + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift index 81b0cbb..757f50b 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift @@ -13,89 +13,93 @@ import Moya import Utilities protocol ProductAPIType { - var addXAuth: Bool { get } + var addXAuth: Bool { get } } protocol NetworkingType { - associatedtype T: TargetType, ProductAPIType - var provider: OnlineProvider { get } + associatedtype T: TargetType, ProductAPIType + var provider: OnlineProvider { get } - static func defaultNetworking() -> Self - static func stubbingNetworking() -> Self + static func defaultNetworking() -> Self + static func stubbingNetworking() -> Self } extension NetworkingType { - static func endpointsClosure(_: String? = nil) -> (T) -> Endpoint where T: TargetType, T: ProductAPIType { - return { target in - let endpoint = MoyaProvider.defaultEndpointMapping(for: target) - Logger.info("Endpoint URL: \(endpoint.url)") - // Sign all non-XApp, non-XAuth token requests - return endpoint - } + static func endpointsClosure(_: String? = nil) -> (T) -> Endpoint + where T: TargetType, T: ProductAPIType { + return { target in + let endpoint = MoyaProvider.defaultEndpointMapping(for: target) + Logger.info("Endpoint URL: \(endpoint.url)") + // Sign all non-XApp, non-XAuth token requests + return endpoint } + } - static func APIKeysBasedStubBehaviour(_: T) -> Moya.StubBehavior { - return .never - } + static func APIKeysBasedStubBehaviour(_: T) -> Moya.StubBehavior { + return .never + } - static var plugins: [PluginType] { - var plugins: [PluginType] = [] - plugins.append(NetworkLoggerPlugin()) - return plugins - } + static var plugins: [PluginType] { + var plugins: [PluginType] = [] + plugins.append(NetworkLoggerPlugin()) + return plugins + } - // (Endpoint, NSURLRequest -> Void) -> Void - static func endpointResolver() -> MoyaProvider.RequestClosure { - return { endpoint, closure in - do { - var request = try endpoint.urlRequest() // endpoint.urlRequest - request.httpShouldHandleCookies = false - closure(.success(request)) - } catch { - Logger.error(error.localizedDescription) - } - } + // (Endpoint, NSURLRequest -> Void) -> Void + static func endpointResolver() -> MoyaProvider.RequestClosure { + return { endpoint, closure in + do { + var request = try endpoint.urlRequest() // endpoint.urlRequest + request.httpShouldHandleCookies = false + closure(.success(request)) + } catch { + Logger.error(error.localizedDescription) + } } + } -// func requestObject(_ target: T, type: Element.Type) -> AnyPublisher { -// return provider.request(target) -// .filterSuccessfulStatusCodes() -// .map(Element.self) -// .mapError { NetworkingError.error($0.localizedDescription) } -// // TODO: fetch from cache -// .tryCatch { _ in self.coreDataManager.localRandom() } -// .mapError { NetworkingError.error($0.localizedDescription) } -// .eraseToAnyPublisher() -// } + // func requestObject(_ target: T, type: Element.Type) -> AnyPublisher { + // return provider.request(target) + // .filterSuccessfulStatusCodes() + // .map(Element.self) + // .mapError { NetworkingError.error($0.localizedDescription) } + // // TODO: fetch from cache + // .tryCatch { _ in self.coreDataManager.localRandom() } + // .mapError { NetworkingError.error($0.localizedDescription) } + // .eraseToAnyPublisher() + // } - func requestArray(_ target: T, type _: Element.Type) -> AnyPublisher<[Element], NetworkingError> { - provider.request(target) - .filterSuccessfulStatusCodes() - .map([Element].self) - .mapError { NetworkingError.error($0.localizedDescription) } - // TODO: fetch from cache -// .tryCatch { _ in self.coreDataManager.localRandom() } - .mapError { NetworkingError.error($0.localizedDescription) } - .eraseToAnyPublisher() - } + func requestArray(_ target: T, type _: Element.Type) + -> AnyPublisher<[Element], NetworkingError> + { + provider.request(target) + .filterSuccessfulStatusCodes() + .map([Element].self) + .mapError { NetworkingError.error($0.localizedDescription) } + // TODO: fetch from cache + // .tryCatch { _ in self.coreDataManager.localRandom() } + .mapError { NetworkingError.error($0.localizedDescription) } + .eraseToAnyPublisher() + } } // MARK: - Provider support func stubbedResponse(_ filename: String) -> Data! { - @objc class TestClass: NSObject {} + @objc class TestClass: NSObject {} - let bundle = Bundle(for: TestClass.self) - let path = bundle.path(forResource: filename, ofType: "json") - return (try? Data(contentsOf: URL(fileURLWithPath: path!))) + let bundle = Bundle(for: TestClass.self) + let path = bundle.path(forResource: filename, ofType: "json") + return (try? Data(contentsOf: URL(fileURLWithPath: path!))) } -private extension String { - var URLEscapedString: String { - return addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)! - } +extension String { + fileprivate var URLEscapedString: String { + return addingPercentEncoding( + withAllowedCharacters: CharacterSet.urlHostAllowed)! + } } func url(_ route: TargetType) -> String { - return route.baseURL.appendingPathComponent(route.path).absoluteString + return route.baseURL.appendingPathComponent(route.path).absoluteString } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/TrendingGithubNetworking.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/TrendingGithubNetworking.swift deleted file mode 100644 index b251313..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/TrendingGithubNetworking.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// TrendingGithubNetworking.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Domain -import Foundation -import Moya - -struct TrendingGithubNetworking: NetworkingType { - typealias T = TrendingGithubAPI - let provider: OnlineProvider - - static func defaultNetworking() -> Self { - return TrendingGithubNetworking(provider: - OnlineProvider(endpointClosure: TrendingGithubNetworking.endpointsClosure(), - requestClosure: TrendingGithubNetworking.endpointResolver(), - stubClosure: TrendingGithubNetworking.APIKeysBasedStubBehaviour, - online: Just(true).setFailureType(to: Never.self) - .eraseToAnyPublisher())) - } - - static func stubbingNetworking() -> Self { - return TrendingGithubNetworking(provider: - OnlineProvider(endpointClosure: endpointsClosure(), - requestClosure: TrendingGithubNetworking.endpointResolver(), - stubClosure: MoyaProvider.immediatelyStub, - online: Just(true).setFailureType(to: Never.self) - .eraseToAnyPublisher())) - } - - func request(_ target: T) -> AnyPublisher { - let actualRequest = provider.request(target) - return actualRequest - } - - private func trendingRequestObject(_ target: TrendingGithubAPI, - type _: T.Type) async throws -> T - { - return try await request(target) - .filterSuccessfulStatusCodes() - .map(T.self) - .mapError { error -> AppError in - AppError(code: error.errorCode, title: nil, message: error.localizedDescription) - } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - .async() - } - - private func trendingRequestArray(_ target: TrendingGithubAPI, - type _: T.Type) async throws -> [T] - { - return try await request(target) - .filterSuccessfulStatusCodes() - .map([T].self) - .mapError { error -> AppError in - AppError(code: error.errorCode, title: nil, message: error.localizedDescription) - } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - .async() - } -} - -extension TrendingGithubNetworking { - func trendingRepositories(language: String, - since: String) async throws -> [TrendingRepository] - { - return try await trendingRequestArray(.trendingRepositories(language: language, - since: since), - type: TrendingRepository.self) - } - - func trendingDevelopers(language: String, - since: String) async throws -> [TrendingUser] - { - return try await trendingRequestArray(.trendingDevelopers(language: language, - since: since), - type: TrendingUser.self) - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/NetworkRepository.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/NetworkRepository.swift new file mode 100644 index 0000000..f72312f --- /dev/null +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/NetworkRepository.swift @@ -0,0 +1,47 @@ +// +// NetworkRepository.swift +// NetworkPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Domain + +public struct NetworkRepository: Repository { + + private let network: AppNetworking + + private init(network: AppNetworking) { + self.network = network + } + + public func prepare() { + fatalError("Unimplemented") + } + + public func create(input: Int) async throws -> Product? { + fatalError("Unimplemented") + } + + public func read(input: Int) async throws -> Product? { + try await network.requestObject( + .product(id: input), + type: Product.self) + } + + public func update(input: Int) async throws -> Product? { + fatalError("Unimplemented") + } + + public func delete(input: Int) async throws -> Product? { + fatalError("Unimplemented") + } +} + +extension NetworkRepository { + public static var live = NetworkRepository( + network: AppNetworking.defaultNetworking()) + public static var stubbed = NetworkRepository( + network: AppNetworking.stubbingNetworking()) +} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/RepositoryProvider.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/RepositoryProvider.swift deleted file mode 100644 index 4e14d33..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/RepositoryProvider.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// UseCaseProvider.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Domain -import Foundation - -public struct RepositoryProvider: Domain.RepositoryProvider { - private let networkProvider: NetworkProvider - - public init() { - networkProvider = NetworkProvider() - } - - public func makeTrendingRepoRepository() -> Domain.TrendingRepoRepository { - return TrendingRepoRepository(network: networkProvider.makeTrendingGithubNetworking()) - } - - public func makeTrendingDeveloperRepository() -> Domain.TrendingDeveloperRepository { - return TrendingDeveloperRepository(network: networkProvider.makeTrendingGithubNetworking()) - } - - public func makeTrendingRepoRepositoryStubbed() -> Domain.TrendingRepoRepository { - return TrendingRepoRepository(network: networkProvider.makeTrendingGithubNetworkingStubbed()) - } - - public func makeTrendingDeveloperRepositoryStubbed() -> Domain.TrendingDeveloperRepository { - return TrendingDeveloperRepository(network: networkProvider.makeTrendingGithubNetworkingStubbed()) - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingDeveloperRepository.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingDeveloperRepository.swift deleted file mode 100644 index a8c077a..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingDeveloperRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// TrendingDeveloperRepository.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Domain - -struct TrendingDeveloperRepository: Domain.TrendingDeveloperRepository { - private let network: TrendingGithubNetworking - - init(network: TrendingGithubNetworking) { - self.network = network - } - - func trendingDeveloper(language: String, since: String) async throws -> [TrendingUser] { - return try await network.trendingDevelopers(language: language, since: since) - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingRepoRepository.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingRepoRepository.swift deleted file mode 100644 index 805bd80..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Repository/TrendingRepoRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// TrendingGithubUseCase.swift -// NetworkPlatform -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Combine -import Domain - -struct TrendingRepoRepository: Domain.TrendingRepoRepository { - private let network: TrendingGithubNetworking - - init(network: TrendingGithubNetworking) { - self.network = network - } - - func trendingRepositories(language: String, since: String) async throws -> [TrendingRepository] { - return try await network.trendingRepositories(language: language, since: since) - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Resources/StubbedResponse/Product.json b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Resources/StubbedResponse/Product.json new file mode 100644 index 0000000..aa98814 --- /dev/null +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Resources/StubbedResponse/Product.json @@ -0,0 +1,12 @@ +{ + "id": 1, + "title": "Rokon", + "price": 109.95, + "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", + "category": "men's clothing", + "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg", + "rating": { + "rate": 3.9, + "count": 120 + } +} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/Languages.json b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/Languages.json deleted file mode 100644 index 8af7e4e..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/Languages.json +++ /dev/null @@ -1,1998 +0,0 @@ -{ - "popular": [ - { - "urlParam": "c++", - "name": "C++" - }, - { - "urlParam": "html", - "name": "HTML" - }, - { - "urlParam": "java", - "name": "Java" - }, - { - "urlParam": "javascript", - "name": "JavaScript" - }, - { - "urlParam": "php", - "name": "PHP" - }, - { - "urlParam": "python", - "name": "Python" - }, - { - "urlParam": "ruby", - "name": "Ruby" - } - ], - "all": [ - { - "urlParam": "1c-enterprise", - "name": "1C Enterprise" - }, - { - "urlParam": "abap", - "name": "ABAP" - }, - { - "urlParam": "abnf", - "name": "ABNF" - }, - { - "urlParam": "actionscript", - "name": "ActionScript" - }, - { - "urlParam": "ada", - "name": "Ada" - }, - { - "urlParam": "adobe-font-metrics", - "name": "Adobe Font Metrics" - }, - { - "urlParam": "agda", - "name": "Agda" - }, - { - "urlParam": "ags-script", - "name": "AGS Script" - }, - { - "urlParam": "alloy", - "name": "Alloy" - }, - { - "urlParam": "alpine-abuild", - "name": "Alpine Abuild" - }, - { - "urlParam": "ampl", - "name": "AMPL" - }, - { - "urlParam": "angelscript", - "name": "AngelScript" - }, - { - "urlParam": "ant-build-system", - "name": "Ant Build System" - }, - { - "urlParam": "antlr", - "name": "ANTLR" - }, - { - "urlParam": "apacheconf", - "name": "ApacheConf" - }, - { - "urlParam": "apex", - "name": "Apex" - }, - { - "urlParam": "api-blueprint", - "name": "API Blueprint" - }, - { - "urlParam": "apl", - "name": "APL" - }, - { - "urlParam": "apollo-guidance-computer", - "name": "Apollo Guidance Computer" - }, - { - "urlParam": "applescript", - "name": "AppleScript" - }, - { - "urlParam": "arc", - "name": "Arc" - }, - { - "urlParam": "asciidoc", - "name": "AsciiDoc" - }, - { - "urlParam": "asn.1", - "name": "ASN.1" - }, - { - "urlParam": "asp", - "name": "ASP" - }, - { - "urlParam": "aspectj", - "name": "AspectJ" - }, - { - "urlParam": "assembly", - "name": "Assembly" - }, - { - "urlParam": "ats", - "name": "ATS" - }, - { - "urlParam": "augeas", - "name": "Augeas" - }, - { - "urlParam": "autohotkey", - "name": "AutoHotkey" - }, - { - "urlParam": "autoit", - "name": "AutoIt" - }, - { - "urlParam": "awk", - "name": "Awk" - }, - { - "urlParam": "ballerina", - "name": "Ballerina" - }, - { - "urlParam": "batchfile", - "name": "Batchfile" - }, - { - "urlParam": "befunge", - "name": "Befunge" - }, - { - "urlParam": "bison", - "name": "Bison" - }, - { - "urlParam": "bitbake", - "name": "BitBake" - }, - { - "urlParam": "blade", - "name": "Blade" - }, - { - "urlParam": "blitzbasic", - "name": "BlitzBasic" - }, - { - "urlParam": "blitzmax", - "name": "BlitzMax" - }, - { - "urlParam": "bluespec", - "name": "Bluespec" - }, - { - "urlParam": "boo", - "name": "Boo" - }, - { - "urlParam": "brainfuck", - "name": "Brainfuck" - }, - { - "urlParam": "brightscript", - "name": "Brightscript" - }, - { - "urlParam": "bro", - "name": "Bro" - }, - { - "urlParam": "c", - "name": "C" - }, - { - "urlParam": "c%23", - "name": "C#" - }, - { - "urlParam": "c++", - "name": "C++" - }, - { - "urlParam": "c-objdump", - "name": "C-ObjDump" - }, - { - "urlParam": "c2hs-haskell", - "name": "C2hs Haskell" - }, - { - "urlParam": "cap'n-proto", - "name": "Cap'n Proto" - }, - { - "urlParam": "cartocss", - "name": "CartoCSS" - }, - { - "urlParam": "ceylon", - "name": "Ceylon" - }, - { - "urlParam": "chapel", - "name": "Chapel" - }, - { - "urlParam": "charity", - "name": "Charity" - }, - { - "urlParam": "chuck", - "name": "ChucK" - }, - { - "urlParam": "cirru", - "name": "Cirru" - }, - { - "urlParam": "clarion", - "name": "Clarion" - }, - { - "urlParam": "clean", - "name": "Clean" - }, - { - "urlParam": "click", - "name": "Click" - }, - { - "urlParam": "clips", - "name": "CLIPS" - }, - { - "urlParam": "clojure", - "name": "Clojure" - }, - { - "urlParam": "closure-templates", - "name": "Closure Templates" - }, - { - "urlParam": "cloud-firestore-security-rules", - "name": "Cloud Firestore Security Rules" - }, - { - "urlParam": "cmake", - "name": "CMake" - }, - { - "urlParam": "cobol", - "name": "COBOL" - }, - { - "urlParam": "coffeescript", - "name": "CoffeeScript" - }, - { - "urlParam": "coldfusion", - "name": "ColdFusion" - }, - { - "urlParam": "coldfusion-cfc", - "name": "ColdFusion CFC" - }, - { - "urlParam": "collada", - "name": "COLLADA" - }, - { - "urlParam": "common-lisp", - "name": "Common Lisp" - }, - { - "urlParam": "common-workflow-language", - "name": "Common Workflow Language" - }, - { - "urlParam": "component-pascal", - "name": "Component Pascal" - }, - { - "urlParam": "conll-u", - "name": "CoNLL-U" - }, - { - "urlParam": "cool", - "name": "Cool" - }, - { - "urlParam": "coq", - "name": "Coq" - }, - { - "urlParam": "cpp-objdump", - "name": "Cpp-ObjDump" - }, - { - "urlParam": "creole", - "name": "Creole" - }, - { - "urlParam": "crystal", - "name": "Crystal" - }, - { - "urlParam": "cson", - "name": "CSON" - }, - { - "urlParam": "csound", - "name": "Csound" - }, - { - "urlParam": "csound-document", - "name": "Csound Document" - }, - { - "urlParam": "csound-score", - "name": "Csound Score" - }, - { - "urlParam": "css", - "name": "CSS" - }, - { - "urlParam": "csv", - "name": "CSV" - }, - { - "urlParam": "cuda", - "name": "Cuda" - }, - { - "urlParam": "cweb", - "name": "CWeb" - }, - { - "urlParam": "cycript", - "name": "Cycript" - }, - { - "urlParam": "cython", - "name": "Cython" - }, - { - "urlParam": "d", - "name": "D" - }, - { - "urlParam": "d-objdump", - "name": "D-ObjDump" - }, - { - "urlParam": "darcs-patch", - "name": "Darcs Patch" - }, - { - "urlParam": "dart", - "name": "Dart" - }, - { - "urlParam": "dataweave", - "name": "DataWeave" - }, - { - "urlParam": "desktop", - "name": "desktop" - }, - { - "urlParam": "diff", - "name": "Diff" - }, - { - "urlParam": "digital-command-language", - "name": "DIGITAL Command Language" - }, - { - "urlParam": "dm", - "name": "DM" - }, - { - "urlParam": "dns-zone", - "name": "DNS Zone" - }, - { - "urlParam": "dockerfile", - "name": "Dockerfile" - }, - { - "urlParam": "dogescript", - "name": "Dogescript" - }, - { - "urlParam": "dtrace", - "name": "DTrace" - }, - { - "urlParam": "dylan", - "name": "Dylan" - }, - { - "urlParam": "e", - "name": "E" - }, - { - "urlParam": "eagle", - "name": "Eagle" - }, - { - "urlParam": "easybuild", - "name": "Easybuild" - }, - { - "urlParam": "ebnf", - "name": "EBNF" - }, - { - "urlParam": "ec", - "name": "eC" - }, - { - "urlParam": "ecere-projects", - "name": "Ecere Projects" - }, - { - "urlParam": "ecl", - "name": "ECL" - }, - { - "urlParam": "eclipse", - "name": "ECLiPSe" - }, - { - "urlParam": "edje-data-collection", - "name": "Edje Data Collection" - }, - { - "urlParam": "edn", - "name": "edn" - }, - { - "urlParam": "eiffel", - "name": "Eiffel" - }, - { - "urlParam": "ejs", - "name": "EJS" - }, - { - "urlParam": "elixir", - "name": "Elixir" - }, - { - "urlParam": "elm", - "name": "Elm" - }, - { - "urlParam": "emacs-lisp", - "name": "Emacs Lisp" - }, - { - "urlParam": "emberscript", - "name": "EmberScript" - }, - { - "urlParam": "eml", - "name": "EML" - }, - { - "urlParam": "eq", - "name": "EQ" - }, - { - "urlParam": "erlang", - "name": "Erlang" - }, - { - "urlParam": "f%23", - "name": "F#" - }, - { - "urlParam": "f*", - "name": "F*" - }, - { - "urlParam": "factor", - "name": "Factor" - }, - { - "urlParam": "fancy", - "name": "Fancy" - }, - { - "urlParam": "fantom", - "name": "Fantom" - }, - { - "urlParam": "figlet-font", - "name": "FIGlet Font" - }, - { - "urlParam": "filebench-wml", - "name": "Filebench WML" - }, - { - "urlParam": "filterscript", - "name": "Filterscript" - }, - { - "urlParam": "fish", - "name": "fish" - }, - { - "urlParam": "flux", - "name": "FLUX" - }, - { - "urlParam": "formatted", - "name": "Formatted" - }, - { - "urlParam": "forth", - "name": "Forth" - }, - { - "urlParam": "fortran", - "name": "Fortran" - }, - { - "urlParam": "freemarker", - "name": "FreeMarker" - }, - { - "urlParam": "frege", - "name": "Frege" - }, - { - "urlParam": "g-code", - "name": "G-code" - }, - { - "urlParam": "game-maker-language", - "name": "Game Maker Language" - }, - { - "urlParam": "gams", - "name": "GAMS" - }, - { - "urlParam": "gap", - "name": "GAP" - }, - { - "urlParam": "gcc-machine-description", - "name": "GCC Machine Description" - }, - { - "urlParam": "gdb", - "name": "GDB" - }, - { - "urlParam": "gdscript", - "name": "GDScript" - }, - { - "urlParam": "genie", - "name": "Genie" - }, - { - "urlParam": "genshi", - "name": "Genshi" - }, - { - "urlParam": "gentoo-ebuild", - "name": "Gentoo Ebuild" - }, - { - "urlParam": "gentoo-eclass", - "name": "Gentoo Eclass" - }, - { - "urlParam": "gerber-image", - "name": "Gerber Image" - }, - { - "urlParam": "gettext-catalog", - "name": "Gettext Catalog" - }, - { - "urlParam": "gherkin", - "name": "Gherkin" - }, - { - "urlParam": "glsl", - "name": "GLSL" - }, - { - "urlParam": "glyph", - "name": "Glyph" - }, - { - "urlParam": "glyph-bitmap-distribution-format", - "name": "Glyph Bitmap Distribution Format" - }, - { - "urlParam": "gn", - "name": "GN" - }, - { - "urlParam": "gnuplot", - "name": "Gnuplot" - }, - { - "urlParam": "go", - "name": "Go" - }, - { - "urlParam": "golo", - "name": "Golo" - }, - { - "urlParam": "gosu", - "name": "Gosu" - }, - { - "urlParam": "grace", - "name": "Grace" - }, - { - "urlParam": "gradle", - "name": "Gradle" - }, - { - "urlParam": "grammatical-framework", - "name": "Grammatical Framework" - }, - { - "urlParam": "graph-modeling-language", - "name": "Graph Modeling Language" - }, - { - "urlParam": "graphql", - "name": "GraphQL" - }, - { - "urlParam": "graphviz-(dot)", - "name": "Graphviz (DOT)" - }, - { - "urlParam": "groovy", - "name": "Groovy" - }, - { - "urlParam": "groovy-server-pages", - "name": "Groovy Server Pages" - }, - { - "urlParam": "hack", - "name": "Hack" - }, - { - "urlParam": "haml", - "name": "Haml" - }, - { - "urlParam": "handlebars", - "name": "Handlebars" - }, - { - "urlParam": "haproxy", - "name": "HAProxy" - }, - { - "urlParam": "harbour", - "name": "Harbour" - }, - { - "urlParam": "haskell", - "name": "Haskell" - }, - { - "urlParam": "haxe", - "name": "Haxe" - }, - { - "urlParam": "hcl", - "name": "HCL" - }, - { - "urlParam": "hiveql", - "name": "HiveQL" - }, - { - "urlParam": "hlsl", - "name": "HLSL" - }, - { - "urlParam": "html", - "name": "HTML" - }, - { - "urlParam": "html+django", - "name": "HTML+Django" - }, - { - "urlParam": "html+ecr", - "name": "HTML+ECR" - }, - { - "urlParam": "html+eex", - "name": "HTML+EEX" - }, - { - "urlParam": "html+erb", - "name": "HTML+ERB" - }, - { - "urlParam": "html+php", - "name": "HTML+PHP" - }, - { - "urlParam": "html+razor", - "name": "HTML+Razor" - }, - { - "urlParam": "http", - "name": "HTTP" - }, - { - "urlParam": "hxml", - "name": "HXML" - }, - { - "urlParam": "hy", - "name": "Hy" - }, - { - "urlParam": "hyphy", - "name": "HyPhy" - }, - { - "urlParam": "idl", - "name": "IDL" - }, - { - "urlParam": "idris", - "name": "Idris" - }, - { - "urlParam": "igor-pro", - "name": "IGOR Pro" - }, - { - "urlParam": "inform-7", - "name": "Inform 7" - }, - { - "urlParam": "ini", - "name": "INI" - }, - { - "urlParam": "inno-setup", - "name": "Inno Setup" - }, - { - "urlParam": "io", - "name": "Io" - }, - { - "urlParam": "ioke", - "name": "Ioke" - }, - { - "urlParam": "irc-log", - "name": "IRC log" - }, - { - "urlParam": "isabelle", - "name": "Isabelle" - }, - { - "urlParam": "isabelle-root", - "name": "Isabelle ROOT" - }, - { - "urlParam": "j", - "name": "J" - }, - { - "urlParam": "jasmin", - "name": "Jasmin" - }, - { - "urlParam": "java", - "name": "Java" - }, - { - "urlParam": "java-properties", - "name": "Java Properties" - }, - { - "urlParam": "java-server-pages", - "name": "Java Server Pages" - }, - { - "urlParam": "javascript", - "name": "JavaScript" - }, - { - "urlParam": "jflex", - "name": "JFlex" - }, - { - "urlParam": "jison", - "name": "Jison" - }, - { - "urlParam": "jison-lex", - "name": "Jison Lex" - }, - { - "urlParam": "jolie", - "name": "Jolie" - }, - { - "urlParam": "json", - "name": "JSON" - }, - { - "urlParam": "json-with-comments", - "name": "JSON with Comments" - }, - { - "urlParam": "json5", - "name": "JSON5" - }, - { - "urlParam": "jsoniq", - "name": "JSONiq" - }, - { - "urlParam": "jsonld", - "name": "JSONLD" - }, - { - "urlParam": "jsx", - "name": "JSX" - }, - { - "urlParam": "julia", - "name": "Julia" - }, - { - "urlParam": "jupyter-notebook", - "name": "Jupyter Notebook" - }, - { - "urlParam": "kicad-layout", - "name": "KiCad Layout" - }, - { - "urlParam": "kicad-legacy-layout", - "name": "KiCad Legacy Layout" - }, - { - "urlParam": "kicad-schematic", - "name": "KiCad Schematic" - }, - { - "urlParam": "kit", - "name": "Kit" - }, - { - "urlParam": "kotlin", - "name": "Kotlin" - }, - { - "urlParam": "krl", - "name": "KRL" - }, - { - "urlParam": "labview", - "name": "LabVIEW" - }, - { - "urlParam": "lasso", - "name": "Lasso" - }, - { - "urlParam": "latte", - "name": "Latte" - }, - { - "urlParam": "lean", - "name": "Lean" - }, - { - "urlParam": "less", - "name": "Less" - }, - { - "urlParam": "lex", - "name": "Lex" - }, - { - "urlParam": "lfe", - "name": "LFE" - }, - { - "urlParam": "lilypond", - "name": "LilyPond" - }, - { - "urlParam": "limbo", - "name": "Limbo" - }, - { - "urlParam": "linker-script", - "name": "Linker Script" - }, - { - "urlParam": "linux-kernel-module", - "name": "Linux Kernel Module" - }, - { - "urlParam": "liquid", - "name": "Liquid" - }, - { - "urlParam": "literate-agda", - "name": "Literate Agda" - }, - { - "urlParam": "literate-coffeescript", - "name": "Literate CoffeeScript" - }, - { - "urlParam": "literate-haskell", - "name": "Literate Haskell" - }, - { - "urlParam": "livescript", - "name": "LiveScript" - }, - { - "urlParam": "llvm", - "name": "LLVM" - }, - { - "urlParam": "logos", - "name": "Logos" - }, - { - "urlParam": "logtalk", - "name": "Logtalk" - }, - { - "urlParam": "lolcode", - "name": "LOLCODE" - }, - { - "urlParam": "lookml", - "name": "LookML" - }, - { - "urlParam": "loomscript", - "name": "LoomScript" - }, - { - "urlParam": "lsl", - "name": "LSL" - }, - { - "urlParam": "lua", - "name": "Lua" - }, - { - "urlParam": "m", - "name": "M" - }, - { - "urlParam": "m4", - "name": "M4" - }, - { - "urlParam": "m4sugar", - "name": "M4Sugar" - }, - { - "urlParam": "makefile", - "name": "Makefile" - }, - { - "urlParam": "mako", - "name": "Mako" - }, - { - "urlParam": "markdown", - "name": "Markdown" - }, - { - "urlParam": "marko", - "name": "Marko" - }, - { - "urlParam": "mask", - "name": "Mask" - }, - { - "urlParam": "mathematica", - "name": "Mathematica" - }, - { - "urlParam": "matlab", - "name": "MATLAB" - }, - { - "urlParam": "maven-pom", - "name": "Maven POM" - }, - { - "urlParam": "max", - "name": "Max" - }, - { - "urlParam": "maxscript", - "name": "MAXScript" - }, - { - "urlParam": "mediawiki", - "name": "MediaWiki" - }, - { - "urlParam": "mercury", - "name": "Mercury" - }, - { - "urlParam": "meson", - "name": "Meson" - }, - { - "urlParam": "metal", - "name": "Metal" - }, - { - "urlParam": "minid", - "name": "MiniD" - }, - { - "urlParam": "mirah", - "name": "Mirah" - }, - { - "urlParam": "modelica", - "name": "Modelica" - }, - { - "urlParam": "modula-2", - "name": "Modula-2" - }, - { - "urlParam": "modula-3", - "name": "Modula-3" - }, - { - "urlParam": "module-management-system", - "name": "Module Management System" - }, - { - "urlParam": "monkey", - "name": "Monkey" - }, - { - "urlParam": "moocode", - "name": "Moocode" - }, - { - "urlParam": "moonscript", - "name": "MoonScript" - }, - { - "urlParam": "mql4", - "name": "MQL4" - }, - { - "urlParam": "mql5", - "name": "MQL5" - }, - { - "urlParam": "mtml", - "name": "MTML" - }, - { - "urlParam": "muf", - "name": "MUF" - }, - { - "urlParam": "mupad", - "name": "mupad" - }, - { - "urlParam": "myghty", - "name": "Myghty" - }, - { - "urlParam": "ncl", - "name": "NCL" - }, - { - "urlParam": "nearley", - "name": "Nearley" - }, - { - "urlParam": "nemerle", - "name": "Nemerle" - }, - { - "urlParam": "nesc", - "name": "nesC" - }, - { - "urlParam": "netlinx", - "name": "NetLinx" - }, - { - "urlParam": "netlinx+erb", - "name": "NetLinx+ERB" - }, - { - "urlParam": "netlogo", - "name": "NetLogo" - }, - { - "urlParam": "newlisp", - "name": "NewLisp" - }, - { - "urlParam": "nextflow", - "name": "Nextflow" - }, - { - "urlParam": "nginx", - "name": "Nginx" - }, - { - "urlParam": "nim", - "name": "Nim" - }, - { - "urlParam": "ninja", - "name": "Ninja" - }, - { - "urlParam": "nit", - "name": "Nit" - }, - { - "urlParam": "nix", - "name": "Nix" - }, - { - "urlParam": "nl", - "name": "NL" - }, - { - "urlParam": "nsis", - "name": "NSIS" - }, - { - "urlParam": "nu", - "name": "Nu" - }, - { - "urlParam": "numpy", - "name": "NumPy" - }, - { - "urlParam": "objdump", - "name": "ObjDump" - }, - { - "urlParam": "objective-c", - "name": "Objective-C" - }, - { - "urlParam": "objective-c++", - "name": "Objective-C++" - }, - { - "urlParam": "objective-j", - "name": "Objective-J" - }, - { - "urlParam": "ocaml", - "name": "OCaml" - }, - { - "urlParam": "omgrofl", - "name": "Omgrofl" - }, - { - "urlParam": "ooc", - "name": "ooc" - }, - { - "urlParam": "opa", - "name": "Opa" - }, - { - "urlParam": "opal", - "name": "Opal" - }, - { - "urlParam": "opencl", - "name": "OpenCL" - }, - { - "urlParam": "openedge-abl", - "name": "OpenEdge ABL" - }, - { - "urlParam": "openrc-runscript", - "name": "OpenRC runscript" - }, - { - "urlParam": "openscad", - "name": "OpenSCAD" - }, - { - "urlParam": "opentype-feature-file", - "name": "OpenType Feature File" - }, - { - "urlParam": "org", - "name": "Org" - }, - { - "urlParam": "ox", - "name": "Ox" - }, - { - "urlParam": "oxygene", - "name": "Oxygene" - }, - { - "urlParam": "oz", - "name": "Oz" - }, - { - "urlParam": "p4", - "name": "P4" - }, - { - "urlParam": "pan", - "name": "Pan" - }, - { - "urlParam": "papyrus", - "name": "Papyrus" - }, - { - "urlParam": "parrot", - "name": "Parrot" - }, - { - "urlParam": "parrot-assembly", - "name": "Parrot Assembly" - }, - { - "urlParam": "parrot-internal-representation", - "name": "Parrot Internal Representation" - }, - { - "urlParam": "pascal", - "name": "Pascal" - }, - { - "urlParam": "pawn", - "name": "Pawn" - }, - { - "urlParam": "pep8", - "name": "Pep8" - }, - { - "urlParam": "perl", - "name": "Perl" - }, - { - "urlParam": "perl-6", - "name": "Perl 6" - }, - { - "urlParam": "php", - "name": "PHP" - }, - { - "urlParam": "pic", - "name": "Pic" - }, - { - "urlParam": "pickle", - "name": "Pickle" - }, - { - "urlParam": "picolisp", - "name": "PicoLisp" - }, - { - "urlParam": "piglatin", - "name": "PigLatin" - }, - { - "urlParam": "pike", - "name": "Pike" - }, - { - "urlParam": "plpgsql", - "name": "PLpgSQL" - }, - { - "urlParam": "plsql", - "name": "PLSQL" - }, - { - "urlParam": "pod", - "name": "Pod" - }, - { - "urlParam": "pod-6", - "name": "Pod 6" - }, - { - "urlParam": "pogoscript", - "name": "PogoScript" - }, - { - "urlParam": "pony", - "name": "Pony" - }, - { - "urlParam": "postcss", - "name": "PostCSS" - }, - { - "urlParam": "postscript", - "name": "PostScript" - }, - { - "urlParam": "pov-ray-sdl", - "name": "POV-Ray SDL" - }, - { - "urlParam": "powerbuilder", - "name": "PowerBuilder" - }, - { - "urlParam": "powershell", - "name": "PowerShell" - }, - { - "urlParam": "processing", - "name": "Processing" - }, - { - "urlParam": "prolog", - "name": "Prolog" - }, - { - "urlParam": "propeller-spin", - "name": "Propeller Spin" - }, - { - "urlParam": "protocol-buffer", - "name": "Protocol Buffer" - }, - { - "urlParam": "public-key", - "name": "Public Key" - }, - { - "urlParam": "pug", - "name": "Pug" - }, - { - "urlParam": "puppet", - "name": "Puppet" - }, - { - "urlParam": "pure-data", - "name": "Pure Data" - }, - { - "urlParam": "purebasic", - "name": "PureBasic" - }, - { - "urlParam": "purescript", - "name": "PureScript" - }, - { - "urlParam": "python", - "name": "Python" - }, - { - "urlParam": "python-console", - "name": "Python console" - }, - { - "urlParam": "python-traceback", - "name": "Python traceback" - }, - { - "urlParam": "q", - "name": "q" - }, - { - "urlParam": "qmake", - "name": "QMake" - }, - { - "urlParam": "qml", - "name": "QML" - }, - { - "urlParam": "quake", - "name": "Quake" - }, - { - "urlParam": "r", - "name": "R" - }, - { - "urlParam": "racket", - "name": "Racket" - }, - { - "urlParam": "ragel", - "name": "Ragel" - }, - { - "urlParam": "raml", - "name": "RAML" - }, - { - "urlParam": "rascal", - "name": "Rascal" - }, - { - "urlParam": "raw-token-data", - "name": "Raw token data" - }, - { - "urlParam": "rdoc", - "name": "RDoc" - }, - { - "urlParam": "realbasic", - "name": "REALbasic" - }, - { - "urlParam": "reason", - "name": "Reason" - }, - { - "urlParam": "rebol", - "name": "Rebol" - }, - { - "urlParam": "red", - "name": "Red" - }, - { - "urlParam": "redcode", - "name": "Redcode" - }, - { - "urlParam": "regular-expression", - "name": "Regular Expression" - }, - { - "urlParam": "ren'py", - "name": "Ren'Py" - }, - { - "urlParam": "renderscript", - "name": "RenderScript" - }, - { - "urlParam": "restructuredtext", - "name": "reStructuredText" - }, - { - "urlParam": "rexx", - "name": "REXX" - }, - { - "urlParam": "rhtml", - "name": "RHTML" - }, - { - "urlParam": "ring", - "name": "Ring" - }, - { - "urlParam": "rmarkdown", - "name": "RMarkdown" - }, - { - "urlParam": "robotframework", - "name": "RobotFramework" - }, - { - "urlParam": "roff", - "name": "Roff" - }, - { - "urlParam": "rouge", - "name": "Rouge" - }, - { - "urlParam": "rpc", - "name": "RPC" - }, - { - "urlParam": "rpm-spec", - "name": "RPM Spec" - }, - { - "urlParam": "ruby", - "name": "Ruby" - }, - { - "urlParam": "runoff", - "name": "RUNOFF" - }, - { - "urlParam": "rust", - "name": "Rust" - }, - { - "urlParam": "sage", - "name": "Sage" - }, - { - "urlParam": "saltstack", - "name": "SaltStack" - }, - { - "urlParam": "sas", - "name": "SAS" - }, - { - "urlParam": "sass", - "name": "Sass" - }, - { - "urlParam": "scala", - "name": "Scala" - }, - { - "urlParam": "scaml", - "name": "Scaml" - }, - { - "urlParam": "scheme", - "name": "Scheme" - }, - { - "urlParam": "scilab", - "name": "Scilab" - }, - { - "urlParam": "scss", - "name": "SCSS" - }, - { - "urlParam": "sed", - "name": "sed" - }, - { - "urlParam": "self", - "name": "Self" - }, - { - "urlParam": "shaderlab", - "name": "ShaderLab" - }, - { - "urlParam": "shell", - "name": "Shell" - }, - { - "urlParam": "shellsession", - "name": "ShellSession" - }, - { - "urlParam": "shen", - "name": "Shen" - }, - { - "urlParam": "slash", - "name": "Slash" - }, - { - "urlParam": "slice", - "name": "Slice" - }, - { - "urlParam": "slim", - "name": "Slim" - }, - { - "urlParam": "smali", - "name": "Smali" - }, - { - "urlParam": "smalltalk", - "name": "Smalltalk" - }, - { - "urlParam": "smarty", - "name": "Smarty" - }, - { - "urlParam": "smt", - "name": "SMT" - }, - { - "urlParam": "solidity", - "name": "Solidity" - }, - { - "urlParam": "sourcepawn", - "name": "SourcePawn" - }, - { - "urlParam": "sparql", - "name": "SPARQL" - }, - { - "urlParam": "spline-font-database", - "name": "Spline Font Database" - }, - { - "urlParam": "sqf", - "name": "SQF" - }, - { - "urlParam": "sql", - "name": "SQL" - }, - { - "urlParam": "sqlpl", - "name": "SQLPL" - }, - { - "urlParam": "squirrel", - "name": "Squirrel" - }, - { - "urlParam": "srecode-template", - "name": "SRecode Template" - }, - { - "urlParam": "stan", - "name": "Stan" - }, - { - "urlParam": "standard-ml", - "name": "Standard ML" - }, - { - "urlParam": "stata", - "name": "Stata" - }, - { - "urlParam": "ston", - "name": "STON" - }, - { - "urlParam": "stylus", - "name": "Stylus" - }, - { - "urlParam": "subrip-text", - "name": "SubRip Text" - }, - { - "urlParam": "sugarss", - "name": "SugarSS" - }, - { - "urlParam": "supercollider", - "name": "SuperCollider" - }, - { - "urlParam": "svg", - "name": "SVG" - }, - { - "urlParam": "swift", - "name": "Swift" - }, - { - "urlParam": "systemverilog", - "name": "SystemVerilog" - }, - { - "urlParam": "tcl", - "name": "Tcl" - }, - { - "urlParam": "tcsh", - "name": "Tcsh" - }, - { - "urlParam": "tea", - "name": "Tea" - }, - { - "urlParam": "terra", - "name": "Terra" - }, - { - "urlParam": "tex", - "name": "TeX" - }, - { - "urlParam": "text", - "name": "Text" - }, - { - "urlParam": "textile", - "name": "Textile" - }, - { - "urlParam": "thrift", - "name": "Thrift" - }, - { - "urlParam": "ti-program", - "name": "TI Program" - }, - { - "urlParam": "tla", - "name": "TLA" - }, - { - "urlParam": "toml", - "name": "TOML" - }, - { - "urlParam": "turing", - "name": "Turing" - }, - { - "urlParam": "turtle", - "name": "Turtle" - }, - { - "urlParam": "twig", - "name": "Twig" - }, - { - "urlParam": "txl", - "name": "TXL" - }, - { - "urlParam": "type-language", - "name": "Type Language" - }, - { - "urlParam": "typescript", - "name": "TypeScript" - }, - { - "urlParam": "unified-parallel-c", - "name": "Unified Parallel C" - }, - { - "urlParam": "unity3d-asset", - "name": "Unity3D Asset" - }, - { - "urlParam": "unix-assembly", - "name": "Unix Assembly" - }, - { - "urlParam": "uno", - "name": "Uno" - }, - { - "urlParam": "unrealscript", - "name": "UnrealScript" - }, - { - "urlParam": "urweb", - "name": "UrWeb" - }, - { - "urlParam": "vala", - "name": "Vala" - }, - { - "urlParam": "vcl", - "name": "VCL" - }, - { - "urlParam": "verilog", - "name": "Verilog" - }, - { - "urlParam": "vhdl", - "name": "VHDL" - }, - { - "urlParam": "vim-script", - "name": "Vim script" - }, - { - "urlParam": "visual-basic", - "name": "Visual Basic" - }, - { - "urlParam": "volt", - "name": "Volt" - }, - { - "urlParam": "vue", - "name": "Vue" - }, - { - "urlParam": "wavefront-material", - "name": "Wavefront Material" - }, - { - "urlParam": "wavefront-object", - "name": "Wavefront Object" - }, - { - "urlParam": "wdl", - "name": "wdl" - }, - { - "urlParam": "web-ontology-language", - "name": "Web Ontology Language" - }, - { - "urlParam": "webassembly", - "name": "WebAssembly" - }, - { - "urlParam": "webidl", - "name": "WebIDL" - }, - { - "urlParam": "windows-registry-entries", - "name": "Windows Registry Entries" - }, - { - "urlParam": "wisp", - "name": "wisp" - }, - { - "urlParam": "world-of-warcraft-addon-data", - "name": "World of Warcraft Addon Data" - }, - { - "urlParam": "x-bitmap", - "name": "X BitMap" - }, - { - "urlParam": "x-font-directory-index", - "name": "X Font Directory Index" - }, - { - "urlParam": "x-pixmap", - "name": "X PixMap" - }, - { - "urlParam": "x10", - "name": "X10" - }, - { - "urlParam": "xbase", - "name": "xBase" - }, - { - "urlParam": "xc", - "name": "XC" - }, - { - "urlParam": "xcompose", - "name": "XCompose" - }, - { - "urlParam": "xml", - "name": "XML" - }, - { - "urlParam": "xojo", - "name": "Xojo" - }, - { - "urlParam": "xpages", - "name": "XPages" - }, - { - "urlParam": "xproc", - "name": "XProc" - }, - { - "urlParam": "xquery", - "name": "XQuery" - }, - { - "urlParam": "xs", - "name": "XS" - }, - { - "urlParam": "xslt", - "name": "XSLT" - }, - { - "urlParam": "xtend", - "name": "Xtend" - }, - { - "urlParam": "yacc", - "name": "Yacc" - }, - { - "urlParam": "yaml", - "name": "YAML" - }, - { - "urlParam": "yang", - "name": "YANG" - }, - { - "urlParam": "yara", - "name": "YARA" - }, - { - "urlParam": "yasnippet", - "name": "YASnippet" - }, - { - "urlParam": "zephir", - "name": "Zephir" - }, - { - "urlParam": "zimpl", - "name": "Zimpl" - } - ] -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/RepositoryTrendings.json b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/RepositoryTrendings.json deleted file mode 100644 index 85445ea..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/RepositoryTrendings.json +++ /dev/null @@ -1,952 +0,0 @@ -[ - { - "author":"airbnb", - "name":"lottie-ios", - "avatar":"https://github.com/airbnb.png", - "description":"An iOS library to natively render After Effects vector animations", - "url":"https://github.com/airbnb/lottie-ios", - "language":"Swift", - "languageColor":"#F05138", - "stars":22502, - "forks":3308, - "currentPeriodStars":8, - "builtBy":[ - { - "username":"buba447", - "href":"https://github.com/buba447", - "avatar":"https://avatars.githubusercontent.com/u/1163980" - }, - { - "username":"calda", - "href":"https://github.com/calda", - "avatar":"https://avatars.githubusercontent.com/u/1811727" - }, - { - "username":"thedrick", - "href":"https://github.com/thedrick", - "avatar":"https://avatars.githubusercontent.com/u/796488" - }, - { - "username":"Coeur", - "href":"https://github.com/Coeur", - "avatar":"https://avatars.githubusercontent.com/u/839992" - }, - { - "username":"StatusQuo", - "href":"https://github.com/StatusQuo", - "avatar":"https://avatars.githubusercontent.com/u/735281" - } - ] - }, - { - "author":"onevcat", - "name":"Kingfisher", - "avatar":"https://github.com/onevcat.png", - "description":"A lightweight, pure-Swift library for downloading and caching images from the web.", - "url":"https://github.com/onevcat/Kingfisher", - "language":"Swift", - "languageColor":"#F05138", - "stars":19755, - "forks":2260, - "currentPeriodStars":4, - "builtBy":[ - { - "username":"onevcat", - "href":"https://github.com/onevcat", - "avatar":"https://avatars.githubusercontent.com/u/1019875" - }, - { - "username":"lixiang1994", - "href":"https://github.com/lixiang1994", - "avatar":"https://avatars.githubusercontent.com/u/13112992" - }, - { - "username":"dstranz", - "href":"https://github.com/dstranz", - "avatar":"https://avatars.githubusercontent.com/u/96502" - }, - { - "username":"xspyhack", - "href":"https://github.com/xspyhack", - "avatar":"https://avatars.githubusercontent.com/u/5518285" - }, - { - "username":"Jack-Ving", - "href":"https://github.com/Jack-Ving", - "avatar":"https://avatars.githubusercontent.com/u/13075269" - } - ] - }, - { - "author":"mac-cain13", - "name":"R.swift", - "avatar":"https://github.com/mac-cain13.png", - "description":"Strong typed, autocompleted resources like images, fonts and segues in Swift projects", - "url":"https://github.com/mac-cain13/R.swift", - "language":"Swift", - "languageColor":"#F05138", - "stars":8539, - "forks":628, - "currentPeriodStars":5, - "builtBy":[ - { - "username":"mac-cain13", - "href":"https://github.com/mac-cain13", - "avatar":"https://avatars.githubusercontent.com/u/618233" - }, - { - "username":"tomlokhorst", - "href":"https://github.com/tomlokhorst", - "avatar":"https://avatars.githubusercontent.com/u/75655" - }, - { - "username":"renrawnalon", - "href":"https://github.com/renrawnalon", - "avatar":"https://avatars.githubusercontent.com/u/605237" - }, - { - "username":"shiraji", - "href":"https://github.com/shiraji", - "avatar":"https://avatars.githubusercontent.com/u/3675458" - }, - { - "username":"tomasharkema", - "href":"https://github.com/tomasharkema", - "avatar":"https://avatars.githubusercontent.com/u/4534203" - } - ] - }, - { - "author":"ReactiveX", - "name":"RxSwift", - "avatar":"https://github.com/ReactiveX.png", - "description":"Reactive Programming in Swift", - "url":"https://github.com/ReactiveX/RxSwift", - "language":"Swift", - "languageColor":"#F05138", - "stars":21665, - "forks":3891, - "currentPeriodStars":8, - "builtBy":[ - { - "username":"kzaher", - "href":"https://github.com/kzaher", - "avatar":"https://avatars.githubusercontent.com/u/1641148" - }, - { - "username":"freak4pc", - "href":"https://github.com/freak4pc", - "avatar":"https://avatars.githubusercontent.com/u/605076" - }, - { - "username":"sergdort", - "href":"https://github.com/sergdort", - "avatar":"https://avatars.githubusercontent.com/u/4622322" - }, - { - "username":"yury", - "href":"https://github.com/yury", - "avatar":"https://avatars.githubusercontent.com/u/5250" - }, - { - "username":"tarunon", - "href":"https://github.com/tarunon", - "avatar":"https://avatars.githubusercontent.com/u/1830205" - } - ] - }, - { - "author":"Quick", - "name":"Quick", - "avatar":"https://github.com/Quick.png", - "description":"The Swift (and Objective-C) testing framework.", - "url":"https://github.com/Quick/Quick", - "language":"Swift", - "languageColor":"#F05138", - "stars":9355, - "forks":876, - "currentPeriodStars":1, - "builtBy":[ - { - "username":"ikesyo", - "href":"https://github.com/ikesyo", - "avatar":"https://avatars.githubusercontent.com/u/909674" - }, - { - "username":"modocache", - "href":"https://github.com/modocache", - "avatar":"https://avatars.githubusercontent.com/u/552921" - }, - { - "username":"jeffh", - "href":"https://github.com/jeffh", - "avatar":"https://avatars.githubusercontent.com/u/68616" - }, - { - "username":"wongzigii", - "href":"https://github.com/wongzigii", - "avatar":"https://avatars.githubusercontent.com/u/7384288" - }, - { - "username":"briancroom", - "href":"https://github.com/briancroom", - "avatar":"https://avatars.githubusercontent.com/u/1062518" - } - ] - }, - { - "author":"rxhanson", - "name":"Rectangle", - "avatar":"https://github.com/rxhanson.png", - "description":"Move and resize windows on macOS with keyboard shortcuts and snap areas", - "url":"https://github.com/rxhanson/Rectangle", - "language":"Swift", - "languageColor":"#F05138", - "stars":15697, - "forks":435, - "currentPeriodStars":21, - "builtBy":[ - { - "username":"rxhanson", - "href":"https://github.com/rxhanson", - "avatar":"https://avatars.githubusercontent.com/u/13651296" - }, - { - "username":"c-harding", - "href":"https://github.com/c-harding", - "avatar":"https://avatars.githubusercontent.com/u/8607022" - }, - { - "username":"patrick-stripe", - "href":"https://github.com/patrick-stripe", - "avatar":"https://avatars.githubusercontent.com/u/23064879" - }, - { - "username":"ryonakano", - "href":"https://github.com/ryonakano", - "avatar":"https://avatars.githubusercontent.com/u/26003928" - }, - { - "username":"sh-cho", - "href":"https://github.com/sh-cho", - "avatar":"https://avatars.githubusercontent.com/u/11611397" - } - ] - }, - { - "author":"Quick", - "name":"Nimble", - "avatar":"https://github.com/Quick.png", - "description":"A Matcher Framework for Swift and Objective-C", - "url":"https://github.com/Quick/Nimble", - "language":"Swift", - "languageColor":"#F05138", - "stars":4383, - "forks":496, - "currentPeriodStars":1, - "builtBy":[ - { - "username":"ikesyo", - "href":"https://github.com/ikesyo", - "avatar":"https://avatars.githubusercontent.com/u/909674" - }, - { - "username":"jeffh", - "href":"https://github.com/jeffh", - "avatar":"https://avatars.githubusercontent.com/u/68616" - }, - { - "username":"norio-nomura", - "href":"https://github.com/norio-nomura", - "avatar":"https://avatars.githubusercontent.com/u/33430" - }, - { - "username":"wongzigii", - "href":"https://github.com/wongzigii", - "avatar":"https://avatars.githubusercontent.com/u/7384288" - }, - { - "username":"abbeycode", - "href":"https://github.com/abbeycode", - "avatar":"https://avatars.githubusercontent.com/u/1349578" - } - ] - }, - { - "author":"RevenueCat", - "name":"purchases-ios", - "avatar":"https://github.com/RevenueCat.png", - "description":"In-app purchases and subscriptions made easy. Support for iOS, iPadOS, watchOS, and Mac.", - "url":"https://github.com/RevenueCat/purchases-ios", - "language":"Swift", - "languageColor":"#F05138", - "stars":1192, - "forks":152, - "currentPeriodStars":9, - "builtBy":[ - { - "username":"aboedo", - "href":"https://github.com/aboedo", - "avatar":"https://avatars.githubusercontent.com/u/3922667" - }, - { - "username":"jeiting", - "href":"https://github.com/jeiting", - "avatar":"https://avatars.githubusercontent.com/u/138742" - }, - { - "username":"NachoSoto", - "href":"https://github.com/NachoSoto", - "avatar":"https://avatars.githubusercontent.com/u/685609" - }, - { - "username":"vegaro", - "href":"https://github.com/vegaro", - "avatar":"https://avatars.githubusercontent.com/u/664544" - }, - { - "username":"taquitos", - "href":"https://github.com/taquitos", - "avatar":"https://avatars.githubusercontent.com/u/1304821" - } - ] - }, - { - "author":"vapor", - "name":"vapor", - "avatar":"https://github.com/vapor.png", - "description":"💧 A server-side Swift HTTP web framework.", - "url":"https://github.com/vapor/vapor", - "language":"Swift", - "languageColor":"#F05138", - "stars":21434, - "forks":1325, - "currentPeriodStars":7, - "builtBy":[ - { - "username":"tanner0101", - "href":"https://github.com/tanner0101", - "avatar":"https://avatars.githubusercontent.com/u/1342803" - }, - { - "username":"loganwright", - "href":"https://github.com/loganwright", - "avatar":"https://avatars.githubusercontent.com/u/5750489" - }, - { - "username":"Joannis", - "href":"https://github.com/Joannis", - "avatar":"https://avatars.githubusercontent.com/u/1951674" - }, - { - "username":"0xTim", - "href":"https://github.com/0xTim", - "avatar":"https://avatars.githubusercontent.com/u/9938337" - }, - { - "username":"shnhrrsn", - "href":"https://github.com/shnhrrsn", - "avatar":"https://avatars.githubusercontent.com/u/42901" - } - ] - }, - { - "author":"Swinject", - "name":"Swinject", - "avatar":"https://github.com/Swinject.png", - "description":"Dependency injection framework for Swift with iOS/macOS/Linux", - "url":"https://github.com/Swinject/Swinject", - "language":"Swift", - "languageColor":"#F05138", - "stars":5120, - "forks":419, - "currentPeriodStars":5, - "builtBy":[ - { - "username":"jakubvano", - "href":"https://github.com/jakubvano", - "avatar":"https://avatars.githubusercontent.com/u/2094466" - }, - { - "username":"yoichitgy", - "href":"https://github.com/yoichitgy", - "avatar":"https://avatars.githubusercontent.com/u/965994" - }, - { - "username":"1ucas", - "href":"https://github.com/1ucas", - "avatar":"https://avatars.githubusercontent.com/u/19654286" - }, - { - "username":"mpdifran", - "href":"https://github.com/mpdifran", - "avatar":"https://avatars.githubusercontent.com/u/1365987" - }, - { - "username":"mowens", - "href":"https://github.com/mowens", - "avatar":"https://avatars.githubusercontent.com/u/4575216" - } - ] - }, - { - "author":"SwiftGen", - "name":"SwiftGen", - "avatar":"https://github.com/SwiftGen.png", - "description":"The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!", - "url":"https://github.com/SwiftGen/SwiftGen", - "language":"Swift", - "languageColor":"#F05138", - "stars":7733, - "forks":599, - "currentPeriodStars":4, - "builtBy":[ - { - "username":"djbe", - "href":"https://github.com/djbe", - "avatar":"https://avatars.githubusercontent.com/u/641356" - }, - { - "username":"AliSoftware", - "href":"https://github.com/AliSoftware", - "avatar":"https://avatars.githubusercontent.com/u/216089" - }, - { - "username":"grantjbutler", - "href":"https://github.com/grantjbutler", - "avatar":"https://avatars.githubusercontent.com/u/526054" - }, - { - "username":"ffittschen", - "href":"https://github.com/ffittschen", - "avatar":"https://avatars.githubusercontent.com/u/7734806" - }, - { - "username":"dostrander", - "href":"https://github.com/dostrander", - "avatar":"https://avatars.githubusercontent.com/u/823322" - } - ] - }, - { - "author":"exelban", - "name":"stats", - "avatar":"https://github.com/exelban.png", - "description":"macOS system monitor in your menu bar", - "url":"https://github.com/exelban/stats", - "language":"Swift", - "languageColor":"#F05138", - "stars":9489, - "forks":334, - "currentPeriodStars":15, - "builtBy":[ - { - "username":"exelban", - "href":"https://github.com/exelban", - "avatar":"https://avatars.githubusercontent.com/u/13332412" - }, - { - "username":"jrthsr700tmax", - "href":"https://github.com/jrthsr700tmax", - "avatar":"https://avatars.githubusercontent.com/u/32911758" - }, - { - "username":"Tai-Zhou", - "href":"https://github.com/Tai-Zhou", - "avatar":"https://avatars.githubusercontent.com/u/21986946" - }, - { - "username":"zbrox", - "href":"https://github.com/zbrox", - "avatar":"https://avatars.githubusercontent.com/u/83163" - }, - { - "username":"rubjo", - "href":"https://github.com/rubjo", - "avatar":"https://avatars.githubusercontent.com/u/42270947" - } - ] - }, - { - "author":"pointfreeco", - "name":"swift-composable-architecture", - "avatar":"https://github.com/pointfreeco.png", - "description":"A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.", - "url":"https://github.com/pointfreeco/swift-composable-architecture", - "language":"Swift", - "languageColor":"#F05138", - "stars":5661, - "forks":521, - "currentPeriodStars":6, - "builtBy":[ - { - "username":"stephencelis", - "href":"https://github.com/stephencelis", - "avatar":"https://avatars.githubusercontent.com/u/658" - }, - { - "username":"mbrandonw", - "href":"https://github.com/mbrandonw", - "avatar":"https://avatars.githubusercontent.com/u/135203" - }, - { - "username":"iampatbrown", - "href":"https://github.com/iampatbrown", - "avatar":"https://avatars.githubusercontent.com/u/19797367" - }, - { - "username":"mluisbrown", - "href":"https://github.com/mluisbrown", - "avatar":"https://avatars.githubusercontent.com/u/4175766" - }, - { - "username":"dannyhertz", - "href":"https://github.com/dannyhertz", - "avatar":"https://avatars.githubusercontent.com/u/407719" - } - ] - }, - { - "author":"realm", - "name":"SwiftLint", - "avatar":"https://github.com/realm.png", - "description":"A tool to enforce Swift style and conventions.", - "url":"https://github.com/realm/SwiftLint", - "language":"Swift", - "languageColor":"#F05138", - "stars":15779, - "forks":1881, - "currentPeriodStars":6, - "builtBy":[ - { - "username":"jpsim", - "href":"https://github.com/jpsim", - "avatar":"https://avatars.githubusercontent.com/u/474794" - }, - { - "username":"marcelofabri", - "href":"https://github.com/marcelofabri", - "avatar":"https://avatars.githubusercontent.com/u/833072" - }, - { - "username":"norio-nomura", - "href":"https://github.com/norio-nomura", - "avatar":"https://avatars.githubusercontent.com/u/33430" - }, - { - "username":"Jeehut", - "href":"https://github.com/Jeehut", - "avatar":"https://avatars.githubusercontent.com/u/6942160" - }, - { - "username":"scottrhoyt", - "href":"https://github.com/scottrhoyt", - "avatar":"https://avatars.githubusercontent.com/u/4259250" - } - ] - }, - { - "author":"DevUtilsApp", - "name":"DevUtils-app", - "avatar":"https://github.com/DevUtilsApp.png", - "description":"Offline Toolbox for Developers", - "url":"https://github.com/DevUtilsApp/DevUtils-app", - "language":"Swift", - "languageColor":"#F05138", - "stars":3217, - "forks":158, - "currentPeriodStars":27, - "builtBy":[ - { - "username":"trungdq88", - "href":"https://github.com/trungdq88", - "avatar":"https://avatars.githubusercontent.com/u/4214509" - }, - { - "username":"Justin-Credible", - "href":"https://github.com/Justin-Credible", - "avatar":"https://avatars.githubusercontent.com/u/1243565" - }, - { - "username":"danielewood", - "href":"https://github.com/danielewood", - "avatar":"https://avatars.githubusercontent.com/u/23008560" - }, - { - "username":"weitieda", - "href":"https://github.com/weitieda", - "avatar":"https://avatars.githubusercontent.com/u/35972055" - } - ] - }, - { - "author":"Lakr233", - "name":"Rayon", - "avatar":"https://github.com/Lakr233.png", - "description":"yet another SSH machine manager", - "url":"https://github.com/Lakr233/Rayon", - "language":"Swift", - "languageColor":"#F05138", - "stars":489, - "forks":25, - "currentPeriodStars":30, - "builtBy":[ - { - "username":"Lakr233", - "href":"https://github.com/Lakr233", - "avatar":"https://avatars.githubusercontent.com/u/25259084" - }, - { - "username":"MisakiCoca", - "href":"https://github.com/MisakiCoca", - "avatar":"https://avatars.githubusercontent.com/u/40165523" - } - ] - }, - { - "author":"serhii-londar", - "name":"open-source-mac-os-apps", - "avatar":"https://github.com/serhii-londar.png", - "description":"🚀 Awesome list of open source applications for macOS. https://t.me/s/opensourcemacosapps", - "url":"https://github.com/serhii-londar/open-source-mac-os-apps", - "language":"Swift", - "languageColor":"#F05138", - "stars":29614, - "forks":1942, - "currentPeriodStars":13, - "builtBy":[ - { - "username":"serhii-londar", - "href":"https://github.com/serhii-londar", - "avatar":"https://avatars.githubusercontent.com/u/15808174" - }, - { - "username":"justinclift", - "href":"https://github.com/justinclift", - "avatar":"https://avatars.githubusercontent.com/u/406299" - }, - { - "username":"alichtman", - "href":"https://github.com/alichtman", - "avatar":"https://avatars.githubusercontent.com/u/20600565" - }, - { - "username":"765678765456765456", - "href":"https://github.com/765678765456765456", - "avatar":"https://avatars.githubusercontent.com/u/22583506" - }, - { - "username":"vpeschenkov", - "href":"https://github.com/vpeschenkov", - "avatar":"https://avatars.githubusercontent.com/u/3672477" - } - ] - }, - { - "author":"pointfreeco", - "name":"swift-snapshot-testing", - "avatar":"https://github.com/pointfreeco.png", - "description":"📸 Delightful Swift snapshot testing.", - "url":"https://github.com/pointfreeco/swift-snapshot-testing", - "language":"Swift", - "languageColor":"#F05138", - "stars":2486, - "forks":292, - "currentPeriodStars":2, - "builtBy":[ - { - "username":"stephencelis", - "href":"https://github.com/stephencelis", - "avatar":"https://avatars.githubusercontent.com/u/658" - }, - { - "username":"mbrandonw", - "href":"https://github.com/mbrandonw", - "avatar":"https://avatars.githubusercontent.com/u/135203" - }, - { - "username":"Sherlouk", - "href":"https://github.com/Sherlouk", - "avatar":"https://avatars.githubusercontent.com/u/15193942" - }, - { - "username":"mr-v", - "href":"https://github.com/mr-v", - "avatar":"https://avatars.githubusercontent.com/u/830743" - }, - { - "username":"rjchatfield", - "href":"https://github.com/rjchatfield", - "avatar":"https://avatars.githubusercontent.com/u/5361118" - } - ] - }, - { - "author":"scinfu", - "name":"SwiftSoup", - "avatar":"https://github.com/scinfu.png", - "description":"SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)", - "url":"https://github.com/scinfu/SwiftSoup", - "language":"Swift", - "languageColor":"#F05138", - "stars":3182, - "forks":225, - "currentPeriodStars":4, - "builtBy":[ - { - "username":"scinfu", - "href":"https://github.com/scinfu", - "avatar":"https://avatars.githubusercontent.com/u/201132" - }, - { - "username":"0xTim", - "href":"https://github.com/0xTim", - "avatar":"https://avatars.githubusercontent.com/u/9938337" - }, - { - "username":"GarthSnyder", - "href":"https://github.com/GarthSnyder", - "avatar":"https://avatars.githubusercontent.com/u/443181" - }, - { - "username":"fassko", - "href":"https://github.com/fassko", - "avatar":"https://avatars.githubusercontent.com/u/29482" - }, - { - "username":"RLovelett", - "href":"https://github.com/RLovelett", - "avatar":"https://avatars.githubusercontent.com/u/335572" - } - ] - }, - { - "author":"Caldis", - "name":"Mos", - "avatar":"https://github.com/Caldis.png", - "description":"一个用于在 macOS ä¸Šå¹³æ»‘ä½ çš„é¼ æ ‡æ»šåŠ¨æ•ˆæžœæˆ–å•ç‹¬è®¾ç½®æ»šåŠ¨æ–¹å‘çš„å°å·¥å…·, è®©ä½ çš„æ»šè½®çˆ½å¦‚è§¦æŽ§æ¿ | A lightweight tool used to smooth scrolling and set scroll direction independently for your mouse on macOS", - "url":"https://github.com/Caldis/Mos", - "language":"Swift", - "languageColor":"#F05138", - "stars":8242, - "forks":350, - "currentPeriodStars":10, - "builtBy":[ - { - "username":"Caldis", - "href":"https://github.com/Caldis", - "avatar":"https://avatars.githubusercontent.com/u/3529490" - }, - { - "username":"mclvren", - "href":"https://github.com/mclvren", - "avatar":"https://avatars.githubusercontent.com/u/40006037" - }, - { - "username":"lima0", - "href":"https://github.com/lima0", - "avatar":"https://avatars.githubusercontent.com/u/47898885" - }, - { - "username":"jakecast", - "href":"https://github.com/jakecast", - "avatar":"https://avatars.githubusercontent.com/u/868863" - }, - { - "username":"godly-devotion", - "href":"https://github.com/godly-devotion", - "avatar":"https://avatars.githubusercontent.com/u/1341760" - } - ] - }, - { - "author":"quoid", - "name":"userscripts", - "avatar":"https://github.com/quoid.png", - "description":"An open-source userscript manager for Safari", - "url":"https://github.com/quoid/userscripts", - "language":"Swift", - "languageColor":"#F05138", - "stars":795, - "forks":33, - "currentPeriodStars":7, - "builtBy":[ - { - "username":"quoid", - "href":"https://github.com/quoid", - "avatar":"https://avatars.githubusercontent.com/u/7660254" - }, - { - "username":"TraderStf", - "href":"https://github.com/TraderStf", - "avatar":"https://avatars.githubusercontent.com/u/5954335" - }, - { - "username":"dekpient", - "href":"https://github.com/dekpient", - "avatar":"https://avatars.githubusercontent.com/u/717270" - }, - { - "username":"lucka-me", - "href":"https://github.com/lucka-me", - "avatar":"https://avatars.githubusercontent.com/u/17932593" - } - ] - }, - { - "author":"danielgindi", - "name":"Charts", - "avatar":"https://github.com/danielgindi.png", - "description":"Beautiful charts for iOS/tvOS/OSX! The Apple side of the crossplatform MPAndroidChart.", - "url":"https://github.com/danielgindi/Charts", - "language":"Swift", - "languageColor":"#F05138", - "stars":25041, - "forks":5161, - "currentPeriodStars":8, - "builtBy":[ - { - "username":"danielgindi", - "href":"https://github.com/danielgindi", - "avatar":"https://avatars.githubusercontent.com/u/366926" - }, - { - "username":"liuxuan30", - "href":"https://github.com/liuxuan30", - "avatar":"https://avatars.githubusercontent.com/u/4375169" - }, - { - "username":"pmairoldi", - "href":"https://github.com/pmairoldi", - "avatar":"https://avatars.githubusercontent.com/u/296718" - }, - { - "username":"jjatie", - "href":"https://github.com/jjatie", - "avatar":"https://avatars.githubusercontent.com/u/19879272" - }, - { - "username":"mathewa6", - "href":"https://github.com/mathewa6", - "avatar":"https://avatars.githubusercontent.com/u/5402357" - } - ] - }, - { - "author":"Carthage", - "name":"Carthage", - "avatar":"https://github.com/Carthage.png", - "description":"A simple, decentralized dependency manager for Cocoa", - "url":"https://github.com/Carthage/Carthage", - "language":"Swift", - "languageColor":"#F05138", - "stars":14521, - "forks":1565, - "currentPeriodStars":3, - "builtBy":[ - { - "username":"ikesyo", - "href":"https://github.com/ikesyo", - "avatar":"https://avatars.githubusercontent.com/u/909674" - }, - { - "username":"jspahrsummers", - "href":"https://github.com/jspahrsummers", - "avatar":"https://avatars.githubusercontent.com/u/432536" - }, - { - "username":"mdiep", - "href":"https://github.com/mdiep", - "avatar":"https://avatars.githubusercontent.com/u/1302" - }, - { - "username":"jdhealy", - "href":"https://github.com/jdhealy", - "avatar":"https://avatars.githubusercontent.com/u/782837" - }, - { - "username":"alanjrogers", - "href":"https://github.com/alanjrogers", - "avatar":"https://avatars.githubusercontent.com/u/22635" - } - ] - }, - { - "author":"grpc", - "name":"grpc-swift", - "avatar":"https://github.com/grpc.png", - "description":"The Swift language implementation of gRPC.", - "url":"https://github.com/grpc/grpc-swift", - "language":"Swift", - "languageColor":"#F05138", - "stars":1471, - "forks":319, - "currentPeriodStars":1, - "builtBy":[ - { - "username":"timburks", - "href":"https://github.com/timburks", - "avatar":"https://avatars.githubusercontent.com/u/405" - }, - { - "username":"glbrntt", - "href":"https://github.com/glbrntt", - "avatar":"https://avatars.githubusercontent.com/u/5047671" - }, - { - "username":"MrMage", - "href":"https://github.com/MrMage", - "avatar":"https://avatars.githubusercontent.com/u/117466" - }, - { - "username":"rebello95", - "href":"https://github.com/rebello95", - "avatar":"https://avatars.githubusercontent.com/u/2643476" - }, - { - "username":"Lukasa", - "href":"https://github.com/Lukasa", - "avatar":"https://avatars.githubusercontent.com/u/1382556" - } - ] - }, - { - "author":"Ranchero-Software", - "name":"NetNewsWire", - "avatar":"https://github.com/Ranchero-Software.png", - "description":"RSS reader for macOS and iOS.", - "url":"https://github.com/Ranchero-Software/NetNewsWire", - "language":"Swift", - "languageColor":"#F05138", - "stars":5330, - "forks":395, - "currentPeriodStars":4, - "builtBy":[ - { - "username":"vincode-io", - "href":"https://github.com/vincode-io", - "avatar":"https://avatars.githubusercontent.com/u/16448027" - }, - { - "username":"brentsimmons", - "href":"https://github.com/brentsimmons", - "avatar":"https://avatars.githubusercontent.com/u/1297121" - }, - { - "username":"stuartbreckenridge", - "href":"https://github.com/stuartbreckenridge", - "avatar":"https://avatars.githubusercontent.com/u/7046652" - }, - { - "username":"Wevah", - "href":"https://github.com/Wevah", - "avatar":"https://avatars.githubusercontent.com/u/130825" - }, - { - "username":"kielgillard", - "href":"https://github.com/kielgillard", - "avatar":"https://avatars.githubusercontent.com/u/567949" - } - ] - } -] diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/UserTrendings.json b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/UserTrendings.json deleted file mode 100644 index f4b628c..0000000 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/StubbedResponse/UserTrendings.json +++ /dev/null @@ -1,302 +0,0 @@ -[ - { - "username":"jpsim", - "name":"JP Simard", - "url":"https://github.com/jpsim", - "sponsorUrl":"https://github.com/sponsors/jpsim", - "avatar":"https://avatars.githubusercontent.com/u/474794", - "repo":{ - "name":"ZenTuner", - "description":"A minimal chromatic tuner for iOS & macOS.", - "url":"https://github.com/jpsim/ZenTuner" - } - }, - { - "username":"nicklockwood", - "name":"Nick Lockwood", - "url":"https://github.com/nicklockwood", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/546885", - "repo":{ - "name":"SwiftFormat", - "description":"A command-line tool and Xcode Extension for formatting Swift code", - "url":"https://github.com/nicklockwood/SwiftFormat" - } - }, - { - "username":"gao-sun", - "name":"Gao Sun", - "url":"https://github.com/gao-sun", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/14722250", - "repo":{ - "name":"eul", - "description":"🖥️ macOS status monitoring app written in SwiftUI.", - "url":"https://github.com/gao-sun/eul" - } - }, - { - "username":"onevcat", - "name":"Wei Wang", - "url":"https://github.com/onevcat", - "sponsorUrl":"https://github.com/sponsors/onevcat", - "avatar":"https://avatars.githubusercontent.com/u/1019875", - "repo":{ - "name":"Kingfisher", - "description":"A lightweight, pure-Swift library for downloading and caching images from the web.", - "url":"https://github.com/onevcat/Kingfisher" - } - }, - { - "username":"yonaskolb", - "name":"Yonas Kolb", - "url":"https://github.com/yonaskolb", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/2393781", - "repo":{ - "name":"XcodeGen", - "description":"A Swift command line tool for generating your Xcode project", - "url":"https://github.com/yonaskolb/XcodeGen" - } - }, - { - "username":"kylef", - "name":"Kyle Fuller", - "url":"https://github.com/kylef", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/44164", - "repo":{ - "name":"Mockingjay", - "description":"An elegant library for stubbing HTTP requests with ease in Swift", - "url":"https://github.com/kylef/Mockingjay" - } - }, - { - "username":"groue", - "name":"Gwendal Roué", - "url":"https://github.com/groue", - "sponsorUrl":"https://github.com/sponsors/groue", - "avatar":"https://avatars.githubusercontent.com/u/54219", - "repo":{ - "name":"GRDB.swift", - "description":"A toolkit for SQLite databases, with a focus on application development", - "url":"https://github.com/groue/GRDB.swift" - } - }, - { - "username":"neilalexander", - "name":"Neil Alexander", - "url":"https://github.com/neilalexander", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/310854", - "repo":{ - "name":"seaglass", - "description":"A truly native Matrix client for macOS - written in Swift/Cocoa, with E2E encryption support", - "url":"https://github.com/neilalexander/seaglass" - } - }, - { - "username":"devxoul", - "name":"Suyeol Jeon", - "url":"https://github.com/devxoul", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/931655", - "repo":{ - "name":"SwiftyImage", - "description":"🎨 Generate image resources in Swift", - "url":"https://github.com/devxoul/SwiftyImage" - } - }, - { - "username":"osy", - "name":"osy", - "url":"https://github.com/osy", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/50960678", - "repo":{ - "name":"Jitterbug", - "description":"Launch JIT enabled iOS app with a second iOS device", - "url":"https://github.com/osy/Jitterbug" - } - }, - { - "username":"AliSoftware", - "name":"Olivier Halligon", - "url":"https://github.com/AliSoftware", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/216089", - "repo":{ - "name":"Reusable", - "description":"A Swift mixin for reusing views easily and in a type-safe way (UITableViewCells, UICollectionViewCells, custom UIViews, ViewControllers, Storyboards…)", - "url":"https://github.com/AliSoftware/Reusable" - } - }, - { - "username":"bizz84", - "name":"Andrea Bizzotto", - "url":"https://github.com/bizz84", - "sponsorUrl":"https://github.com/sponsors/bizz84", - "avatar":"https://avatars.githubusercontent.com/u/153167", - "repo":{ - "name":"SwiftyStoreKit", - "description":"Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺", - "url":"https://github.com/bizz84/SwiftyStoreKit" - } - }, - { - "username":"mrousavy", - "name":"Marc Rousavy", - "url":"https://github.com/mrousavy", - "sponsorUrl":"https://github.com/sponsors/mrousavy", - "avatar":"https://avatars.githubusercontent.com/u/15199031", - "repo":{ - "name":"react-native-vision-camera", - "description":"📸 The Camera library that sees the vision.", - "url":"https://github.com/mrousavy/react-native-vision-camera" - } - }, - { - "username":"AvdLee", - "name":"Antoine van der Lee", - "url":"https://github.com/AvdLee", - "sponsorUrl":"https://github.com/sponsors/AvdLee", - "avatar":"https://avatars.githubusercontent.com/u/4329185", - "repo":{ - "name":"SwiftUIKitView", - "description":"Easily use UIKit views in your SwiftUI applications. Create Xcode Previews for UIView elements", - "url":"https://github.com/AvdLee/SwiftUIKitView" - } - }, - { - "username":"jtbandes", - "name":"Jacob Bandes-Storch", - "url":"https://github.com/jtbandes", - "sponsorUrl":"https://github.com/sponsors/jtbandes", - "avatar":"https://avatars.githubusercontent.com/u/14237", - "repo":{ - "name":"SpacePOD", - "description":"Space! – an iOS widget displaying NASA's Astronomy Picture of the Day", - "url":"https://github.com/jtbandes/SpacePOD" - } - }, - { - "username":"krzysztofzablocki", - "name":"Krzysztof ZabÅ‚ocki", - "url":"https://github.com/krzysztofzablocki", - "sponsorUrl":"https://github.com/sponsors/krzysztofzablocki", - "avatar":"https://avatars.githubusercontent.com/u/1468993", - "repo":{ - "name":"Difference", - "description":"Simple way to identify what is different between 2 instances of any type. Must have for TDD.", - "url":"https://github.com/krzysztofzablocki/Difference" - } - }, - { - "username":"gordonbrander", - "name":"Gordon Brander", - "url":"https://github.com/gordonbrander", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/58421", - "repo":{ - "name":"ObservableStore", - "description":"A lightweight Elm-like Store for SwiftUI", - "url":"https://github.com/gordonbrander/ObservableStore" - } - }, - { - "username":"malcommac", - "name":"Daniele Margutti", - "url":"https://github.com/malcommac", - "sponsorUrl":"https://github.com/sponsors/malcommac", - "avatar":"https://avatars.githubusercontent.com/u/235645", - "repo":{ - "name":"SwiftDate", - "description":"🐔 Toolkit to parse, validate, manipulate, compare and display dates, time & timezones in Swift.", - "url":"https://github.com/malcommac/SwiftDate" - } - }, - { - "username":"mac-cain13", - "name":"Mathijs Kadijk", - "url":"https://github.com/mac-cain13", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/618233", - "repo":{ - "name":"R.swift", - "description":"Strong typed, autocompleted resources like images, fonts and segues in Swift projects", - "url":"https://github.com/mac-cain13/R.swift" - } - }, - { - "username":"kean", - "name":"Alexander Grebenyuk", - "url":"https://github.com/kean", - "sponsorUrl":"https://github.com/sponsors/kean", - "avatar":"https://avatars.githubusercontent.com/u/1567433", - "repo":{ - "name":"Pulse", - "description":"Logger and network inspector for Apple platforms", - "url":"https://github.com/kean/Pulse" - } - }, - { - "username":"alexisakers", - "name":"Alexis (Aubry) Akers", - "url":"https://github.com/alexisakers", - "sponsorUrl":"https://github.com/sponsors/alexisakers", - "avatar":"https://avatars.githubusercontent.com/u/16192914", - "repo":{ - "name":"JavaScriptKit", - "description":"JavaScript Toolkit for WKWebView", - "url":"https://github.com/alexisakers/JavaScriptKit" - } - }, - { - "username":"marmelroy", - "name":"Roy Marmelstein", - "url":"https://github.com/marmelroy", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/889949", - "repo":{ - "name":"PhoneNumberKit", - "description":"A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.", - "url":"https://github.com/marmelroy/PhoneNumberKit" - } - }, - { - "username":"mxcl", - "name":"Max Howell", - "url":"https://github.com/mxcl", - "sponsorUrl":"https://github.com/sponsors/mxcl", - "avatar":"https://avatars.githubusercontent.com/u/58962", - "repo":{ - "name":"PromiseKit", - "description":"Promises for Swift & ObjC.", - "url":"https://github.com/mxcl/PromiseKit" - } - }, - { - "username":"JohnSundell", - "name":"John Sundell", - "url":"https://github.com/JohnSundell", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/2466701", - "repo":{ - "name":"Publish", - "description":"A static site generator for Swift developers", - "url":"https://github.com/JohnSundell/Publish" - } - }, - { - "username":"inamiy", - "name":"Yasuhiro Inami", - "url":"https://github.com/inamiy", - "sponsorUrl":null, - "avatar":"https://avatars.githubusercontent.com/u/138476", - "repo":{ - "name":"SherlockForms", - "description":"🕵️‍♂️ An elegant SwiftUI Form builder to create a searchable Settings and DebugMenu screens for iOS.", - "url":"https://github.com/inamiy/SherlockForms" - } - } -] diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/AnyPublisher+Extensions.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/AnyPublisher+Extensions.swift index 0d2adf0..f536b13 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/AnyPublisher+Extensions.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/AnyPublisher+Extensions.swift @@ -10,30 +10,30 @@ import Combine import Foundation enum AsyncError: Error { - case finishedWithoutValue + case finishedWithoutValue } extension AnyPublisher { - - func async() async throws -> Output { - try await withCheckedThrowingContinuation { continuation in - var cancellable: AnyCancellable? - var finishedWithoutValue = true - cancellable = first() - .sink { result in - switch result { - case .finished: - if finishedWithoutValue { - continuation.resume(throwing: AsyncError.finishedWithoutValue) - } - case let .failure(error): - continuation.resume(throwing: error) - } - cancellable?.cancel() - } receiveValue: { value in - finishedWithoutValue = false - continuation.resume(with: .success(value)) - } + + func async() async throws -> Output { + try await withCheckedThrowingContinuation { continuation in + var cancellable: AnyCancellable? + var finishedWithoutValue = true + cancellable = first() + .sink { result in + switch result { + case .finished: + if finishedWithoutValue { + continuation.resume(throwing: AsyncError.finishedWithoutValue) + } + case let .failure(error): + continuation.resume(throwing: error) + } + cancellable?.cancel() + } receiveValue: { value in + finishedWithoutValue = false + continuation.resume(with: .success(value)) } } + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/VerbosePlugin.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/VerbosePlugin.swift index ce1f6bd..b34c19e 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/VerbosePlugin.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Utility/VerbosePlugin.swift @@ -6,41 +6,44 @@ // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // -import Foundation import Combine +import Foundation import Moya import Utilities struct VerbosePlugin: PluginType { - let verbose: Bool - - func prepare(_ request: URLRequest, target _: TargetType) -> URLRequest { -#if DEBUG - if let body = request.httpBody, - let string = String(data: body, encoding: .utf8) { - if verbose { - Logger.info("Request Body: \(string))") - } + let verbose: Bool + + func prepare(_ request: URLRequest, target _: TargetType) -> URLRequest { + #if DEBUG + if let body = request.httpBody, + let string = String(data: body, encoding: .utf8) + { + if verbose { + Logger.info("Request Body: \(string))") } -#endif - return request - } - - func didReceive(_ result: Result, target _: TargetType) { -#if DEBUG - switch result { - case let .success(body): - if verbose { - if let json = try? JSONSerialization.jsonObject(with: body.data, options: .mutableContainers) { - Logger.info("Response: \(json))") - } else { - let response = String(data: body.data, encoding: .utf8)! - Logger.info("Response: \(response))") - } - } - case .failure: - break + } + #endif + return request + } + + func didReceive(_ result: Result, target _: TargetType) { + #if DEBUG + switch result { + case let .success(body): + if verbose { + if let json = try? JSONSerialization.jsonObject( + with: body.data, options: .mutableContainers) + { + Logger.info("Response: \(json))") + } else { + let response = String(data: body.data, encoding: .utf8)! + Logger.info("Response: \(response))") + } } -#endif - } + case .failure: + break + } + #endif + } } diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Tests/NetworkPlatformTests/NetworkPlatformTests.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Tests/NetworkPlatformTests/NetworkPlatformTests.swift index 7649e55..d77a796 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Tests/NetworkPlatformTests/NetworkPlatformTests.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Tests/NetworkPlatformTests/NetworkPlatformTests.swift @@ -1,11 +1,12 @@ import XCTest + @testable import NetworkPlatform final class NetworkPlatformTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(NetworkPlatform().text, "Hello, World!") - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(NetworkPlatform().text, "Hello, World!") + } } diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/.gitignore b/{{cookiecutter.app_name}}/PersistentPlatform/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/.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/{{cookiecutter.app_name}}/PersistentPlatform/.swiftpm/xcode/xcshareddata/xcschemes/PersistentPlatform.xcscheme b/{{cookiecutter.app_name}}/PersistentPlatform/.swiftpm/xcode/xcshareddata/xcschemes/PersistentPlatform.xcscheme new file mode 100644 index 0000000..a31bb38 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/.swiftpm/xcode/xcshareddata/xcschemes/PersistentPlatform.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Package.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Package.swift new file mode 100644 index 0000000..c636e7a --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "PersistentPlatform", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "PersistentPlatform", + targets: ["PersistentPlatform"]) + ], + dependencies: [ + .package(path: "../Domain"), + .package(path: "../Utilities"), + .package( + url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), + ], + 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. + .target( + name: "PersistentPlatform", + dependencies: [ + .product(name: "Domain", package: "Domain"), + .product(name: "Utilities", package: "Utilities"), + .product(name: "Dependencies", package: "swift-dependencies"), + ] + ) + ] +) diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/Constants+CoreData.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/Constants+CoreData.swift new file mode 100644 index 0000000..a08f048 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/Constants+CoreData.swift @@ -0,0 +1,19 @@ +// +// Constants+CoreData.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Foundation +import Utilities + +public enum Constants {} + +extension Constants { + public enum CoreData { + public static let model = "Model" + public static let subdirectory = "Model.momd" + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/DomainConvertible.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/DomainConvertible.swift new file mode 100644 index 0000000..9f6bf26 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/DomainConvertible.swift @@ -0,0 +1,38 @@ +// +// DomainConvertible.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain +import Foundation + +public protocol DomainConvertible { + associatedtype DomainType + func asDomain(context: NSManagedObjectContext) -> DomainType +} + +extension Array: DomainConvertible where Element: DomainConvertible { + public func asDomain(context: NSManagedObjectContext) -> [Element.DomainType] + { + self.map { $0.asDomain(context: context) } + } +} + +extension Set: DomainConvertible where Element: DomainConvertible { + public func asDomain(context: NSManagedObjectContext) -> [Element.DomainType] + { + self.map { $0.asDomain(context: context) } + } +} + +extension NSOrderedSet { + func asDomain( + context: NSManagedObjectContext, entityElementType: EntityType.Type + ) -> [DomainElement] where EntityType.DomainType == DomainElement { + return self.compactMap { ($0 as? EntityType)?.asDomain(context: context) } + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Compatible.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Compatible.swift new file mode 100644 index 0000000..e1a0de9 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Compatible.swift @@ -0,0 +1,19 @@ +// +// NSManagedObjectModel+Compatible.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Foundation + +extension NSManagedObjectModel { + static func compatibleModelForStoreMetadata(_ metadata: [String: Any]) + -> NSManagedObjectModel? + { + NSManagedObjectModel.mergedModel( + from: [Bundle.module], forStoreMetadata: metadata) + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Resource.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Resource.swift new file mode 100644 index 0000000..e273932 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSManagedObjectModel/NSManagedObjectModel+Resource.swift @@ -0,0 +1,32 @@ +// +// NSManagedObjectModel+Resource.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain +import Utilities + +extension NSManagedObjectModel { + static func managedObjectModel(forResource resource: String) throws + -> NSManagedObjectModel + { + let omoURL = Bundle.module.url( + forResource: resource, withExtension: "omo", + subdirectory: Constants.CoreData.subdirectory) + let momURL = Bundle.module.url( + forResource: resource, withExtension: "mom", + subdirectory: Constants.CoreData.subdirectory) + + guard let url = omoURL ?? momURL else { + throw AppError.databaseCorrupted("Unable to find model in bundle.") + } + guard let model = NSManagedObjectModel(contentsOf: url) else { + throw AppError.databaseCorrupted("Unable to load model in bundle.") + } + return model + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSPersistentStoreCoordinator/NSPersistentStoreCoordinator+SQLite.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSPersistentStoreCoordinator/NSPersistentStoreCoordinator+SQLite.swift new file mode 100644 index 0000000..223f52e --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Extensions/NSPersistentStoreCoordinator/NSPersistentStoreCoordinator+SQLite.swift @@ -0,0 +1,63 @@ +// +// NSPersistentStoreCoordinator+SQLite.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain + +extension NSPersistentStoreCoordinator { + static func destroyStore(at storeURL: URL) throws { + do { + let persistentStoreCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: NSManagedObjectModel()) + try persistentStoreCoordinator.destroyPersistentStore( + at: storeURL, ofType: NSSQLiteStoreType, options: nil) + } catch { + let message = + "Failed to destroy persistent store at \(storeURL), error: \(error)." + throw AppError.databaseCorrupted(message) + } + } + + static func replaceStore(at targetURL: URL, withStoreAt sourceURL: URL) throws + { + do { + let persistentStoreCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: NSManagedObjectModel()) + try persistentStoreCoordinator.replacePersistentStore( + at: targetURL, destinationOptions: nil, + withPersistentStoreFrom: sourceURL, + sourceOptions: nil, ofType: NSSQLiteStoreType + ) + } catch { + let message = + "Failed to replace persistent store at \(targetURL) with \(sourceURL), error: \(error)." + throw AppError.databaseCorrupted(message) + } + } + + static func metadata(at storeURL: URL) -> [String: Any]? { + try? NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, at: storeURL, options: nil + ) + } + + func addPersistentStore(at storeURL: URL, options: [AnyHashable: Any]) throws + -> NSPersistentStore + { + do { + return try addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, + options: options + ) + } catch { + let message = + "Failed to add persistent store to coordinator, error: \(error)." + throw AppError.databaseCorrupted(message) + } + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataClass.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataClass.swift new file mode 100644 index 0000000..f6a3ddb --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataClass.swift @@ -0,0 +1,28 @@ +// +// Product+CoreDataClass.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain +import Foundation + +@objc(Product) +public class Product: NSManagedObject {} + +// MARK: DomainConvertible + +extension Product: DomainConvertible { + public typealias DomainType = Domain.Product + + public func asDomain(context: NSManagedObjectContext) -> Domain.Product { + context.performAndWait { [self] in + Domain.Product( + id: .init(id) ?? 0, price: price, title: title, image: image, + category: category, description: productDescription) + } + } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataProperties.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataProperties.swift new file mode 100644 index 0000000..61eecf6 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/MODefinition/Product+CoreDataProperties.swift @@ -0,0 +1,23 @@ +// +// Product+CoreDataProperties.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain + +extension Product { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Product") + } + + @NSManaged public var id: String + @NSManaged public var price: Double + @NSManaged public var title: String + @NSManaged public var image: String + @NSManaged public var category: String + @NSManaged public var productDescription: String +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 0000000..2129f10 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/PersistenceController.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/PersistenceController.swift new file mode 100644 index 0000000..57a3524 --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/PersistenceController.swift @@ -0,0 +1,407 @@ +// +// PersistenceController.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import CoreData +import Domain +import Utilities + +actor PersistenceController { + static let shared = PersistenceController() + var persistentStoreNotLoaded = true + let container: NSPersistentContainer = { + let objectModel = try! NSManagedObjectModel.managedObjectModel( + forResource: Constants.CoreData.model) + let container = NSPersistentContainer( + name: Constants.CoreData.model, managedObjectModel: objectModel) + let description = container.persistentStoreDescriptions.first + description?.shouldInferMappingModelAutomatically = false + description?.shouldMigrateStoreAutomatically = false + return container + }() + + var viewContext: NSManagedObjectContext { + container.viewContext + } +} + +// MARK: Preparation + +extension PersistenceController { + + private func saveContext(_ context: NSManagedObjectContext) { + guard context.hasChanges else { return } + do { + try context.save() + } catch { + assertionFailure("Unresolved error \(error)") + } + } + + private func fetch( + _ request: NSFetchRequest, + context: NSManagedObjectContext + ) throws -> [T] where T: NSFetchRequestResult { + try context.performAndWait { + try context.fetch(request) + } + + } + + func prepare() async throws { + if persistentStoreNotLoaded { + _ = try await loadPersistentStore() + persistentStoreNotLoaded = false + } + } + + func rebuild() async throws { + guard let storeURL = container.persistentStoreDescriptions.first?.url else { + throw AppError.databaseCorrupted( + "PersistentContainer was not set up properly.") + } + + try NSPersistentStoreCoordinator.destroyStore(at: storeURL) + + return try await withCheckedThrowingContinuation { continuation in + container.loadPersistentStores { _, error in + guard error == nil else { + let message = "Was unable to load store \(String(describing: error))." + continuation.resume(throwing: AppError.databaseCorrupted(message)) + return + } + } + } + } + + func materializedObjects( + context: NSManagedObjectContext, predicate: NSPredicate + ) -> [NSManagedObject] { + context.performAndWait { + var objects = [NSManagedObject]() + for object in context.registeredObjects where !object.isFault { + guard object.entity.attributesByName.keys.contains("uid"), + predicate.evaluate(with: object) + else { continue } + objects.append(object) + } + return objects + } + } + + private func loadPersistentStore() async throws { + return try await withCheckedThrowingContinuation { continuation in + container.loadPersistentStores { desc, error in + guard error == nil else { + let message = "Was unable to load store \(String(describing: error))." + continuation.resume(throwing: AppError.databaseCorrupted(message)) + return + } + debugPrint("Persistent store loaded: \(desc)") + continuation.resume() + } + } + } +} + +// MARK: Foundation + +extension PersistenceController { + + private func batchFetch( + entityType: MO.Type, + fetchLimit: Int = 100, + predicate: NSPredicate? = nil, + findBeforeFetch: Bool = false, + sortDescriptors: [NSSortDescriptor]? = nil, + context: NSManagedObjectContext + ) -> [MO] { + var results = [MO]() + results = context.performAndWait { + if findBeforeFetch, let predicate = predicate { + if let objects = materializedObjects( + context: context, predicate: predicate) + as? [MO], + !objects.isEmpty + { + results = objects + } + } + let request = NSFetchRequest( + entityName: String(describing: entityType) + ) + request.predicate = predicate + request.fetchLimit = fetchLimit + request.sortDescriptors = sortDescriptors + + results = (try? fetch(request, context: context)) ?? [] + return results + } + return results + } + + func batchFetchDomainObject( + entityType: MO.Type, + fetchLimit: Int = 100, + predicate: NSPredicate? = nil, + findBeforeFetch: Bool = false, + sortDescriptors: [NSSortDescriptor]? = nil, + context: NSManagedObjectContext? = nil + ) -> [MO.DomainType] where MO: DomainConvertible { + let context = context ?? container.newBackgroundContext() + return batchFetch( + entityType: entityType, + fetchLimit: fetchLimit, + predicate: predicate, + findBeforeFetch: findBeforeFetch, + sortDescriptors: sortDescriptors, + context: context + ).asDomain(context: context) + } + + private func fetch( + entityType: MO.Type, + predicate: NSPredicate? = nil, + findBeforeFetch: Bool = false, + commitChanges: ((MO?) -> Void)? = nil, + context: NSManagedObjectContext + ) -> MO? { + return context.performAndWait { + let managedObject = batchFetch( + entityType: entityType, fetchLimit: 1, + predicate: predicate, findBeforeFetch: findBeforeFetch, + context: context + ).first + commitChanges?(managedObject) + return managedObject + } + } + + private func fetchDomainObject( + entityType: MO.Type, + uid: String, + findBeforeFetch: Bool = false, + commitChanges: ((MO?) -> Void)? = nil, + contextToUse: NSManagedObjectContext? = nil + ) -> MO.DomainType? where MO: DomainConvertible { + let context = contextToUse ?? container.newBackgroundContext() + return context.performAndWait { + fetch( + entityType: entityType, + uid: uid, + findBeforeFetch: findBeforeFetch, + commitChanges: commitChanges, + context: context + )?.asDomain(context: context) + } + } + + func fetchDomainObject( + entityType: MO.Type, + predicate: NSPredicate? = nil, + findBeforeFetch: Bool = false, + commitChanges: ((MO?) -> Void)? = nil, + contextToUse: NSManagedObjectContext? = nil + ) -> MO.DomainType? where MO: DomainConvertible { + + let context = contextToUse ?? container.viewContext + + return fetch( + entityType: entityType, + predicate: predicate, + findBeforeFetch: findBeforeFetch, + commitChanges: commitChanges, + context: context + )?.asDomain(context: context) + } + + private func fetch( + entityType: MO.Type, + uid: String, + findBeforeFetch: Bool = false, + commitChanges: ((MO?) -> Void)? = nil, + context: NSManagedObjectContext + ) -> MO? { + return context.performAndWait { + fetch( + entityType: entityType, + predicate: NSPredicate(format: "uid == %@", uid), + findBeforeFetch: findBeforeFetch, + commitChanges: commitChanges, + context: context + ) + } + } + + private func fetchOrCreate( + entityType: MO.Type, + predicate: NSPredicate? = nil, + commitChanges: ((MO?) -> Void)? = nil, + context: NSManagedObjectContext + ) -> MO { + if let storedDomainObject: MO = fetch( + entityType: entityType, + predicate: predicate, + context: context + ) { + return storedDomainObject + } else { + return context.performAndWait { + let newMO = MO(context: context) + commitChanges?(newMO) + saveContext(context) + return newMO + } + + } + } + + private func fetchOrCreate( + entityType: MO.Type, uid: String, context: NSManagedObjectContext + ) -> MO { + fetchOrCreate( + entityType: entityType, + predicate: NSPredicate(format: "uid == %@", uid), + commitChanges: { $0?.uid = uid }, + context: context + ) + } + + private func fetchOrCreateDomainObject( + entityType: MO.Type, + predicate: NSPredicate? = nil, + commitChanges: ((MO?) -> Void)? = nil, + context: NSManagedObjectContext + ) -> MO.DomainType where MO: DomainConvertible { + fetchOrCreate( + entityType: entityType, + predicate: predicate, + commitChanges: commitChanges, + context: context + ).asDomain(context: context) + } + + public func batchUpdate( + entityType: MO.Type, + predicate: NSPredicate? = nil, + commitChanges: ([MO]) -> Void, + context: NSManagedObjectContext + ) { + context.performAndWait { + commitChanges( + batchFetch( + entityType: entityType, + predicate: predicate, + findBeforeFetch: false, + context: context + )) + saveContext(context) + } + } + + func update( + entityType: MO.Type, + predicate: NSPredicate? = nil, + createIfNil: Bool = true, + commitChanges: @escaping (MO?) -> Void, + contextToUse: NSManagedObjectContext? = nil + ) { + let context = contextToUse ?? container.newBackgroundContext() + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + context.perform { + let storedMO: MO? + + if createIfNil { + storedMO = self.fetchOrCreate( + entityType: entityType, predicate: predicate, context: context) + } else { + storedMO = self.fetch( + entityType: entityType, predicate: predicate, context: context) + } + + if let storedMO = storedMO { + context.performAndWait { + commitChanges(storedMO) + self.saveContext(context) + } + } + } + } + + private func update( + entityType: MO.Type, + uid: String, + createIfNil: Bool = false, + commitChanges: @escaping ((MO) -> Void), + context: NSManagedObjectContext + ) -> MO? { + context.performAndWait { + let storedMO: MO? + if createIfNil { + storedMO = fetchOrCreate( + entityType: entityType, uid: uid, context: context) + } else { + storedMO = fetch(entityType: entityType, uid: uid, context: context) + } + if let storedMO = storedMO { + commitChanges(storedMO) + saveContext(context) + } + + return storedMO + } + } + + public func purgeEntity( + entityType: MO.Type, predicate: NSPredicate? = nil, + context: NSManagedObjectContext + ) { + let fetchRequest: NSFetchRequest = + entityType.fetchRequest() + fetchRequest.predicate = predicate + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + context.performAndWait { + _ = try? context.execute(batchDeleteRequest) + saveContext(context) + } + + } +} + +// MARK: Public API + +extension PersistenceController { + public func save(product: Domain.Product) async { + update(entityType: Product.self, createIfNil: true) { + $0?.id = "\(product.id)" + $0?.title = product.title + $0?.price = product.price + $0?.image = product.image + // $0?.rating = product.rating.toManagedObject(in: persistenceController.container.newBackgroundContext()) + $0?.category = product.category + $0?.productDescription = product.description + } + } + + public func getProduct(id: Int) async throws -> Domain.Product? { + fetchDomainObject(entityType: Product.self) + } +} + +// MARK: Definition + +protocol ManagedObjectConvertible { + associatedtype ManagedObject: NSManagedObject, DomainConvertible + + @discardableResult + func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject +} + +protocol Identifiable: NSManagedObject { + var uid: String { get set } +} diff --git a/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Repository/PersistentRepository.swift b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Repository/PersistentRepository.swift new file mode 100644 index 0000000..31b2cef --- /dev/null +++ b/{{cookiecutter.app_name}}/PersistentPlatform/Sources/PersistentPlatform/Repository/PersistentRepository.swift @@ -0,0 +1,59 @@ +// +// PersistentRepository.swift +// PersistentPlatform +// +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. +// + +import Domain + +public struct PersistentRepository: Repository { + + private let persistenceController: PersistenceController + + private init(persistenceController: PersistenceController) { + self.persistenceController = persistenceController + } + + public func prepare() async { + do { + try await persistenceController.prepare() + } catch { + fatalError(error.localizedDescription) + } + } + + public func create(input: Domain.Product) async throws { + await persistenceController.update( + entityType: Product.self, createIfNil: true + ) { + $0?.id = "\(input.id)" + $0?.title = input.title + $0?.price = input.price + $0?.image = input.image + // $0?.rating = product.rating.toManagedObject(in: persistenceController.container.newBackgroundContext()) + $0?.category = input.category + $0?.productDescription = input.description + } + } + + public func read(input: Int) async throws -> Domain.Product? { + await persistenceController.fetchDomainObject(entityType: Product.self) + } + + public func update(input: Int) async throws -> Product? { + fatalError("Unimplemented") + } + + public func delete(input: Int) async throws -> Product? { + fatalError("Unimplemented") + } +} + +extension PersistentRepository { + public static var live = PersistentRepository( + persistenceController: PersistenceController.shared) + public static var stubbed = PersistentRepository( + persistenceController: PersistenceController.shared) +} diff --git a/{{cookiecutter.app_name}}/Utilities/Package.swift b/{{cookiecutter.app_name}}/Utilities/Package.swift index fcfc1a3..c642d3a 100644 --- a/{{cookiecutter.app_name}}/Utilities/Package.swift +++ b/{{cookiecutter.app_name}}/Utilities/Package.swift @@ -4,19 +4,17 @@ import PackageDescription let package = Package( - name: "Utilities", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library( - name: "Utilities", - targets: ["Utilities"]), - ], - dependencies: [ - - ], - targets: [ - .target( - name: "Utilities", - dependencies: []) - ] + name: "Utilities", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library( + name: "Utilities", + targets: ["Utilities"]) + ], + dependencies: [], + targets: [ + .target( + name: "Utilities", + dependencies: []) + ] ) diff --git a/{{cookiecutter.app_name}}/Utilities/Sources/Utilities/Logger.swift b/{{cookiecutter.app_name}}/Utilities/Sources/Utilities/Logger.swift index 38f5381..1338dbf 100644 --- a/{{cookiecutter.app_name}}/Utilities/Sources/Utilities/Logger.swift +++ b/{{cookiecutter.app_name}}/Utilities/Sources/Utilities/Logger.swift @@ -9,41 +9,62 @@ import Foundation public enum Logger { - - private static let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - return formatter - }() - - public static func info(_ messages: Any?..., file: String = #file, function: String = #function, line: Int = #line) { - printMessage(messages, state: "ℹ️ INFO", file: file, function: function, line: line) - } - - public static func error(_ messages: Any?..., file: String = #file, function: String = #function, line: Int = #line) { - printMessage(messages, state: "💥 ERROR", file: file, function: function, line: line) - } - - public static func warning(_ messages: Any?..., file: String = #file, function: String = #function, line: Int = #line) { - printMessage(messages, state: "⚠️ WARNING", file: file, function: function, line: line) - } - - public static func success(_ messages: Any?..., file: String = #file, function: String = #function, line: Int = #line) { - printMessage(messages, state: "✅ SUCCESS", file: file, function: function, line: line) - } - - private static func printMessage(_ messages: Any?..., state: String, file: String, function: String, line: Int) { -#if RELEASE - return -#endif - - let dateString = dateFormatter.string(from: Date()) - print("\(dateString) - \(state) \(sourceFileName(file)).\(function):\(line)", messages) - - } - - private static func sourceFileName(_ filePath: String) -> String { - let components = filePath.components(separatedBy: "/") - return components.isEmpty ? "" : (components.last ?? "") - } + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return formatter + }() + + public static func info( + _ messages: Any?..., file: String = #file, function: String = #function, + line: Int = #line + ) { + printMessage( + messages, state: "ℹ️ INFO", file: file, function: function, line: line) + } + + public static func error( + _ messages: Any?..., file: String = #file, function: String = #function, + line: Int = #line + ) { + printMessage( + messages, state: "💥 ERROR", file: file, function: function, line: line) + } + + public static func warning( + _ messages: Any?..., file: String = #file, function: String = #function, + line: Int = #line + ) { + printMessage( + messages, state: "⚠️ WARNING", file: file, function: function, line: line) + } + + public static func success( + _ messages: Any?..., file: String = #file, function: String = #function, + line: Int = #line + ) { + printMessage( + messages, state: "✅ SUCCESS", file: file, function: function, line: line) + } + + private static func printMessage( + _ messages: Any?..., state: String, file: String, function: String, + line: Int + ) { + #if RELEASE + return + #endif + + let dateString = dateFormatter.string(from: Date()) + print( + "\(dateString) - \(state) \(sourceFileName(file)).\(function):\(line)", + messages) + + } + + private static func sourceFileName(_ filePath: String) -> String { + let components = filePath.components(separatedBy: "/") + return components.isEmpty ? "" : (components.last ?? "") + } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj index 8a9fd74..da4ad45 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2F0D066C2B76030C00442618 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 2F0D066B2B76030C00442618 /* App */; }; + 2F0D066E2B76035F00442618 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 2F0D066D2B76035F00442618 /* ComposableArchitecture */; }; 2F1CB98A291A097F0033C297 /* BuildConfiguration.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F1CB989291A097F0033C297 /* BuildConfiguration.plist */; }; 2F7B0E5A273AD7AC00C68B1F /* MainApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7B0E59273AD7AC00C68B1F /* MainApp.swift */; }; 2F7B0E6B273AD7AD00C68B1F /* __cookiecutter_app_name__Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7B0E6A273AD7AD00C68B1F /* __cookiecutter_app_name__Tests.swift */; }; @@ -63,6 +65,7 @@ 2F7B0E70273AD7AD00C68B1F /* {{cookiecutter.app_name}}UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "{{cookiecutter.app_name}}UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 2F7B0E74273AD7AD00C68B1F /* __cookiecutter_app_name__UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = __cookiecutter_app_name__UITests.swift; sourceTree = ""; }; 2F7B0E76273AD7AD00C68B1F /* __cookiecutter_app_name__UITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = __cookiecutter_app_name__UITestsLaunchTests.swift; sourceTree = ""; }; + 2F88959D2B75F8FB0047F158 /* PersistentPlatform */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PersistentPlatform; sourceTree = ""; }; 2F9DFD462990EA1800611A47 /* Utilities */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Utilities; sourceTree = ""; }; 2FBF5D1D2A73C01B00527C7D /* Common */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Common; sourceTree = ""; }; 6782D5572912526F0015FCBB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -80,8 +83,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B203074B2B207D4E00EBA314 /* ComposableArchitecture in Frameworks */, - B20307482B207C8600EBA314 /* App in Frameworks */, + 2F0D066E2B76035F00442618 /* ComposableArchitecture in Frameworks */, + 2F0D066C2B76030C00442618 /* App in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -120,6 +123,7 @@ 2F7B0E4D273AD7AC00C68B1F = { isa = PBXGroup; children = ( + 2F88959D2B75F8FB0047F158 /* PersistentPlatform */, 2FBF5D1D2A73C01B00527C7D /* Common */, 2F4989C3298E8F470007A245 /* Domain */, 2F4989C5298E8F9E0007A245 /* Features */, @@ -252,8 +256,8 @@ ); name = "{{cookiecutter.app_name}}"; packageProductDependencies = ( - B20307472B207C8600EBA314 /* App */, - B203074A2B207D4E00EBA314 /* ComposableArchitecture */, + 2F0D066B2B76030C00442618 /* App */, + 2F0D066D2B76035F00442618 /* ComposableArchitecture */, ); productName = "{{cookiecutter.app_name}}"; productReference = 2F7B0E56273AD7AC00C68B1F /* {{cookiecutter.app_name}}.app */; @@ -1332,13 +1336,13 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B20307472B207C8600EBA314 /* App */ = { + 2F0D066B2B76030C00442618 /* App */ = { isa = XCSwiftPackageProductDependency; productName = App; }; - B203074A2B207D4E00EBA314 /* ComposableArchitecture */ = { + 2F0D066D2B76035F00442618 /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; - package = B20307492B207D4E00EBA314 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + package = 6782D5B32912554D0015FCBB /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; productName = ComposableArchitecture; }; /* End XCSwiftPackageProductDependency section */ diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d114f95..be0fc9c 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "b2fa556e4e48cbf06cf8c63def138c98f4b811fa", - "version" : "5.8.0" + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca", - "version" : "1.1.2" + "revision" : "e072139e13f2f3e582251b49835abcf3421ac69a", + "version" : "1.2.3" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version" : "1.0.0" + "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", + "version" : "1.1.2" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864", - "version" : "1.1.2" + "revision" : "09e49dd46932adfe80ce672b4b3772d79ee6c21a", + "version" : "1.2.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", - "version" : "1.2.0" + "revision" : "d9e72f3083c08375794afa216fb2f89c0114f303", + "version" : "1.2.1" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" + "revision" : "b58e6627149808b40634c4552fcf2f44d0b3ca87", + "version" : "1.1.0" } } ], diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift index a5ef175..710e503 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift @@ -13,13 +13,14 @@ import SwiftUI #warning("Please rename to your app name") @main struct MainApp: App { - var body: some Scene { - let store = Store(initialState: AppFeature.State(), - reducer: { AppFeature() } - ) - - WindowGroup { - AppView(store: store) - } + var body: some Scene { + let store = Store( + initialState: AppFeature.State(), + reducer: { AppFeature() } + ) + + WindowGroup { + AppView(store: store) } + } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Resources/Generated/BuildConfiguration.plist b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Resources/Generated/BuildConfiguration.plist index 1f4e98c..6e2d5fc 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Resources/Generated/BuildConfiguration.plist +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Resources/Generated/BuildConfiguration.plist @@ -3,7 +3,7 @@ name - Development + Production baseURL https://gtrend.yapie.me testFlags diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}Tests/__cookiecutter_app_name__Tests.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}Tests/__cookiecutter_app_name__Tests.swift index 4a35b1a..02b7956 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}Tests/__cookiecutter_app_name__Tests.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}Tests/__cookiecutter_app_name__Tests.swift @@ -7,29 +7,30 @@ // import XCTest + @testable import __cookiecutter_app_name__ #warning("Please rename to your app name") class AppTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. } + } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITests.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITests.swift index 86618f6..8f973ec 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITests.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITests.swift @@ -11,34 +11,34 @@ import XCTest #warning("Please rename to your app name") class AppUITests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } } + } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITestsLaunchTests.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITestsLaunchTests.swift index 0b43a8d..313845f 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITestsLaunchTests.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}UITests/__cookiecutter_app_name__UITestsLaunchTests.swift @@ -11,24 +11,24 @@ import XCTest #warning("Please rename to your app name") class AppUITestsLaunchTests: XCTestCase { - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } - override func setUpWithError() throws { - continueAfterFailure = false - } + override func setUpWithError() throws { + continueAfterFailure = false + } - func testLaunch() throws { - let app = XCUIApplication() - app.launch() + func testLaunch() throws { + let app = XCUIApplication() + app.launch() - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } } From 624493033fe2ccd8c1bc6f91468c320db4237d7d Mon Sep 17 00:00:00 2001 From: Mohammed Rokon Uddin Date: Fri, 9 Feb 2024 14:57:18 +0600 Subject: [PATCH 2/4] chore: change copy right --- .../___FILEBASENAME___Feature.swift | 102 +++++++++--------- .../___FILEBASENAME___View.swift | 24 ++--- .../Sources/Counter/CounterFeature.swift | 4 +- .../Sources/Counter/CounterView.swift | 4 +- 4 files changed, 67 insertions(+), 67 deletions(-) diff --git a/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___Feature.swift b/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___Feature.swift index c892922..88f7730 100755 --- a/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___Feature.swift +++ b/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___Feature.swift @@ -6,61 +6,61 @@ //___COPYRIGHT___ // -import Domain import Commons -import Foundation import ComposableArchitecture +import Domain +import Foundation + +public struct ___VARIABLE_moduleName___Feature: Reducer { + + public struct State: Equatable { + public init() {} + } + + public enum Action: BaseAction { + + enum ViewAction: Equatable { + case onAppear -public struct ___VARIABLE_moduleName___Feature: Reducer { - - public struct State: Equatable { - public init() {} } - - public enum Action: BaseAction { - - enum ViewAction: Equatable { - case onAppear - - } - - enum InlyingAction: Equatable { - - } - - enum DelegateAction: Equatable { - - } - - case view(ViewAction) - case inlying(InlyingAction) - case delegate(DelegateAction) + + enum InlyingAction: Equatable { + } - - public func reduce(into state: inout State, action: Action) -> Effect { - switch action { - case .view(let action): - switch action { - case .onAppear: - return .none - - } - case .inlying(let action): - switch action { - - } - case .delegate(let action): - switch action { - - } - } + + enum DelegateAction: Equatable { + } - -// Remove commented code if not needed -// public var body: some Reducer { -// -// Reduce { state, action in -// -// } -// } + + case view(ViewAction) + case inlying(InlyingAction) + case delegate(DelegateAction) + } + + public func reduce(into state: inout State, action: Action) -> Effect { + switch action { + case .view(let action): + switch action { + case .onAppear: + return .none + + } + case .inlying(let action): + switch action { + + } + case .delegate(let action): + switch action { + + } + } + } + + // Remove commented code if not needed + // public var body: some Reducer { + // + // Reduce { state, action in + // + // } + // } } diff --git a/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___View.swift b/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___View.swift index 1159475..bcc2715 100755 --- a/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___View.swift +++ b/TCA.xctemplate/___FILEBASENAME___/___FILEBASENAME___View.swift @@ -6,24 +6,24 @@ //___COPYRIGHT___ // -import SwiftUI import Commons -import Resources import ComposableArchitecture +import Resources +import SwiftUI public struct ___VARIABLE_moduleName___View: View { - let store: StoreOf<___VARIABLE_moduleName___Feature> + let store: StoreOf<___VARIABLE_moduleName___Feature> - public init(store: StoreOf<___VARIABLE_moduleName___Feature>) { - self.store = store - } + public init(store: StoreOf<___VARIABLE_moduleName___Feature>) { + self.store = store + } - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Text("Hello World!!!") - .onAppear { - viewStore.send(.onAppear) - } + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Text("Hello World!!!") + .onAppear { + viewStore.send(.onAppear) } } + } } diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift index b0c0102..b242f46 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift @@ -2,8 +2,8 @@ // CounterFeature.swift // Features // -// Created by Rokon on 24/01/2024. -// Copyright © 2024 MLBD. All rights reserved. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Common diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift index 5871b6d..bf0234d 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift @@ -2,8 +2,8 @@ // CounterView.swift // Features // -// Created by Rokon on 24/01/2024. -// Copyright © 2024 MLBD. All rights reserved. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Common From b69a4542ac1385974742a3df145d59544be4b6f7 Mon Sep 17 00:00:00 2001 From: Mohammed Rokon Uddin Date: Fri, 9 Feb 2024 15:46:58 +0600 Subject: [PATCH 3/4] chore: rename property --- .../Features/Sources/App/AppClient.swift | 2 +- .../Features/Sources/App/AppFeature.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift index 8919e67..574f364 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppClient.swift @@ -30,7 +30,7 @@ extension AppClient { } extension DependencyValues { - public var productClient: AppClient { + public var appClient: AppClient { get { self[AppClient.self] } set { self[AppClient.self] = newValue } } diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift index 3dcc0af..536ab2c 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift @@ -13,7 +13,7 @@ import Domain public struct AppFeature: FeatureReducer { - @Dependency(\.productClient) var productClient + @Dependency(\.appClient) var appClient public init() {} public struct State: Equatable, Hashable { @@ -48,12 +48,12 @@ public struct AppFeature: FeatureReducer { switch viewAction { case .onAppear: return .run { send in - await productClient.prepare(Void()) + await appClient.prepare(Void()) await send( .internal( .productResponse( TaskResult { - try await productClient.product(1) + try await appClient.product(1) }))) } case .showSheet: @@ -67,7 +67,7 @@ public struct AppFeature: FeatureReducer { case .save: return .run { [product = state.product] send in do { - try await productClient.save(product!) + try await appClient.save(product!) } catch { } From ca76092529b99ce6aa663238eff986c5034f7c97 Mon Sep 17 00:00:00 2001 From: Mohammed Rokon Uddin Date: Fri, 9 Feb 2024 16:51:55 +0600 Subject: [PATCH 4/4] chore: remove commented codes --- .../Common/Sources/Common/BaseAction.swift | 22 ----------------- .../Network/NetworkingType.swift | 24 ------------------- 2 files changed, 46 deletions(-) delete mode 100644 {{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift deleted file mode 100644 index 3a60272..0000000 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// BaseAction.swift -// Common -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -// keep Actions organized and their intent explicit -// https://github.com/pointfreeco/swift-composable-architecture/discussions/1440 - -public protocol BaseAction { - associatedtype ViewAction - associatedtype DelegateAction - associatedtype InlyingAction - - static func view(_: ViewAction) -> Self - static func delegate(_: DelegateAction) -> Self - static func inlying(_: InlyingAction) -> Self -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift index 757f50b..388d844 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Sources/NetworkPlatform/Network/NetworkingType.swift @@ -57,30 +57,6 @@ extension NetworkingType { } } } - - // func requestObject(_ target: T, type: Element.Type) -> AnyPublisher { - // return provider.request(target) - // .filterSuccessfulStatusCodes() - // .map(Element.self) - // .mapError { NetworkingError.error($0.localizedDescription) } - // // TODO: fetch from cache - // .tryCatch { _ in self.coreDataManager.localRandom() } - // .mapError { NetworkingError.error($0.localizedDescription) } - // .eraseToAnyPublisher() - // } - - func requestArray(_ target: T, type _: Element.Type) - -> AnyPublisher<[Element], NetworkingError> - { - provider.request(target) - .filterSuccessfulStatusCodes() - .map([Element].self) - .mapError { NetworkingError.error($0.localizedDescription) } - // TODO: fetch from cache - // .tryCatch { _ in self.coreDataManager.localRandom() } - .mapError { NetworkingError.error($0.localizedDescription) } - .eraseToAnyPublisher() - } } // MARK: - Provider support