diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..95c4320
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..0930c84
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,70 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "PathKit",
+ "repositoryURL": "https://github.com/kylef/PathKit.git",
+ "state": {
+ "branch": null,
+ "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0",
+ "version": "0.9.2"
+ }
+ },
+ {
+ "package": "PromiseKit",
+ "repositoryURL": "https://github.com/mxcl/PromiseKit",
+ "state": {
+ "branch": null,
+ "revision": "f14f16cc2602afec1030e4f492100d6d43dca544",
+ "version": "6.13.1"
+ }
+ },
+ {
+ "package": "Rainbow",
+ "repositoryURL": "https://github.com/onevcat/Rainbow",
+ "state": {
+ "branch": null,
+ "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155",
+ "version": "3.1.5"
+ }
+ },
+ {
+ "package": "Spectre",
+ "repositoryURL": "https://github.com/kylef/Spectre.git",
+ "state": {
+ "branch": null,
+ "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5",
+ "version": "0.9.0"
+ }
+ },
+ {
+ "package": "Stencil",
+ "repositoryURL": "https://github.com/kylef/Stencil.git",
+ "state": {
+ "branch": null,
+ "revision": "0e9a78d6584e3812cd9c09494d5c7b483e8f533c",
+ "version": "0.13.1"
+ }
+ },
+ {
+ "package": "StencilSwiftKit",
+ "repositoryURL": "https://github.com/SwiftGen/StencilSwiftKit.git",
+ "state": {
+ "branch": null,
+ "revision": "dbf02bd6afe71b65ead2bd250aaf974abc688094",
+ "version": "2.7.2"
+ }
+ },
+ {
+ "package": "SwiftCLI",
+ "repositoryURL": "https://github.com/jakeheis/SwiftCLI",
+ "state": {
+ "branch": null,
+ "revision": "c72c4564f8c0a24700a59824880536aca45a4cae",
+ "version": "6.0.1"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..b6d2131
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,41 @@
+// swift-tools-version:5.1
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "AnalyticsGen",
+ platforms: [
+ .macOS(.v10_12)
+ ],
+ dependencies: [
+ .package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0"),
+ .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.2"),
+ .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0"),
+ .package(url: "https://github.com/mxcl/PromiseKit", from: "6.8.0"),
+ .package(url: "https://github.com/kylef/Stencil.git", from: "0.13.0"),
+ .package(url: "https://github.com/SwiftGen/StencilSwiftKit.git", from: "2.7.2")
+ ],
+ targets: [
+ .target(
+ name: "AnalyticsGen",
+ dependencies: [
+ "AnalyticsGenTools",
+ "SwiftCLI",
+ "PathKit",
+ "Rainbow",
+ "PromiseKit",
+ "Stencil",
+ "StencilSwiftKit"
+ ]
+ ),
+ .target(
+ name: "AnalyticsGenTools",
+ dependencies: ["SwiftCLI", "PathKit"]
+ ),
+ .testTarget(
+ name: "AnalyticsGenTests",
+ dependencies: ["AnalyticsGen"]
+ )
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cfc70dd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# AnalyticsGen
+
+A description of this package.
diff --git a/Sources/AnalyticsGen/Coders/Decodable/DecodableCoder.swift b/Sources/AnalyticsGen/Coders/Decodable/DecodableCoder.swift
new file mode 100644
index 0000000..ee47bf9
--- /dev/null
+++ b/Sources/AnalyticsGen/Coders/Decodable/DecodableCoder.swift
@@ -0,0 +1,44 @@
+//
+// DecodableCoder.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+protocol DecodableCoder {
+
+ // MARK: - Instance Methods
+
+ func encode(_ object: T, encoder: JSONEncoder) throws -> [String: Any]
+ func encode(_ array: [T], encoder: JSONEncoder) throws -> [[String: Any]]
+
+ func decode(from json: [String: Any], decoder: JSONDecoder) throws -> T
+ func decode(from json: [[String: Any]], decoder: JSONDecoder) throws -> [T]
+}
+
+// MARK: -
+
+extension DecodableCoder {
+
+ // MARK: - Instance Methods
+
+ func encode(_ object: T, encoder: JSONEncoder = JSONEncoder()) throws -> [String: Any] {
+ return try self.encode(object, encoder: encoder)
+ }
+
+ func encode(_ array: [T], encoder: JSONEncoder = JSONEncoder()) throws -> [[String: Any]] {
+ return try self.encode(array, encoder: encoder)
+ }
+
+ // MARK: -
+
+ func decode(from json: [String: Any], decoder: JSONDecoder = JSONDecoder()) throws -> T {
+ return try self.decode(from: json, decoder: decoder)
+ }
+
+ func decode(from json: [[String: Any]], decoder: JSONDecoder = JSONDecoder()) throws -> [T] {
+ return try self.decode(from: json, decoder: decoder)
+ }
+}
diff --git a/Sources/AnalyticsGen/Coders/Decodable/DefualtDecodableCoder.swift b/Sources/AnalyticsGen/Coders/Decodable/DefualtDecodableCoder.swift
new file mode 100644
index 0000000..2487ee6
--- /dev/null
+++ b/Sources/AnalyticsGen/Coders/Decodable/DefualtDecodableCoder.swift
@@ -0,0 +1,59 @@
+//
+// DefualtDecodableCoder.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+final class DefaultDecodableCoder: DecodableCoder {
+
+ // MARK: - DecodableCoder
+
+ func encode(_ object: T, encoder: JSONEncoder) throws -> [String: Any] {
+ let data = try encoder.encode(object)
+ let object = try JSONSerialization.jsonObject(with: data)
+
+ guard let json = object as? [String: Any] else {
+ let context = DecodingError.Context(
+ codingPath: [],
+ debugDescription: "Deserialized object is not a dictionary"
+ )
+
+ throw DecodingError.typeMismatch(type(of: object), context)
+ }
+
+ return json
+ }
+
+ func encode(_ array: [T], encoder: JSONEncoder) throws -> [[String: Any]] {
+ let data = try encoder.encode(array)
+ let object = try JSONSerialization.jsonObject(with: data)
+
+ guard let json = object as? [[String: Any]] else {
+ let context = DecodingError.Context(
+ codingPath: [],
+ debugDescription: "Deserialized object is not a array"
+ )
+
+ throw DecodingError.typeMismatch(type(of: object), context)
+ }
+
+ return json
+ }
+
+ // MARK: -
+
+ func decode(from json: [String: Any], decoder: JSONDecoder) throws -> T {
+ let data = try JSONSerialization.data(withJSONObject: json)
+
+ return try decoder.decode(from: data)
+ }
+
+ func decode(from json: [[String: Any]], decoder: JSONDecoder) throws -> [T] {
+ let data = try JSONSerialization.data(withJSONObject: json)
+
+ return try decoder.decode(from: data)
+ }
+}
diff --git a/Sources/AnalyticsGen/Coders/Tracker/DefaultTrackerCoder.swift b/Sources/AnalyticsGen/Coders/Tracker/DefaultTrackerCoder.swift
new file mode 100644
index 0000000..7bfedf1
--- /dev/null
+++ b/Sources/AnalyticsGen/Coders/Tracker/DefaultTrackerCoder.swift
@@ -0,0 +1,29 @@
+//
+// DefaultTrackerCoder.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+final class DefaultTrackerCoder: TrackerCoder {
+
+ // MARK: - Instance Properties
+
+ let decodableCoder: DecodableCoder
+
+ // MARK: - Initializers
+
+ init(decodableCoder: DecodableCoder) {
+ self.decodableCoder = decodableCoder
+ }
+
+ // MARK: - TrackerCoder
+
+ func encode(trackers: [Tracker]) -> [String: Any] {
+ let trackersJSON = (try? self.decodableCoder.encode(trackers)) ?? []
+
+ return ["trackers": trackersJSON]
+ }
+}
diff --git a/Sources/AnalyticsGen/Coders/Tracker/TrackerCoder.swift b/Sources/AnalyticsGen/Coders/Tracker/TrackerCoder.swift
new file mode 100644
index 0000000..8e102c1
--- /dev/null
+++ b/Sources/AnalyticsGen/Coders/Tracker/TrackerCoder.swift
@@ -0,0 +1,15 @@
+//
+// TrackerCoder.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+protocol TrackerCoder {
+
+ // MARK: - Instance Methods
+
+ func encode(trackers: [Tracker]) -> [String: Any]
+}
diff --git a/Sources/AnalyticsGen/Commands/AsyncExecutableCommand.swift b/Sources/AnalyticsGen/Commands/AsyncExecutableCommand.swift
new file mode 100644
index 0000000..155b286
--- /dev/null
+++ b/Sources/AnalyticsGen/Commands/AsyncExecutableCommand.swift
@@ -0,0 +1,45 @@
+//
+// AsyncExecutableCommand.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import SwiftCLI
+import Rainbow
+
+protocol AsyncExecutableCommand: Command {
+
+ // MARK: - Instance Methods
+
+ func executeAsyncAndExit() throws
+
+ func fail(message: String) -> Never
+ func succeed(message: String) -> Never
+}
+
+// MARK: -
+
+extension AsyncExecutableCommand {
+
+ // MARK: - Instance Methods
+
+ func execute() throws {
+ try self.executeAsyncAndExit()
+
+ RunLoop.main.run()
+ }
+
+ func fail(message: String) -> Never {
+ self.stderr <<< message.red
+
+ exit(EXIT_FAILURE)
+ }
+
+ func succeed(message: String) -> Never {
+ self.stdout <<< message.green
+
+ exit(EXIT_SUCCESS)
+ }
+}
diff --git a/Sources/AnalyticsGen/Commands/GenerationConfigurableCommand.swift b/Sources/AnalyticsGen/Commands/GenerationConfigurableCommand.swift
new file mode 100644
index 0000000..e43c509
--- /dev/null
+++ b/Sources/AnalyticsGen/Commands/GenerationConfigurableCommand.swift
@@ -0,0 +1,30 @@
+//
+// GenerationConfigurableCommand.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+import SwiftCLI
+
+protocol GenerationConfigurableCommand: Command {
+
+ // MARK: - Instance Properties
+
+ var template: Key { get }
+ var destination: Key { get }
+
+ var generationConfiguration: GenerationConfiguration { get }
+}
+
+// MARK: -
+
+extension GenerationConfigurableCommand {
+
+ // MARK: - Instance Properties
+
+ var generationConfiguration: GenerationConfiguration {
+ GenerationConfiguration(template: template.value, templateOptions: nil, destination: self.destination.value)
+ }
+}
diff --git a/Sources/AnalyticsGen/Commands/TrackersCommand.swift b/Sources/AnalyticsGen/Commands/TrackersCommand.swift
new file mode 100644
index 0000000..15c5099
--- /dev/null
+++ b/Sources/AnalyticsGen/Commands/TrackersCommand.swift
@@ -0,0 +1,62 @@
+//
+// TrackersCommand.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+import SwiftCLI
+import PromiseKit
+
+final class TrackersCommand: AsyncExecutableCommand, GenerationConfigurableCommand {
+
+ // MARK: - Instance Properties
+
+ let generator: TrackersGenerator
+
+ // MARK: -
+
+ let name = "trackers"
+ let shortDescription = "Generate code for analytics trackers from server"
+
+ // MARK: -
+
+ var template = Key(
+ "--template",
+ "-t",
+ description: """
+ Path to the template file.
+ If no template is passed a default template will be used.
+ """
+ )
+
+ let destination = Key(
+ "--destination",
+ "-d",
+ description: """
+ The path to the file to generate.
+ By default, generated code will be printed on stdout.
+ """
+ )
+
+ // MARK: - Initializers
+
+ init(generator: TrackersGenerator) {
+ self.generator = generator
+ }
+
+ // MARK: - AsyncExecutableCommand
+
+ func executeAsyncAndExit() throws {
+ firstly {
+ self.generator.generate(configuration: self.generationConfiguration)
+ }.done {
+ self.succeed(message: "Analytics trackers generated successfully!")
+ }.catch { error in
+ self.fail(message: "Failed to generate analytics trackers: \(error)")
+ }
+ }
+}
+
+
diff --git a/Sources/AnalyticsGen/Dependencies.swift b/Sources/AnalyticsGen/Dependencies.swift
new file mode 100644
index 0000000..7577137
--- /dev/null
+++ b/Sources/AnalyticsGen/Dependencies.swift
@@ -0,0 +1,43 @@
+//
+// Dependencies.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+import AnalyticsGenTools
+
+enum Dependencies {
+
+ // MARK: - Type Properties
+
+ static let analyticsGenHTTPService: AnalyticsGenHTTPService = HTTPService()
+
+ // MARK: -
+
+ static let analyticsGenAPIProvider: AnalyticsGenAPIProvider = DefaultAnalyticsGenAPIProvider(
+ httpService: Dependencies.analyticsGenHTTPService
+ )
+
+ static let analyticsTrackersProvider: AnalyticsTrackersProvider = DefaultAnalyticsTrackersProvider(
+ apiProvider: Dependencies.analyticsGenAPIProvider
+ )
+
+ // MARK: -
+
+ static let decodableTracker: DecodableCoder = DefaultDecodableCoder()
+ static let trackerCoder: TrackerCoder = DefaultTrackerCoder(decodableCoder: Dependencies.decodableTracker)
+
+ // MARK: -
+
+ static let templateRenderer: TemplateRenderer = DefaultTemplateRenderer()
+
+ // MARK: -
+
+ static let trackersGenerator: TrackersGenerator = DefaultTrackersGenerator(
+ analyticsTrackersProvider: Dependencies.analyticsTrackersProvider,
+ trackerCoder: Dependencies.trackerCoder,
+ templateRenderer: Dependencies.templateRenderer
+ )
+}
diff --git a/Sources/AnalyticsGen/Generators/GenerationParametersResolving.swift b/Sources/AnalyticsGen/Generators/GenerationParametersResolving.swift
new file mode 100644
index 0000000..de24f39
--- /dev/null
+++ b/Sources/AnalyticsGen/Generators/GenerationParametersResolving.swift
@@ -0,0 +1,55 @@
+//
+// GenerationParametersResolving.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+protocol GenerationParametersResolving {
+
+ // MARK: - Instance Properties
+
+ var defaultTemplateType: RenderTemplateType { get }
+ var defaultDestination: RenderDestination { get }
+
+ // MARK: - Instance Methods
+
+ func resolveGenerationParameters(from configuration: GenerationConfiguration) throws -> GenerationParameters
+}
+
+// MARK: -
+
+extension GenerationParametersResolving {
+
+ // MARK: - Instance Methods
+
+ func resolveTemplateType(from configuration: GenerationConfiguration) -> RenderTemplateType {
+ if let templatePath = configuration.template {
+ return .custom(path: templatePath)
+ } else {
+ return self.defaultTemplateType
+ }
+ }
+
+ func resolveDestination(from configuration: GenerationConfiguration) -> RenderDestination {
+ if let destinationPath = configuration.destination {
+ return .file(path: destinationPath)
+ } else {
+ return self.defaultDestination
+ }
+ }
+
+ // MARK: -
+
+ func resolveGenerationParameters(from configuration: GenerationConfiguration) throws -> GenerationParameters {
+ let templateType = self.resolveTemplateType(from: configuration)
+ let destination = self.resolveDestination(from: configuration)
+
+ let template = RenderTemplate(type: templateType, options: configuration.templateOptions ?? [:])
+ let render = RenderParameters(template: template, destination: destination)
+
+ return GenerationParameters(render: render)
+ }
+}
diff --git a/Sources/AnalyticsGen/Generators/TrackersGenerator/DefaultTrackersGenerator.swift b/Sources/AnalyticsGen/Generators/TrackersGenerator/DefaultTrackersGenerator.swift
new file mode 100644
index 0000000..a510beb
--- /dev/null
+++ b/Sources/AnalyticsGen/Generators/TrackersGenerator/DefaultTrackersGenerator.swift
@@ -0,0 +1,62 @@
+//
+// DefaultTrackersGenerator.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import AnalyticsGenTools
+import PromiseKit
+
+final class DefaultTrackersGenerator: TrackersGenerator, GenerationParametersResolving {
+
+ // MARK: - Instance Properties
+
+ let analyticsTrackersProvider: AnalyticsTrackersProvider
+ let trackerCoder: TrackerCoder
+ let templateRenderer: TemplateRenderer
+
+ // MARK: -
+
+ let defaultTemplateType: RenderTemplateType = .native(name: "Trackers")
+ let defaultDestination: RenderDestination = .console
+
+ // MARK: - Initializers
+
+ init(
+ analyticsTrackersProvider: AnalyticsTrackersProvider,
+ trackerCoder: TrackerCoder,
+ templateRenderer: TemplateRenderer
+ ) {
+ self.analyticsTrackersProvider = analyticsTrackersProvider
+ self.trackerCoder = trackerCoder
+ self.templateRenderer = templateRenderer
+ }
+
+ // MARK: - Instance Methods
+
+ private func generate(parameters: GenerationParameters) -> Promise {
+ return firstly {
+ self.analyticsTrackersProvider.fetchAnalyticsTrackers()
+ }.done { trackers in
+ let context = self.trackerCoder.encode(trackers: trackers)
+
+ try self.templateRenderer.render(
+ template: parameters.render.template,
+ to: parameters.render.destination,
+ context: context
+ )
+ }
+ }
+
+ // MARK: - TrackersGenerator
+
+ func generate(configuration: GenerationConfiguration) -> Promise {
+ return perform(on: DispatchQueue.global(qos: .userInitiated)) {
+ try self.resolveGenerationParameters(from: configuration)
+ }.then { parameters in
+ self.generate(parameters: parameters)
+ }
+ }
+}
diff --git a/Sources/AnalyticsGen/Generators/TrackersGenerator/TrackersGenerator.swift b/Sources/AnalyticsGen/Generators/TrackersGenerator/TrackersGenerator.swift
new file mode 100644
index 0000000..b47998e
--- /dev/null
+++ b/Sources/AnalyticsGen/Generators/TrackersGenerator/TrackersGenerator.swift
@@ -0,0 +1,16 @@
+//
+// TrackersGenerator.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import PromiseKit
+
+protocol TrackersGenerator {
+
+ // MARK: - Instance Methods
+
+ func generate(configuration: GenerationConfiguration) -> Promise
+}
diff --git a/Sources/AnalyticsGen/Models/Configuration/GenerationConfiguration.swift b/Sources/AnalyticsGen/Models/Configuration/GenerationConfiguration.swift
new file mode 100644
index 0000000..f56b29c
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Configuration/GenerationConfiguration.swift
@@ -0,0 +1,46 @@
+//
+// GenerationConfiguration.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 18/01/2020.
+//
+
+import Foundation
+import AnalyticsGenTools
+
+struct GenerationConfiguration: Decodable {
+
+ // MARK: - Nested Types
+
+ private enum CodingKeys: String, CodingKey {
+ case template
+ case templateOptions
+ case destination
+ }
+
+ // MARK: - Instance Properties
+
+ let template: String?
+ let templateOptions: [String: Any]?
+ let destination: String?
+
+ // MARK: - Initializers
+
+ init(template: String?, templateOptions: [String: Any]?, destination: String?) {
+ self.template = template
+ self.templateOptions = templateOptions
+ self.destination = destination
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.template = try container.decodeIfPresent(forKey: .template)
+
+ self.templateOptions = try container
+ .decodeIfPresent([String: AnyCodable].self, forKey: .templateOptions)?
+ .mapValues { $0.value }
+
+ self.destination = try container.decodeIfPresent(forKey: .destination)
+ }
+}
diff --git a/Sources/AnalyticsGen/Models/Event.swift b/Sources/AnalyticsGen/Models/Event.swift
new file mode 100644
index 0000000..dcd7407
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Event.swift
@@ -0,0 +1,18 @@
+//
+// Event.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+
+struct Event: Codable {
+
+ // MARK: - Instance Properties
+
+ let id: Int
+ let name: String
+ let description: String
+ let parameters: [Parameter]
+}
diff --git a/Sources/AnalyticsGen/Models/Parameter.swift b/Sources/AnalyticsGen/Models/Parameter.swift
new file mode 100644
index 0000000..c7ffe26
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Parameter.swift
@@ -0,0 +1,19 @@
+//
+// Parameter.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+
+struct Parameter: Codable {
+
+ // MARK: - Instance Properties
+
+ let id: Int
+ let name: String
+ let description: String
+ let isOptional: Bool
+ let type: String
+}
diff --git a/Sources/AnalyticsGen/Models/Parameters/GenerationParameters.swift b/Sources/AnalyticsGen/Models/Parameters/GenerationParameters.swift
new file mode 100644
index 0000000..49dfb80
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Parameters/GenerationParameters.swift
@@ -0,0 +1,15 @@
+//
+// GenerationParameters.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+struct GenerationParameters {
+
+ // MARK: - Instance Properties
+
+ let render: RenderParameters
+}
diff --git a/Sources/AnalyticsGen/Models/Parameters/RenderParameters.swift b/Sources/AnalyticsGen/Models/Parameters/RenderParameters.swift
new file mode 100644
index 0000000..e87cdb1
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Parameters/RenderParameters.swift
@@ -0,0 +1,16 @@
+//
+// RenderParameters.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+struct RenderParameters {
+
+ // MARK: - Instance Properties
+
+ let template: RenderTemplate
+ let destination: RenderDestination
+}
diff --git a/Sources/AnalyticsGen/Models/Tracker.swift b/Sources/AnalyticsGen/Models/Tracker.swift
new file mode 100644
index 0000000..ff41ba9
--- /dev/null
+++ b/Sources/AnalyticsGen/Models/Tracker.swift
@@ -0,0 +1,18 @@
+//
+// Tracker.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+
+struct Tracker: Codable {
+
+ // MARK: - Instance Properties
+
+ let id: Int
+ let name: String
+ let `import`: String
+ let events: [Event]
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIEmptyParameters.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIEmptyParameters.swift
new file mode 100644
index 0000000..017e9b9
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIEmptyParameters.swift
@@ -0,0 +1,15 @@
+//
+// AnalyticsGenAPIEmptyParameters.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 18/01/2020.
+//
+
+import Foundation
+
+struct AnalyticsGenAPIEmptyParameters: Encodable {
+
+ // MARK: - Encodable
+
+ func encode(to encoder: Encoder) throws { }
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIProvider.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIProvider.swift
new file mode 100644
index 0000000..6c4e4b0
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIProvider.swift
@@ -0,0 +1,16 @@
+//
+// AnalyticsGenAPIProvider.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import PromiseKit
+
+protocol AnalyticsGenAPIProvider {
+
+ // MARK: - Instance Methods
+
+ func request(route: Route) -> Promise
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIRoute.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIRoute.swift
new file mode 100644
index 0000000..bc8b040
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIRoute.swift
@@ -0,0 +1,68 @@
+//
+// AnalyticsGenAPIRoute.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import AnalyticsGenTools
+
+protocol AnalyticsGenAPIRoute {
+
+ // MARK: - Nested Types
+
+ associatedtype QueryParameters: Encodable
+ associatedtype BodyParameters: Encodable
+ associatedtype Response: Decodable
+
+ // MARK: - Instance Properties
+
+ var apiVersion: AnalyticsGenAPIVersion { get }
+ var httpMethod: HTTPMethod { get }
+ var urlPath: String { get }
+ var accessToken: String? { get }
+ var queryParameters: QueryParameters? { get }
+ var bodyParameters: BodyParameters? { get }
+}
+
+// MARK: -
+
+extension AnalyticsGenAPIRoute {
+
+ // MARK: - Instance Properties
+
+ var apiVersion: AnalyticsGenAPIVersion {
+ .v1
+ }
+
+ var httpMethod: HTTPMethod {
+ .get
+ }
+
+ var accessToken: String? {
+ nil
+ }
+}
+
+// MARK: -
+
+extension AnalyticsGenAPIRoute where QueryParameters == AnalyticsGenAPIEmptyParameters {
+
+ // MARK: - Instance Properties
+
+ var queryParameters: QueryParameters? {
+ nil
+ }
+}
+
+// MARK: -
+
+extension AnalyticsGenAPIRoute where BodyParameters == AnalyticsGenAPIEmptyParameters {
+
+ // MARK: - Instance Properties
+
+ var bodyParameters: BodyParameters? {
+ nil
+ }
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIVersion.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIVersion.swift
new file mode 100644
index 0000000..db6e75e
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenAPIVersion.swift
@@ -0,0 +1,24 @@
+//
+// AnalyticsGenAPIVersion.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+
+enum AnalyticsGenAPIVersion {
+
+ // MARK: - Enumeration Cases
+
+ case v1
+
+ // MARK: - Instance Properties
+
+ var urlPath: String {
+ switch self {
+ case .v1:
+ return "v1"
+ }
+ }
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenHTTPService.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenHTTPService.swift
new file mode 100644
index 0000000..7aeee4f
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/AnalyticsGenHTTPService.swift
@@ -0,0 +1,20 @@
+//
+// AnalyticsGenHTTPService.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import AnalyticsGenTools
+
+protocol AnalyticsGenHTTPService {
+
+ // MARK: - Instance Methods
+
+ func request(route: HTTPRoute) -> HTTPTask
+}
+
+// MARK: - HTTPService
+
+extension HTTPService: AnalyticsGenHTTPService { }
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/DefaultAnalyticsGenAPIProvider.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/DefaultAnalyticsGenAPIProvider.swift
new file mode 100644
index 0000000..705c7b5
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/DefaultAnalyticsGenAPIProvider.swift
@@ -0,0 +1,81 @@
+//
+// DefaultAnalyticsGenAPIProvider.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import PromiseKit
+import AnalyticsGenTools
+
+final class DefaultAnalyticsGenAPIProvider: AnalyticsGenAPIProvider {
+
+ // MARK: - Instance Properties
+
+ private let queryEncoder: HTTPQueryEncoder
+ private let bodyEncoder: HTTPBodyEncoder
+ private let responseDecoder: HTTPResponseDecoder
+
+ // MARK: -
+
+ let httpService: AnalyticsGenHTTPService
+
+ // MARK: - Initializers
+
+ init(httpService: AnalyticsGenHTTPService) {
+ self.httpService = httpService
+
+ let urlEncoder = URLEncoder()
+ let jsonEncoder = JSONEncoder()
+ let jsonDecoder = JSONDecoder()
+
+ self.queryEncoder = HTTPQueryURLEncoder(urlEncoder: urlEncoder)
+ self.bodyEncoder = HTTPBodyJSONEncoder(jsonEncoder: jsonEncoder)
+ self.responseDecoder = jsonDecoder
+ }
+
+ // MARK: - Instance Methods
+
+ private func makeHTTPRoute(for route: Route) -> HTTPRoute {
+ let url = URL.analyticsGenURL
+ .appendingPathComponent(route.apiVersion.urlPath)
+ .appendingPathComponent(route.urlPath)
+
+ return HTTPRoute(
+ method: route.httpMethod,
+ url: url,
+ queryParameters: route.queryParameters,
+ queryEncoder: self.queryEncoder,
+ bodyParameters: route.bodyParameters,
+ bodyEncoder: self.bodyEncoder
+ )
+ }
+
+ // MARK: - AnalyticsGenAPIProvider
+
+ func request(route: Route) -> Promise where Route: AnalyticsGenAPIRoute {
+ return Promise(resolver: { seal in
+ let task = self.httpService.request(route: self.makeHTTPRoute(for: route))
+
+ task.responseDecodable(type: Route.Response.self, decoder: self.responseDecoder, completion: { response in
+ switch response.result {
+ case let .failure(error):
+ seal.reject(error)
+
+ case let .success(value):
+ seal.fulfill(value)
+ }
+ })
+ })
+ }
+}
+
+// MARK: - URL
+
+private extension URL {
+
+ // MARK: - Type Properties
+
+ static let analyticsGenURL = URL(string: "http://localhost:8080")!
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/Routes/AnalyticsGenAPITrackerRoute.swift b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/Routes/AnalyticsGenAPITrackerRoute.swift
new file mode 100644
index 0000000..766f0a6
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsGenAPI/Routes/AnalyticsGenAPITrackerRoute.swift
@@ -0,0 +1,23 @@
+//
+// AnalyticsGenAPITrackerRoute.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 18/01/2020.
+//
+
+import Foundation
+
+struct AnalyticsGenAPITrackerRoute: AnalyticsGenAPIRoute {
+
+ // MARK: - Nested Types
+
+ typealias Response = [Tracker]
+ typealias QueryParameters = AnalyticsGenAPIEmptyParameters
+ typealias BodyParameters = AnalyticsGenAPIEmptyParameters
+
+ // MARK: - Instance Properties
+
+ var urlPath: String {
+ "tracker"
+ }
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsTrackers/AnalyticsTrackersProvider.swift b/Sources/AnalyticsGen/Providers/AnalyticsTrackers/AnalyticsTrackersProvider.swift
new file mode 100644
index 0000000..a4815e5
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsTrackers/AnalyticsTrackersProvider.swift
@@ -0,0 +1,16 @@
+//
+// AnalyticsTrackersProvider.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import PromiseKit
+
+protocol AnalyticsTrackersProvider {
+
+ // MARK: - Instance Methods
+
+ func fetchAnalyticsTrackers() -> Promise<[Tracker]>
+}
diff --git a/Sources/AnalyticsGen/Providers/AnalyticsTrackers/DefaultAnalyticsTrackersProvider.swift b/Sources/AnalyticsGen/Providers/AnalyticsTrackers/DefaultAnalyticsTrackersProvider.swift
new file mode 100644
index 0000000..fdef617
--- /dev/null
+++ b/Sources/AnalyticsGen/Providers/AnalyticsTrackers/DefaultAnalyticsTrackersProvider.swift
@@ -0,0 +1,34 @@
+//
+// DefaultAnalyticsTrackersProvider.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 12/01/2020.
+//
+
+import Foundation
+import PromiseKit
+
+final class DefaultAnalyticsTrackersProvider: AnalyticsTrackersProvider {
+
+ // MARK: - Instance Properties
+
+ private let apiProvider: AnalyticsGenAPIProvider
+
+ // MARK: - Initializers
+
+ init(apiProvider: AnalyticsGenAPIProvider) {
+ self.apiProvider = apiProvider
+ }
+
+ // MARK: - AnalyticsTrackersProvider
+
+ func fetchAnalyticsTrackers() -> Promise<[Tracker]> {
+ let route = AnalyticsGenAPITrackerRoute()
+
+ let promise = firstly {
+ self.apiProvider.request(route: route)
+ }
+
+ return promise
+ }
+}
diff --git a/Sources/AnalyticsGen/Render/DefaultTemplateRenderer.swift b/Sources/AnalyticsGen/Render/DefaultTemplateRenderer.swift
new file mode 100644
index 0000000..ae1386a
--- /dev/null
+++ b/Sources/AnalyticsGen/Render/DefaultTemplateRenderer.swift
@@ -0,0 +1,95 @@
+//
+// DefaultTemplateRenderer.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+import PathKit
+import Stencil
+import StencilSwiftKit
+
+final class DefaultTemplateRenderer: TemplateRenderer {
+
+ // MARK: - Nested Types
+
+ private enum Constants {
+
+ // MARK: - Type Properties
+
+ static let templatesFileExtension = ".stencil"
+ static let templatesXcodeRelativePath = "../Templates"
+ static let templatesPodsRelativePath = "../Templates"
+ static let templatesShareRelativePath = "../../share/fugen"
+ static let templateOptionsKey = "options"
+ }
+
+ // MARK: - Instance Methods
+
+ private func resolveTemplatePath(of templateType: RenderTemplateType) throws -> Path {
+ switch templateType {
+ case let .native(name: templateName):
+ let templateFileName = templateName.appending(Constants.templatesFileExtension)
+
+ #if DEBUG
+ let xcodeTemplatesPath = Path.current.appending(Constants.templatesXcodeRelativePath)
+
+ if xcodeTemplatesPath.exists {
+ return xcodeTemplatesPath.appending(templateFileName)
+ }
+ #endif
+
+ var executablePath = Path(ProcessInfo.processInfo.executablePath)
+
+ while executablePath.isSymlink {
+ executablePath = try executablePath.symlinkDestination()
+ }
+
+ let podsTemplatesPath = executablePath.appending(Constants.templatesPodsRelativePath)
+
+ if podsTemplatesPath.exists {
+ return podsTemplatesPath.appending(templateFileName)
+ }
+
+ return executablePath.appending(Constants.templatesShareRelativePath).appending(templateFileName)
+
+ case let .custom(path: templatePath):
+ return Path(templatePath)
+ }
+ }
+
+ private func write(output: String, to destination: RenderDestination) throws {
+ switch destination {
+ case let .file(path: filePath):
+ let filePath = Path(filePath)
+
+ try filePath.parent().mkpath()
+ try filePath.write(output)
+
+ case .console:
+ print(output)
+ }
+ }
+
+ // MARK: - TemplateRenderer
+
+ func render(template: RenderTemplate, to destination: RenderDestination, context: [String: Any]) throws {
+ let templatePath = try self.resolveTemplatePath(of: template.type)
+
+ let stencilEnvironment = Environment(
+ loader: FileSystemLoader(paths: [templatePath.parent()]),
+ templateClass: StencilSwiftTemplate.self
+ )
+
+ let stencilTemplate = StencilSwiftTemplate(
+ templateString: try templatePath.read(),
+ environment: stencilEnvironment
+ )
+
+ let context = context.merging([Constants.templateOptionsKey: template.options], uniquingKeysWith: { $1 })
+ let output = try stencilTemplate.render(context)
+
+ try self.write(output: output, to: destination)
+ }
+}
diff --git a/Sources/AnalyticsGen/Render/RenderDestination.swift b/Sources/AnalyticsGen/Render/RenderDestination.swift
new file mode 100644
index 0000000..b05fbaf
--- /dev/null
+++ b/Sources/AnalyticsGen/Render/RenderDestination.swift
@@ -0,0 +1,16 @@
+//
+// RenderDestination.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+enum RenderDestination {
+
+ // MARK: - Enumeration Cases
+
+ case file(path: String)
+ case console
+}
diff --git a/Sources/AnalyticsGen/Render/RenderTemplate.swift b/Sources/AnalyticsGen/Render/RenderTemplate.swift
new file mode 100644
index 0000000..ec63034
--- /dev/null
+++ b/Sources/AnalyticsGen/Render/RenderTemplate.swift
@@ -0,0 +1,16 @@
+//
+// RenderTemplate.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 18/01/2020.
+//
+
+import Foundation
+
+struct RenderTemplate {
+
+ // MARK: - Instance Properties
+
+ let type: RenderTemplateType
+ let options: [String: Any]
+}
diff --git a/Sources/AnalyticsGen/Render/RenderTemplateType.swift b/Sources/AnalyticsGen/Render/RenderTemplateType.swift
new file mode 100644
index 0000000..03e80d2
--- /dev/null
+++ b/Sources/AnalyticsGen/Render/RenderTemplateType.swift
@@ -0,0 +1,16 @@
+//
+// RenderTemplateType.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 19/01/2020.
+//
+
+import Foundation
+
+enum RenderTemplateType {
+
+ // MARK: - Enumeration Cases
+
+ case native(name: String)
+ case custom(path: String)
+}
diff --git a/Sources/AnalyticsGen/Render/TemplateRenderer.swift b/Sources/AnalyticsGen/Render/TemplateRenderer.swift
new file mode 100644
index 0000000..d3ae6d9
--- /dev/null
+++ b/Sources/AnalyticsGen/Render/TemplateRenderer.swift
@@ -0,0 +1,15 @@
+//
+// TemplateRenderer.swift
+// AnalyticsGen
+//
+// Created by Timur Shafigullin on 18/01/2020.
+//
+
+import Foundation
+
+protocol TemplateRenderer {
+
+ // MARK: - Instance Methods
+
+ func render(template: RenderTemplate, to destination: RenderDestination, context: [String: Any]) throws
+}
diff --git a/Sources/AnalyticsGen/main.swift b/Sources/AnalyticsGen/main.swift
new file mode 100644
index 0000000..fbba041
--- /dev/null
+++ b/Sources/AnalyticsGen/main.swift
@@ -0,0 +1,20 @@
+import Foundation
+import SwiftCLI
+
+// MARK: - Constants
+
+private enum Constants {
+
+ // MARK: - Type Properties
+
+ static let version = "0.1.0"
+ static let description = "Generate analytics code for you Swift iOS project"
+}
+
+let analyticsGen = CLI(name: "AnalyticsGen", version: Constants.version, description: Constants.description)
+
+analyticsGen.commands = [
+ TrackersCommand(generator: Dependencies.trackersGenerator)
+]
+
+analyticsGen.goAndExitOnError()
diff --git a/Sources/AnalyticsGenTools/Extensions/Bundle+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/Bundle+Extensions.swift
new file mode 100644
index 0000000..644d663
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/Bundle+Extensions.swift
@@ -0,0 +1,40 @@
+import Foundation
+
+extension Bundle {
+
+ // MARK: - Instance Properties
+
+ public var bundleName: String? {
+ string(forInfoDictionaryKey: "CFBundleName")
+ }
+
+ public var developmentRegion: String? {
+ string(forInfoDictionaryKey: "CFBundleDevelopmentRegion")
+ }
+
+ public var displayName: String? {
+ string(forInfoDictionaryKey: "CFBundleDisplayName")
+ }
+
+ public var executableName: String? {
+ string(forInfoDictionaryKey: "CFBundleExecutable")
+ }
+
+ public var version: String? {
+ string(forInfoDictionaryKey: "CFBundleShortVersionString")
+ }
+
+ public var build: String? {
+ string(forInfoDictionaryKey: "CFBundleVersion")
+ }
+
+ // MARK: - Instance Methods
+
+ public func string(forInfoDictionaryKey key: String) -> String? {
+ return object(forInfoDictionaryKey: key) as? String
+ }
+
+ public func url(forInfoDictionaryKey key: String) -> URL? {
+ return string(forInfoDictionaryKey: key).flatMap(URL.init(string:))
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/CLI+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/CLI+Extensions.swift
new file mode 100644
index 0000000..5ae2b4d
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/CLI+Extensions.swift
@@ -0,0 +1,15 @@
+import Foundation
+import SwiftCLI
+
+extension CLI {
+
+ // MARK: - Instance Methods
+
+ public func goAndExitOnError() {
+ let result = go()
+
+ if result != EXIT_SUCCESS {
+ exit(result)
+ }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/DispatchQueue+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/DispatchQueue+Extensions.swift
new file mode 100644
index 0000000..4a60eee
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/DispatchQueue+Extensions.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+extension DispatchQueue {
+
+ // MARK: - Instance Methods
+
+ public func async(flags: DispatchWorkItemFlags?, _ work: @escaping() -> Void) {
+ if let flags = flags {
+ async(flags: flags, execute: work)
+ } else {
+ async(execute: work)
+ }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/FloatingPoint+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/FloatingPoint+Extensions.swift
new file mode 100644
index 0000000..0e04acd
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/FloatingPoint+Extensions.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+extension FloatingPoint {
+
+ // MARK: - Instance Methods
+
+ public func rounded(precision: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self {
+ let scale = Self(precision * 10)
+
+ return (self * scale).rounded(rule) / scale
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/JSONDecoder+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/JSONDecoder+Extensions.swift
new file mode 100644
index 0000000..48ff60e
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/JSONDecoder+Extensions.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+extension JSONDecoder {
+
+ // MARK: - Initializers
+
+ public convenience init(
+ dateDecodingStrategy: DateDecodingStrategy = .deferredToDate,
+ dataDecodingStrategy: DataDecodingStrategy = .base64,
+ nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw,
+ keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys,
+ userInfo: [CodingUserInfoKey: Any] = [:]
+ ) {
+ self.init()
+
+ self.dateDecodingStrategy = dateDecodingStrategy
+ self.dataDecodingStrategy = dataDecodingStrategy
+ self.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy
+ self.keyDecodingStrategy = keyDecodingStrategy
+ self.userInfo = userInfo
+ }
+
+ // MARK: - Instance Methods
+
+ public func decode(from data: Data) throws -> T {
+ try decode(T.self, from: data)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/JSONEncoder+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/JSONEncoder+Extensions.swift
new file mode 100644
index 0000000..47fc8d7
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/JSONEncoder+Extensions.swift
@@ -0,0 +1,24 @@
+import Foundation
+
+extension JSONEncoder {
+
+ // MARK: - Initializers
+
+ public convenience init(
+ outputFormatting: OutputFormatting = [],
+ dateEncodingStrategy: DateEncodingStrategy = .deferredToDate,
+ dataEncodingStrategy: DataEncodingStrategy = .base64,
+ nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw,
+ keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys,
+ userInfo: [CodingUserInfoKey: Any] = [:]
+ ) {
+ self.init()
+
+ self.outputFormatting = outputFormatting
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.dataEncodingStrategy = dataEncodingStrategy
+ self.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy
+ self.keyEncodingStrategy = keyEncodingStrategy
+ self.userInfo = userInfo
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift
new file mode 100644
index 0000000..07ad067
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+extension KeyedDecodingContainerProtocol {
+
+ // MARK: - Instance Methods
+
+ public func decode(forKey key: Key) throws -> T {
+ return try decode(T.self, forKey: key)
+ }
+
+ public func decodeIfPresent(forKey key: Self.Key) throws -> T? {
+ return try decodeIfPresent(T.self, forKey: key)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/OperatingSystemVersion+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/OperatingSystemVersion+Extensions.swift
new file mode 100644
index 0000000..261a946
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/OperatingSystemVersion+Extensions.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+extension OperatingSystemVersion {
+
+ // MARK: - Instance Properties
+
+ public var fullVersion: String {
+ guard patchVersion > 0 else {
+ return "\(majorVersion).\(minorVersion)"
+ }
+
+ return "\(majorVersion).\(minorVersion).\(patchVersion)"
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/Optional+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/Optional+Extensions.swift
new file mode 100644
index 0000000..32e93cf
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/Optional+Extensions.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+extension Optional {
+
+ // MARK: - Instance Properties
+
+ public var isNil: Bool {
+ self == nil
+ }
+}
+
+extension Optional where Wrapped: Collection {
+
+ // MARK: - Instance Properties
+
+ public var isEmptyOrNil: Bool {
+ self?.isEmpty ?? true
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/Path+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/Path+Extensions.swift
new file mode 100644
index 0000000..0ea7849
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/Path+Extensions.swift
@@ -0,0 +1,19 @@
+import Foundation
+import PathKit
+
+extension Path {
+
+ // MARK: - Instance Methods
+
+ public func appending(_ path: Path) -> Path {
+ return self + path
+ }
+
+ public func appending(_ path: String) -> Path {
+ return self + path
+ }
+
+ public func appending(fileName: String, `extension`: String) -> Path {
+ return self + "\(fileName).\(`extension`)"
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/ProcessInfo+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/ProcessInfo+Extensions.swift
new file mode 100644
index 0000000..23dc4da
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/ProcessInfo+Extensions.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+extension ProcessInfo {
+
+ // MARK: - Instance Properties
+
+ public var executablePath: String {
+ arguments[0]
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/Promise+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/Promise+Extensions.swift
new file mode 100644
index 0000000..8d51720
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/Promise+Extensions.swift
@@ -0,0 +1,51 @@
+import Foundation
+import PromiseKit
+
+extension Promise {
+
+ // MARK: - Type Methods
+
+ public static func error(_ error: Error) -> Promise {
+ return Promise(error: error)
+ }
+
+ // MARK: - Instance Methods
+
+ public func nest(
+ on queue: DispatchQueue? = conf.Q.map,
+ flags: DispatchWorkItemFlags? = nil,
+ _ body: @escaping(T) throws -> U
+ ) -> Promise {
+ then(on: queue, flags: flags) { value in
+ try body(value).map(on: nil) { _ in
+ value
+ }
+ }
+ }
+
+ public func asOptional() -> Promise {
+ return map(on: nil) { $0 as T? }
+ }
+}
+
+public func perform(
+ on queue: DispatchQueue? = conf.Q.map,
+ flags: DispatchWorkItemFlags? = nil,
+ _ body: @escaping () throws -> T
+) -> Promise {
+ return Promise { seal in
+ let work = {
+ do {
+ seal.fulfill(try body())
+ } catch {
+ seal.reject(error)
+ }
+ }
+
+ if let queue = queue {
+ queue.async(flags: flags, work)
+ } else {
+ work()
+ }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/RangeReplaceableCollection+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/RangeReplaceableCollection+Extensions.swift
new file mode 100644
index 0000000..799cc85
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/RangeReplaceableCollection+Extensions.swift
@@ -0,0 +1,30 @@
+import Foundation
+
+extension RangeReplaceableCollection {
+
+ // MARK: - Instance Methods
+
+ public mutating func prepend(contentsOf collection: T) where Self.Element == T.Element {
+ insert(contentsOf: collection, at: startIndex)
+ }
+
+ public mutating func prepend(_ element: Element) {
+ insert(element, at: startIndex)
+ }
+
+ public func prepending(contentsOf collection: T) -> Self where Self.Element == T.Element {
+ return collection + self
+ }
+
+ public func prepending(_ element: Element) -> Self {
+ return prepending(contentsOf: [element])
+ }
+
+ public func appending(contentsOf collection: T) -> Self where Self.Element == T.Element {
+ return self + collection
+ }
+
+ public func appending(_ element: Element) -> Self {
+ return appending(contentsOf: [element])
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/Sequence+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/Sequence+Extensions.swift
new file mode 100644
index 0000000..13dd679
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/Sequence+Extensions.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+extension Sequence {
+
+ // MARK: - Instance Properties
+
+ public var lazyFirst: Element? {
+ first { _ in true }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift
new file mode 100644
index 0000000..4f7a8ce
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+extension SingleValueDecodingContainer {
+
+ // MARK: - Instance Methods
+
+ public func decode() throws -> T {
+ return try decode(T.self)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/String+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/String+Extensions.swift
new file mode 100644
index 0000000..d2edba7
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/String+Extensions.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+extension String {
+
+ // MARK: - Type Properties
+
+ public static let empty = ""
+
+ // MARK: - Instance Properties
+
+ public var firstUppercased: String {
+ prefix(1).uppercased().appending(dropFirst())
+ }
+
+ public var firstLowercased: String {
+ prefix(1).lowercased().appending(dropFirst())
+ }
+
+ public var firstCapitalized: String {
+ prefix(1).capitalized.appending(dropFirst())
+ }
+
+ public var camelized: String {
+ components(separatedBy: CharacterSet.alphanumerics.inverted)
+ .map { $0.firstUppercased }
+ .joined()
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Extensions/UnkeyedDecodingContainer+Extensions.swift b/Sources/AnalyticsGenTools/Extensions/UnkeyedDecodingContainer+Extensions.swift
new file mode 100644
index 0000000..2faf581
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Extensions/UnkeyedDecodingContainer+Extensions.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+extension UnkeyedDecodingContainer {
+
+ // MARK: - Instance Methods
+
+ public mutating func decode() throws -> T {
+ return try decode(T.self)
+ }
+
+ public mutating func decodeIfPresent() throws -> T? {
+ return try decodeIfPresent(T.self)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift
new file mode 100644
index 0000000..08ee92c
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public protocol HTTPBodyEncoder {
+
+ // MARK: - Instance Methods
+
+ func encode(request: URLRequest, parameters: T) throws -> URLRequest
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift
new file mode 100644
index 0000000..c61b817
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public final class HTTPBodyJSONEncoder: HTTPBodyEncoder {
+
+ // MARK: - Type Properties
+
+ public static let `default` = HTTPBodyJSONEncoder(jsonEncoder: JSONEncoder())
+
+ // MARK: - Instance Properties
+
+ public let jsonEncoder: JSONEncoder
+
+ // MARK: - Initializers
+
+ public init(jsonEncoder: JSONEncoder) {
+ self.jsonEncoder = jsonEncoder
+ }
+
+ // MARK: - Instance Methods
+
+ public func encode(request: URLRequest, parameters: T) throws -> URLRequest {
+ var request = request
+
+ request.httpBody = try jsonEncoder.encode(parameters)
+
+ if !request.httpBody.isEmptyOrNil {
+ if request.value(forHTTPHeaderField: .contentTypeHeaderField) == nil {
+ request.setValue(.contentTypeHeaderValue, forHTTPHeaderField: .contentTypeHeaderField)
+ }
+ }
+
+ return request
+ }
+}
+
+private extension String {
+
+ // MARK: - Type Properties
+
+ static let contentTypeHeaderField = "Content-Type"
+ static let contentTypeHeaderValue = "application/json"
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift
new file mode 100644
index 0000000..5739a43
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift
@@ -0,0 +1,44 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public final class HTTPBodyURLEncoder: HTTPBodyEncoder {
+
+ // MARK: - Type Properties
+
+ public static let `default` = HTTPBodyURLEncoder(urlEncoder: URLEncoder())
+
+ // MARK: - Instance Properties
+
+ public let urlEncoder: URLEncoder
+
+ // MARK: - Initializers
+
+ public init(urlEncoder: URLEncoder) {
+ self.urlEncoder = urlEncoder
+ }
+
+ // MARK: - Instance Methods
+
+ public func encode(request: URLRequest, parameters: T) throws -> URLRequest {
+ var request = request
+
+ request.httpBody = try urlEncoder.encode(parameters)
+
+ if !request.httpBody.isEmptyOrNil, request.value(forHTTPHeaderField: .contentTypeHeaderField) == nil {
+ request.setValue(.contentTypeHeaderValue, forHTTPHeaderField: .contentTypeHeaderField)
+ }
+
+ return request
+ }
+}
+
+private extension String {
+
+ // MARK: - Type Properties
+
+ static let contentTypeHeaderField = "Content-Type"
+ static let contentTypeHeaderValue = "application/x-www-form-urlencoded; charset=utf-8"
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPActivityIndicator.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPActivityIndicator.swift
new file mode 100644
index 0000000..50fa279
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPActivityIndicator.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+public protocol HTTPActivityIndicator: AnyObject {
+
+ // MARK: - Instance Properties
+
+ var activityCount: Int { get }
+
+ // MARK: - Instance Methods
+
+ func incrementActivityCount()
+ func decrementActivityCount()
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPAnyEncodable.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPAnyEncodable.swift
new file mode 100644
index 0000000..fa3f9b3
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPAnyEncodable.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+internal struct HTTPAnyEncodable: Encodable {
+
+ // MARK: - Instance Properties
+
+ internal let value: Encodable
+
+ // MARK: - Initializers
+
+ internal init(_ value: Encodable) {
+ self.value = value
+ }
+
+ // MARK: - Instance Methods
+
+ internal func encode(to encoder: Encoder) throws {
+ try value.encode(to: encoder)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPError.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPError.swift
new file mode 100644
index 0000000..eed9472
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPError.swift
@@ -0,0 +1,184 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct HTTPError: Error, CustomStringConvertible {
+
+ // MARK: - Nested Types
+
+ public enum Code {
+ case unknown
+ case cancelled
+ case fileSystem
+ case tooManyRequests
+ case networkConnection
+ case secureConnection
+ case timedOut
+ case badRequest
+ case badResponse
+ case resource
+ case server
+ case access
+ }
+
+ // MARK: - Instance Properties
+
+ public let code: Code
+ public let reason: Any?
+ public let data: Data?
+
+ public var statusCode: HTTPStatusCode? {
+ reason as? HTTPStatusCode
+ }
+
+ // MARK: - CustomStringConvertible
+
+ public var description: String {
+ var description = "\(type(of: self)).\(code)"
+
+ if let reason = reason {
+ let reasonDescription: String
+
+ if let reason = reason as? HTTPErrorStringConvertible {
+ reasonDescription = reason.httpErrorDescription
+ } else {
+ reasonDescription = String(reflecting: reason)
+ }
+
+ description.append("(\(reasonDescription))")
+ }
+
+ return description
+ }
+
+ // MARK: - Initializers
+
+ public init(code: Code, reason: Any? = nil, data: Data? = nil) {
+ self.code = code
+ self.reason = reason
+ self.data = data
+ }
+
+ public init(urlError error: URLError, data: Data? = nil) {
+ // swiftlint:disable:previous function_body_length
+
+ let code: Code
+
+ switch error.code {
+ case .cancelled:
+ code = .cancelled
+
+ case .fileDoesNotExist,
+ .fileIsDirectory,
+ .cannotCreateFile,
+ .cannotOpenFile,
+ .cannotCloseFile,
+ .cannotWriteToFile,
+ .cannotRemoveFile,
+ .cannotMoveFile:
+ code = .fileSystem
+
+ case .backgroundSessionInUseByAnotherProcess,
+ .backgroundSessionRequiresSharedContainer,
+ .backgroundSessionWasDisconnected,
+ .cannotLoadFromNetwork,
+ .cannotFindHost,
+ .cannotConnectToHost,
+ .dnsLookupFailed,
+ .internationalRoamingOff,
+ .networkConnectionLost,
+ .notConnectedToInternet,
+ .secureConnectionFailed,
+ .callIsActive,
+ .dataNotAllowed:
+ code = .networkConnection
+
+ case .clientCertificateRejected,
+ .clientCertificateRequired,
+ .serverCertificateHasBadDate,
+ .serverCertificateHasUnknownRoot,
+ .serverCertificateNotYetValid,
+ .serverCertificateUntrusted:
+ code = .secureConnection
+
+ case .timedOut:
+ code = .timedOut
+
+ case .badURL,
+ .requestBodyStreamExhausted,
+ .unsupportedURL:
+ code = .badRequest
+
+ case .badServerResponse,
+ .cannotDecodeContentData,
+ .cannotDecodeRawData,
+ .cannotParseResponse,
+ .downloadDecodingFailedMidStream,
+ .downloadDecodingFailedToComplete:
+ code = .badResponse
+
+ case .httpTooManyRedirects,
+ .redirectToNonExistentLocation,
+ .resourceUnavailable,
+ .zeroByteResource:
+ code = .resource
+
+ case .noPermissionsToReadFile,
+ .userAuthenticationRequired,
+ .userCancelledAuthentication:
+ code = .access
+
+ #if !os(Linux)
+ case .appTransportSecurityRequiresSecureConnection:
+ // swiftlint:disable:previous vertical_whitespace_between_cases
+ code = .secureConnection
+
+ case .dataLengthExceedsMaximum:
+ code = .resource
+ #endif
+
+ default:
+ code = .unknown
+ }
+
+ self.init(code: code, reason: error, data: data)
+ }
+
+ public init?(statusCode: HTTPStatusCode, data: Data? = nil) {
+ guard statusCode.state != .success else {
+ return nil
+ }
+
+ let code: Code
+
+ switch statusCode {
+ case 429:
+ code = .tooManyRequests
+
+ case 511:
+ code = .networkConnection
+
+ case 408, 504:
+ code = .timedOut
+
+ case 400, 406, 411, 412, 413, 414, 415, 416, 417, 422, 424, 425, 426, 428, 431, 449:
+ code = .badRequest
+
+ case 404, 405, 409, 410, 423, 434, 451:
+ code = .resource
+
+ case 500, 501, 502, 503, 505, 506, 507, 508, 509, 510:
+ code = .server
+
+ case 401, 402, 403, 407, 444:
+ code = .access
+
+ default:
+ code = .unknown
+ }
+
+ self.init(code: code, reason: statusCode, data: data)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPErrorStringConvertible.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPErrorStringConvertible.swift
new file mode 100644
index 0000000..16c05ed
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPErrorStringConvertible.swift
@@ -0,0 +1,23 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public protocol HTTPErrorStringConvertible {
+
+ // MARK: - Instance Properties
+
+ var httpErrorDescription: String { get }
+}
+
+// MARK: -
+
+extension URLError: HTTPErrorStringConvertible {
+
+ // MARK: - Instance Properties
+
+ public var httpErrorDescription: String {
+ return "\(type(of: self)).\(self.code.rawValue)"
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPHeader.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPHeader.swift
new file mode 100644
index 0000000..e52c899
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPHeader.swift
@@ -0,0 +1,129 @@
+import Foundation
+
+public struct HTTPHeader: Equatable, CustomStringConvertible {
+
+ // MARK: - Type Methods
+
+ private static func qualityEncoded (_ collection: T) -> String where T.Element == String {
+ return collection
+ .enumerated()
+ .map { "\($1);q=\(1.0 - (Double($0) * 0.1))" }
+ .joined(separator: ", ")
+ }
+
+ // MARK: -
+
+ public static func acceptCharset(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Accept-Charset", value: value)
+ }
+
+ public static func acceptLanguage(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Accept-Language", value: value)
+ }
+
+ public static func acceptEncoding(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Accept-Encoding", value: value)
+ }
+
+ public static func authorization(username: String, password: String) -> HTTPHeader {
+ let credential = Data("\(username):\(password)".utf8).base64EncodedString()
+
+ return authorization("Basic \(credential)")
+ }
+
+ public static func authorization(bearerToken: String) -> HTTPHeader {
+ return authorization("Bearer \(bearerToken)")
+ }
+
+ public static func authorization(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Authorization", value: value)
+ }
+
+ public static func contentDisposition(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Content-Disposition", value: value)
+ }
+
+ public static func contentType(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Content-Type", value: value)
+ }
+
+ public static func userAgent(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "User-Agent", value: value)
+ }
+
+ public static func cookie(_ value: String) -> HTTPHeader {
+ return HTTPHeader(name: "Cookie", value: value)
+ }
+
+ // MARK: -
+
+ public static func defaultAcceptLanguage() -> HTTPHeader {
+ return acceptLanguage(qualityEncoded(Locale.preferredLanguages.prefix(6)))
+ }
+
+ public static func defaultAcceptEncoding() -> HTTPHeader {
+ let encodings: [String]
+
+ if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
+ encodings = ["br", "gzip", "deflate"]
+ } else {
+ encodings = ["gzip", "deflate"]
+ }
+
+ return acceptEncoding(qualityEncoded(encodings))
+ }
+
+ public static func defaultUserAgent(bundle: Bundle = Bundle.main) -> HTTPHeader {
+ let systemName: String
+
+ #if os(iOS)
+ systemName = "iOS"
+ #elseif os(watchOS)
+ systemName = "watchOS"
+ #elseif os(tvOS)
+ systemName = "tvOS"
+ #elseif os(macOS)
+ systemName = "macOS"
+ #elseif os(Linux)
+ systemName = "Linux"
+ #else
+ systemName = "Unknown"
+ #endif
+
+ let system = "\(systemName) \(ProcessInfo.processInfo.operatingSystemVersion.fullVersion)"
+
+ let appBundleIdentifier = bundle.bundleIdentifier ?? "Unknown"
+ let appExecutableName = bundle.executableName ?? "Unknown"
+ let appVersion = bundle.version ?? "Unknown"
+ let appBuild = bundle.build ?? "Unknown"
+
+ return userAgent("\(appExecutableName)/\(appVersion) (\(appBundleIdentifier); build:\(appBuild); \(system))")
+ }
+
+ // MARK: - Instance Properties
+
+ public let name: String
+ public let value: String
+
+ // MARK: - CustomStringConvertible
+
+ public var description: String {
+ return "\(name): \(value)"
+ }
+
+ // MARK: - Initializers
+
+ public init(name: String, value: String) {
+ self.name = name
+ self.value = value
+ }
+}
+
+extension Collection where Element == HTTPHeader {
+
+ // MARK: - Instance Properties
+
+ public var rawHTTPHeaders: [String: String] {
+ return Dictionary(map { ($0.name, $0.value) }) { $1 }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPMethod.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPMethod.swift
new file mode 100644
index 0000000..3f19b1c
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPMethod.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+public enum HTTPMethod: String {
+
+ // MARK: - Enumeration Cases
+
+ case head = "HEAD"
+ case get = "GET"
+ case delete = "DELETE"
+ case patch = "PATCH"
+ case post = "POST"
+ case put = "PUT"
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPResponse.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPResponse.swift
new file mode 100644
index 0000000..4bceff4
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPResponse.swift
@@ -0,0 +1,70 @@
+import Foundation
+
+public struct HTTPResponse: CustomStringConvertible {
+
+ // MARK: - Instance Properties
+
+ public let result: Result
+ public let statusCode: HTTPStatusCode?
+ public let headers: [HTTPHeader]?
+
+ // MARK: -
+
+ public var value: T? {
+ switch result {
+ case let .success(value):
+ return value
+
+ case .failure:
+ return nil
+ }
+ }
+
+ public var error: HTTPError? {
+ switch result {
+ case .success:
+ return nil
+
+ case let .failure(error):
+ return error
+ }
+ }
+
+ public var isSuccess: Bool {
+ switch result {
+ case .success:
+ return true
+
+ case .failure:
+ return false
+ }
+ }
+
+ public var isFailure: Bool {
+ return !isSuccess
+ }
+
+ // MARK: - CustomStringConvertible
+
+ public var description: String {
+ switch result {
+ case .success:
+ return "\(type(of: self)).success"
+
+ case let .failure(error):
+ return "\(type(of: self)).failure(\(error))"
+ }
+ }
+
+ // MARK: - Initializers
+
+ public init(
+ _ result: Result,
+ statusCode: HTTPStatusCode? = nil,
+ headers: [HTTPHeader]? = nil
+ ) {
+ self.result = result
+ self.statusCode = statusCode
+ self.headers = headers
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPRoute.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPRoute.swift
new file mode 100644
index 0000000..db96156
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPRoute.swift
@@ -0,0 +1,80 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public struct HTTPRoute: CustomStringConvertible {
+
+ // MARK: - Instance Properties
+
+ public let method: HTTPMethod
+ public let url: URL
+ public let headers: [HTTPHeader]
+
+ public let queryParameters: Encodable?
+ public let queryEncoder: HTTPQueryEncoder
+
+ public let bodyParameters: Encodable?
+ public let bodyEncoder: HTTPBodyEncoder
+
+ // MARK: - CustomStringConvertible
+
+ public var description: String {
+ return "\(type(of: self)).\(method)(\(url.absoluteString))"
+ }
+
+ // MARK: - Initializers
+
+ public init(
+ method: HTTPMethod,
+ url: URL,
+ headers: [HTTPHeader] = [],
+ queryParameters: Encodable? = nil,
+ queryEncoder: HTTPQueryEncoder = HTTPQueryURLEncoder.default,
+ bodyParameters: Encodable? = nil,
+ bodyEncoder: HTTPBodyEncoder = HTTPBodyJSONEncoder.default
+ ) {
+ self.method = method
+ self.url = url
+ self.headers = headers
+
+ self.queryParameters = queryParameters
+ self.queryEncoder = queryEncoder
+
+ self.bodyParameters = bodyParameters
+ self.bodyEncoder = bodyEncoder
+ }
+
+ // MARK: - Instance Methods
+
+ public func asRequest() throws -> URLRequest {
+ var request: URLRequest
+
+ if let queryParameters = queryParameters {
+ let encodedURL = try queryEncoder.encode(
+ url: url,
+ parameters: HTTPAnyEncodable(queryParameters)
+ )
+
+ request = URLRequest(url: encodedURL)
+ } else {
+ request = URLRequest(url: url)
+ }
+
+ request.httpMethod = method.rawValue
+
+ for header in headers {
+ request.setValue(header.value, forHTTPHeaderField: header.name)
+ }
+
+ if let bodyParameters = bodyParameters {
+ request = try bodyEncoder.encode(
+ request: request,
+ parameters: HTTPAnyEncodable(bodyParameters)
+ )
+ }
+
+ return request
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPService.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPService.swift
new file mode 100644
index 0000000..1edd85f
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPService.swift
@@ -0,0 +1,68 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public final class HTTPService {
+
+ // MARK: - Instance Properties
+
+ private let session: URLSession
+
+ // MARK: -
+
+ public var sessionConfiguration: URLSessionConfiguration {
+ return session.configuration
+ }
+
+ public var activityIndicator: HTTPActivityIndicator?
+
+ // MARK: - Initializers
+
+ public init(
+ sessionConfiguration: URLSessionConfiguration = .httpServiceDefault,
+ activityIndicator: HTTPActivityIndicator? = nil
+ ) {
+ self.session = URLSession(configuration: sessionConfiguration)
+ self.activityIndicator = activityIndicator
+ }
+
+ // MARK: - Instance Methods
+
+ public func request(route: HTTPRoute) -> HTTPTask {
+ DispatchQueue.main.async {
+ self.activityIndicator?.incrementActivityCount()
+ }
+
+ return HTTPServiceTask(session: session, route: route)
+ .launch()
+ .response { _ in
+ DispatchQueue.main.async {
+ self.activityIndicator?.decrementActivityCount()
+ }
+ }
+ }
+}
+
+extension URLSessionConfiguration {
+
+ // MARK: - Type Properties
+
+ public static var httpServiceDefault: URLSessionConfiguration {
+ let configuration = URLSessionConfiguration.default
+
+ let headers = [
+ HTTPHeader.defaultAcceptLanguage(),
+ HTTPHeader.defaultAcceptEncoding(),
+ HTTPHeader.defaultUserAgent()
+ ]
+
+ let rawHeaders = Dictionary(uniqueKeysWithValues: headers.map { ($0.name, $0.value) })
+
+ configuration.httpAdditionalHeaders = rawHeaders
+ configuration.timeoutIntervalForRequest = 45
+
+ return configuration
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPServiceTask.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPServiceTask.swift
new file mode 100644
index 0000000..280c342
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPServiceTask.swift
@@ -0,0 +1,226 @@
+import Foundation
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public final class HTTPServiceTask {
+
+ // MARK: - Instance Properties
+
+ private var responseQueue: OperationQueue
+
+ // MARK: -
+
+ internal var sessionTask: SessionTask?
+ internal let session: URLSession
+
+ // MARK: -
+
+ public let route: HTTPRoute
+
+ public private(set) var response: HTTPResponse?
+
+ // MARK: - Initializers
+
+ internal init(session: URLSession, route: HTTPRoute) {
+ self.session = session
+ self.route = route
+
+ responseQueue = OperationQueue()
+
+ responseQueue.maxConcurrentOperationCount = 1
+ responseQueue.qualityOfService = .userInitiated
+ responseQueue.isSuspended = true
+ }
+
+ // MARK: - Instance Methods
+
+ internal func addResponseOperation(_ operation: @escaping (_ response: HTTPResponse) -> Void) {
+ responseQueue.addOperation {
+ operation(self.response ?? HTTPResponse(.failure(HTTPError(code: .unknown))))
+ }
+ }
+
+ internal func handleResponse(_ response: HTTPResponse) {
+ self.response = response
+
+ responseQueue.isSuspended = false
+ }
+
+ internal func handleResponse(_ response: URLResponse?, data: Data?, error: Error?) {
+ var statusCode: HTTPStatusCode?
+ var headers: [HTTPHeader]?
+
+ if let response = response as? HTTPURLResponse {
+ statusCode = HTTPStatusCode(rawValue: response.statusCode)
+
+ headers = response.allHeaderFields.compactMap { (name, value) in
+ guard let name = name as? String, let value = value as? String else {
+ return nil
+ }
+
+ return HTTPHeader(name: name, value: value)
+ }
+ }
+
+ let result: Result
+
+ switch error {
+ case let error as URLError:
+ result = .failure(HTTPError(urlError: error, data: data))
+
+ case let error?:
+ result = .failure(HTTPError(code: .unknown, reason: error, data: data))
+
+ case nil:
+ if let error = HTTPError(statusCode: statusCode ?? 0, data: data) {
+ result = .failure(error)
+ } else {
+ result = .success(data)
+ }
+ }
+
+ handleResponse(HTTPResponse(result, statusCode: statusCode, headers: headers))
+ }
+
+ internal func serializeResponse(
+ _ response: HTTPResponse,
+ serializer: Serializer
+ ) -> HTTPResponse {
+ let result: Result = response.result.flatMap { data in
+ do {
+ let statusCode = response.statusCode ?? 0
+ let object: Serializer.SerializedObject
+
+ if let data = data, !data.isEmpty {
+ object = try serializer.serialize(
+ data: data,
+ statusCode: statusCode,
+ method: self.route.method
+ )
+ } else {
+ object = try serializer.serializeEmptyResponse(
+ statusCode: statusCode,
+ method: self.route.method
+ )
+ }
+
+ return .success(object)
+ } catch {
+ return .failure(HTTPError(code: .badResponse, reason: error, data: data))
+ }
+ }
+
+ return HTTPResponse(result, statusCode: response.statusCode, headers: response.headers)
+ }
+
+ // MARK: -
+
+ @discardableResult
+ public func response(
+ on queue: DispatchQueue,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ addResponseOperation { response in
+ queue.async {
+ completion(response)
+ }
+ }
+
+ return self
+ }
+
+ @discardableResult
+ public func response(
+ on queue: DispatchQueue,
+ serializer: Serializer,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ addResponseOperation { response in
+ let serializedResponse = self.serializeResponse(response, serializer: serializer)
+
+ queue.async {
+ completion(serializedResponse)
+ }
+ }
+
+ return self
+ }
+
+ @discardableResult
+ public func responseData(
+ on queue: DispatchQueue,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return response(
+ on: queue,
+ serializer: HTTPDataResponseSerializer(),
+ completion: completion
+ )
+ }
+
+ @discardableResult
+ public func responseString(
+ on queue: DispatchQueue,
+ encoding: String.Encoding,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return response(
+ on: queue,
+ serializer: HTTPStringResponseSerializer(encoding: encoding),
+ completion: completion
+ )
+ }
+
+ @discardableResult
+ public func responseJSON(
+ on queue: DispatchQueue,
+ options: JSONSerialization.ReadingOptions,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return response(
+ on: queue,
+ serializer: HTTPJSONResponseSerializer(options: options),
+ completion: completion
+ )
+ }
+
+ @discardableResult
+ public func responseDecodable(
+ type: T.Type,
+ on queue: DispatchQueue,
+ decoder: HTTPResponseDecoder,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return response(
+ on: queue,
+ serializer: HTTPDecodableResponseSerializer(decoder: decoder),
+ completion: completion
+ )
+ }
+
+ public func cancel() {
+ sessionTask?.cancel()
+ }
+}
+
+extension HTTPServiceTask: HTTPTask where SessionTask == URLSessionDataTask {
+
+ // MARK: - Instance Methods
+
+ @discardableResult
+ internal func launch() -> Self {
+ do {
+ sessionTask = session.dataTask(with: try route.asRequest()) { data, response, error in
+ self.handleResponse(response, data: data, error: error)
+ }
+
+ sessionTask?.resume()
+ } catch {
+ handleResponse(HTTPResponse(.failure(HTTPError(code: .badRequest, reason: error))))
+ }
+
+ return self
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPStatusCode.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPStatusCode.swift
new file mode 100644
index 0000000..1bc50e3
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPStatusCode.swift
@@ -0,0 +1,56 @@
+import Foundation
+
+public struct HTTPStatusCode: HTTPErrorStringConvertible, Hashable, ExpressibleByIntegerLiteral {
+
+ // MARK: - Nested Types
+
+ public enum State {
+
+ // MARK: - Enumeration Cases
+
+ case unknown
+ case information
+ case success
+ case redirection
+ case failure
+ }
+
+ // MARK: - Instance Properties
+
+ public let rawValue: Int
+
+ public var state: State {
+ switch rawValue {
+ case 100...199:
+ return .information
+
+ case 200...299:
+ return .success
+
+ case 300...399:
+ return .redirection
+
+ case 400...599:
+ return .failure
+
+ default:
+ return .unknown
+ }
+ }
+
+ // MARK: - HTTPErrorStringConvertible
+
+ public var httpErrorDescription: String {
+ "\(type(of: self)).\(rawValue)"
+ }
+
+ // MARK: - Initializers
+
+ public init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ public init(integerLiteral: Int) {
+ self.init(rawValue: integerLiteral)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPTask.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPTask.swift
new file mode 100644
index 0000000..2bb8e9e
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPTask.swift
@@ -0,0 +1,110 @@
+import Foundation
+
+public protocol HTTPTask: AnyObject {
+
+ // MARK: - Instance Properties
+
+ var route: HTTPRoute { get }
+ var response: HTTPResponse? { get }
+
+ var isFinished: Bool { get }
+
+ // MARK: - Instance Methods
+
+ @discardableResult
+ func response(
+ on queue: DispatchQueue,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ @discardableResult
+ func response(
+ on queue: DispatchQueue,
+ serializer: Serializer,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ @discardableResult
+ func responseData(
+ on queue: DispatchQueue,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ @discardableResult
+ func responseString(
+ on queue: DispatchQueue,
+ encoding: String.Encoding,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ @discardableResult
+ func responseJSON(
+ on queue: DispatchQueue,
+ options: JSONSerialization.ReadingOptions,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ @discardableResult
+ func responseDecodable(
+ type: T.Type,
+ on queue: DispatchQueue,
+ decoder: HTTPResponseDecoder,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self
+
+ func cancel()
+}
+
+extension HTTPTask {
+
+ // MARK: - Instance Properties
+
+ public var isFinished: Bool {
+ return response != nil
+ }
+
+ // MARK: - Instance Methods
+
+ @discardableResult
+ public func response(completion: @escaping (_ response: HTTPResponse) -> Void) -> Self {
+ return response(on: .main, completion: completion)
+ }
+
+ @discardableResult
+ public func response(
+ serializer: Serializer,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return response(on: .main, serializer: serializer, completion: completion)
+ }
+
+ @discardableResult
+ public func responseData(completion: @escaping (_ response: HTTPResponse) -> Void) -> Self {
+ return responseData(on: .main, completion: completion)
+ }
+
+ @discardableResult
+ public func responseString(
+ encoding: String.Encoding = .utf8,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return responseString(on: .main, encoding: encoding, completion: completion)
+ }
+
+ @discardableResult
+ public func responseJSON(
+ options: JSONSerialization.ReadingOptions = .allowFragments,
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return responseJSON(on: .main, options: options, completion: completion)
+ }
+
+ @discardableResult
+ public func responseDecodable(
+ type: T.Type,
+ decoder: HTTPResponseDecoder = JSONDecoder(),
+ completion: @escaping (_ response: HTTPResponse) -> Void
+ ) -> Self {
+ return responseDecodable(type: type, on: .main, decoder: decoder, completion: completion)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/HTTPUIActivityIndicator.swift b/Sources/AnalyticsGenTools/HTTPService/HTTPUIActivityIndicator.swift
new file mode 100644
index 0000000..d928ab5
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/HTTPUIActivityIndicator.swift
@@ -0,0 +1,40 @@
+#if canImport(UIKit)
+import UIKit
+
+public final class HTTPUIActivityIndicator: WebActivityIndicator {
+
+ // MARK: - Type Properties
+
+ public static let shared = WebUIActivityIndicator()
+
+ // MARK: - Instance Properties
+
+ public private(set) var activityCount = 0 {
+ didSet {
+ UIApplication.shared.isNetworkActivityIndicatorVisible = activityCount > 0
+ }
+ }
+
+ // MARK: - Initializers
+
+ private init() { }
+
+ // MARK: - Instance Methods
+
+ public func resetActivityCount() {
+ activityCount = 0
+ }
+
+ // MARK: - WebActivityIndicator
+
+ public func incrementActivityCount() {
+ activityCount += 1
+ }
+
+ public func decrementActivityCount() {
+ if activityCount > 0 {
+ activityCount -= 1
+ }
+ }
+}
+#endif
diff --git a/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift b/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift
new file mode 100644
index 0000000..8a13df9
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+public protocol HTTPQueryEncoder {
+
+ // MARK: - Instance Methods
+
+ func encode(url: URL, parameters: T) throws -> URL
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift b/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift
new file mode 100644
index 0000000..f26e487
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift
@@ -0,0 +1,48 @@
+import Foundation
+
+public final class HTTPQueryURLEncoder: HTTPQueryEncoder {
+
+ // MARK: - Type Properties
+
+ public static let `default` = HTTPQueryURLEncoder(urlEncoder: URLEncoder())
+
+ // MARK: - Instance Properties
+
+ public let urlEncoder: URLEncoder
+
+ // MARK: - Initializers
+
+ public init(urlEncoder: URLEncoder) {
+ self.urlEncoder = urlEncoder
+ }
+
+ // MARK: - Instance Methods
+
+ public func encode(url: URL, parameters: T) throws -> URL {
+ guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
+ throw MessageError("Invalid URL")
+ }
+
+ let query = [
+ urlComponents.percentEncodedQuery,
+ try urlEncoder.encodeToQuery(parameters)
+ ]
+
+ urlComponents.percentEncodedQuery = query
+ .compactMap { $0 }
+ .joined(separator: .querySeparator)
+
+ guard let url = urlComponents.url else {
+ throw MessageError("Invalid query parameters")
+ }
+
+ return url
+ }
+}
+
+private extension String {
+
+ // MARK: - Type Properties
+
+ static let querySeparator = "&"
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift
new file mode 100644
index 0000000..abe44fe
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift
@@ -0,0 +1,34 @@
+import Foundation
+
+public final class HTTPDataResponseSerializer: HTTPResponseSerializer {
+
+ // MARK: - Instance Properties
+
+ public let emptyResponseStatusCodes: Set
+ public let emptyResponseMethods: Set
+
+ // MARK: - Initializers
+
+ public init(
+ emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes,
+ emptyResponseMethods: Set = defaultEmptyResponseMethods
+ ) {
+ self.emptyResponseStatusCodes = emptyResponseStatusCodes
+ self.emptyResponseMethods = emptyResponseMethods
+ }
+
+ // MARK: - Instance Methods
+
+ public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> Data {
+ return data
+ }
+}
+
+extension Data: HTTPEmptyResponse {
+
+ // MARK: - Type Methods
+
+ public static func emptyResponseInstance() -> Data {
+ return Data()
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift
new file mode 100644
index 0000000..5c2f692
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+public final class HTTPDecodableResponseSerializer: HTTPResponseSerializer {
+
+ // MARK: - Instance Properties
+
+ public let decoder: HTTPResponseDecoder
+
+ // MARK: - HTTPResponseSerializer
+
+ public let emptyResponseStatusCodes: Set
+ public let emptyResponseMethods: Set
+
+ // MARK: - Initializers
+
+ public init(
+ decoder: HTTPResponseDecoder,
+ emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes,
+ emptyResponseMethods: Set = defaultEmptyResponseMethods
+ ) {
+ self.decoder = decoder
+
+ self.emptyResponseStatusCodes = emptyResponseStatusCodes
+ self.emptyResponseMethods = emptyResponseMethods
+ }
+
+ // MARK: - Instance Methods
+
+ public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> T {
+ return try decoder.decode(T.self, from: data)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift
new file mode 100644
index 0000000..95d500c
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+public protocol HTTPEmptyResponse {
+
+ // MARK: - Type Methods
+
+ static func emptyResponseInstance() -> Self
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift
new file mode 100644
index 0000000..934f9af
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+public final class HTTPJSONResponseSerializer: HTTPResponseSerializer {
+
+ // MARK: - Instance Properties
+
+ public let options: JSONSerialization.ReadingOptions
+
+ // MARK: - HTTPResponseSerializer
+
+ public let emptyResponseStatusCodes: Set
+ public let emptyResponseMethods: Set
+
+ // MARK: - Initializers
+
+ public init(
+ options: JSONSerialization.ReadingOptions,
+ emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes,
+ emptyResponseMethods: Set = defaultEmptyResponseMethods
+ ) {
+ self.options = options
+ self.emptyResponseStatusCodes = emptyResponseStatusCodes
+ self.emptyResponseMethods = emptyResponseMethods
+ }
+
+ // MARK: - Instance Methods
+
+ public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> Any {
+ return try JSONSerialization.jsonObject(with: data, options: options)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift
new file mode 100644
index 0000000..0a6f7e8
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+public protocol HTTPResponseDecoder {
+
+ // MARK: - Instance Methods
+
+ func decode(_ type: T.Type, from data: Data) throws -> T
+}
+
+// MARK: -
+
+extension JSONDecoder: HTTPResponseDecoder { }
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift
new file mode 100644
index 0000000..51fed09
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift
@@ -0,0 +1,49 @@
+import Foundation
+
+public protocol HTTPResponseSerializer {
+
+ // MARK: - Nested Types
+
+ associatedtype SerializedObject
+
+ // MARK: - Instance Properties
+
+ var emptyResponseStatusCodes: Set { get }
+ var emptyResponseMethods: Set { get }
+
+ // MARK: - Instance Methods
+
+ func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject
+ func serializeEmptyResponse(statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject
+}
+
+extension HTTPResponseSerializer {
+
+ // MARK: - Type Properties
+
+ public static var defaultEmptyResponseStatusCodes: Set {
+ [204, 205]
+ }
+
+ public static var defaultEmptyResponseMethods: Set {
+ [.head]
+ }
+
+ // MARK: - Instance Methods
+
+ public func serializeEmptyResponse(statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject {
+ guard emptyResponseStatusCodes.contains(statusCode) || emptyResponseMethods.contains(method) else {
+ throw MessageError("Empty response is unacceptable")
+ }
+
+ guard let emptyResponseType = SerializedObject.self as? HTTPEmptyResponse.Type else {
+ throw MessageError("\(SerializedObject.self) cannot have an empty instance")
+ }
+
+ guard let emptyResponseInstance = emptyResponseType.emptyResponseInstance() as? SerializedObject else {
+ throw MessageError("Empty instance of \(SerializedObject.self) has an invalid type")
+ }
+
+ return emptyResponseInstance
+ }
+}
diff --git a/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift
new file mode 100644
index 0000000..58c8853
--- /dev/null
+++ b/Sources/AnalyticsGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift
@@ -0,0 +1,44 @@
+import Foundation
+
+public final class HTTPStringResponseSerializer: HTTPResponseSerializer {
+
+ // MARK: - Instance Properties
+
+ public let encoding: String.Encoding
+
+ // MARK: - HTTPResponseSerializer
+
+ public let emptyResponseStatusCodes: Set
+ public let emptyResponseMethods: Set
+
+ // MARK: - Initializers
+
+ public init(
+ encoding: String.Encoding,
+ emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes,
+ emptyResponseMethods: Set = defaultEmptyResponseMethods
+ ) {
+ self.encoding = encoding
+ self.emptyResponseStatusCodes = emptyResponseStatusCodes
+ self.emptyResponseMethods = emptyResponseMethods
+ }
+
+ // MARK: - Instance Methods
+
+ public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> String {
+ guard let string = String(data: data, encoding: encoding) else {
+ throw MessageError("String serialization failed with encoding: \(encoding)")
+ }
+
+ return string
+ }
+}
+
+extension String: HTTPEmptyResponse {
+
+ // MARK: - Type Methods
+
+ public static func emptyResponseInstance() -> String {
+ return ""
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Shared/AnyCodable.swift b/Sources/AnalyticsGenTools/Shared/AnyCodable.swift
new file mode 100644
index 0000000..616ddc4
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Shared/AnyCodable.swift
@@ -0,0 +1,207 @@
+import Foundation
+
+public struct AnyCodable: Codable {
+
+ // MARK: - Instance Properties
+
+ public let value: Any
+
+ // MARK: - Initializers
+
+ public init(_ value: T?) {
+ self.value = value as Any
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+
+ if container.decodeNil() {
+ self.init(nil as Any?)
+ } else {
+ let string = try? container.decode(String.self)
+
+ if let bool = try? container.decode(Bool.self) {
+ if let string = string, string != String(describing: bool) {
+ self.init(string)
+ } else {
+ self.init(bool)
+ }
+ } else if let int = try? container.decode(Int.self) {
+ if let string = string, string != String(describing: int) {
+ self.init(string)
+ } else {
+ self.init(int)
+ }
+ } else if let uint = try? container.decode(UInt.self) {
+ if let string = string, string != String(describing: uint) {
+ self.init(string)
+ } else {
+ self.init(uint)
+ }
+ } else if let double = try? container.decode(Double.self) {
+ if let string = string, string != String(describing: double) {
+ self.init(string)
+ } else {
+ self.init(double)
+ }
+ } else if let string = string {
+ self.init(string)
+ } else if let array = try? container.decode([AnyCodable].self) {
+ self.init(array.map { $0.value })
+ } else if let dictionary = try? container.decode([String: AnyCodable].self) {
+ self.init(dictionary.mapValues { $0.value })
+ } else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "AnyCodable value cannot be decoded"
+ )
+ }
+ }
+ }
+
+ // MARK: - Instance Methods
+
+ public func encode(to encoder: Encoder) throws {
+ // swiftlint:disable:previous function_body_length
+
+ var container = encoder.singleValueContainer()
+
+ switch value {
+ case nil as Any?:
+ try container.encodeNil()
+
+ case let bool as Bool:
+ try container.encode(bool)
+
+ case let int as Int:
+ try container.encode(int)
+
+ case let int8 as Int8:
+ try container.encode(int8)
+
+ case let int16 as Int16:
+ try container.encode(int16)
+
+ case let int32 as Int32:
+ try container.encode(int32)
+
+ case let int64 as Int64:
+ try container.encode(int64)
+
+ case let uint as UInt:
+ try container.encode(uint)
+
+ case let uint8 as UInt8:
+ try container.encode(uint8)
+
+ case let uint16 as UInt16:
+ try container.encode(uint16)
+
+ case let uint32 as UInt32:
+ try container.encode(uint32)
+
+ case let uint64 as UInt64:
+ try container.encode(uint64)
+
+ case let float as Float:
+ try container.encode(float)
+
+ case let double as Double:
+ try container.encode(double)
+
+ case let string as String:
+ try container.encode(string)
+
+ case let date as Date:
+ try container.encode(date)
+
+ case let url as URL:
+ try container.encode(url.absoluteString)
+
+ case let array as [Any?]:
+ try container.encode(array.map(AnyCodable.init))
+
+ case let dictionary as [String: Any?]:
+ try container.encode(dictionary.mapValues(AnyCodable.init))
+
+ default:
+ let context = EncodingError.Context(
+ codingPath: container.codingPath,
+ debugDescription: "AnyCodable value cannot be encoded"
+ )
+
+ throw EncodingError.invalidValue(value, context)
+ }
+ }
+}
+
+extension AnyCodable: Equatable {
+
+ // MARK: - Type Methods
+
+ public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
+ // swiftlint:disable:previous function_body_length
+
+ switch (lhs.value, rhs.value) {
+ case (nil as Any?, nil as Any?):
+ return true
+
+ case let (lhs as Bool, rhs as Bool):
+ return lhs == rhs
+
+ case let (lhs as Int, rhs as Int):
+ return lhs == rhs
+
+ case let (lhs as Int8, rhs as Int8):
+ return lhs == rhs
+
+ case let (lhs as Int16, rhs as Int16):
+ return lhs == rhs
+
+ case let (lhs as Int32, rhs as Int32):
+ return lhs == rhs
+
+ case let (lhs as Int64, rhs as Int64):
+ return lhs == rhs
+
+ case let (lhs as UInt, rhs as UInt):
+ return lhs == rhs
+
+ case let (lhs as UInt8, rhs as UInt8):
+ return lhs == rhs
+
+ case let (lhs as UInt16, rhs as UInt16):
+ return lhs == rhs
+
+ case let (lhs as UInt32, rhs as UInt32):
+ return lhs == rhs
+
+ case let (lhs as UInt64, rhs as UInt64):
+ return lhs == rhs
+
+ case let (lhs as Float, rhs as Float):
+ return lhs == rhs
+
+ case let (lhs as Double, rhs as Double):
+ return lhs == rhs
+
+ case let (lhs as String, rhs as String):
+ return lhs == rhs
+
+ case let (lhs as Date, rhs as Date):
+ return lhs == rhs
+
+ case let (lhs as URL, rhs as URL):
+ return lhs == rhs
+
+ case let (lhs as [String: Any], rhs as [String: Any]):
+ return lhs.mapValues(AnyCodable.init) == rhs.mapValues(AnyCodable.init)
+
+ case let (lhs as [Any], rhs as [Any]):
+ return lhs.map(AnyCodable.init) == rhs.map(AnyCodable.init)
+
+ default:
+ return false
+ }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Shared/AnyCodingKey.swift b/Sources/AnalyticsGenTools/Shared/AnyCodingKey.swift
new file mode 100644
index 0000000..6e2cfd7
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Shared/AnyCodingKey.swift
@@ -0,0 +1,29 @@
+import Foundation
+
+public struct AnyCodingKey: CodingKey {
+
+ // MARK: - Instance Properties
+
+ public let stringValue: String
+ public let intValue: Int?
+
+ // MARK: - Initializers
+
+ public init(_ stringValue: String) {
+ self.stringValue = stringValue
+ self.intValue = nil
+ }
+
+ public init(_ intValue: Int) {
+ self.stringValue = "\(intValue)"
+ self.intValue = intValue
+ }
+
+ public init?(stringValue: String) {
+ self.init(stringValue)
+ }
+
+ public init?(intValue: Int) {
+ self.init(intValue)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Shared/Cache.swift b/Sources/AnalyticsGenTools/Shared/Cache.swift
new file mode 100644
index 0000000..f65bd00
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Shared/Cache.swift
@@ -0,0 +1,66 @@
+import Foundation
+
+public final class Cache {
+
+ // MARK: - Instance Properties
+
+ private let wrapped = NSCache, CacheValue>()
+
+ // MARK: - Initializers
+
+ public init() { }
+
+ // MARK: - Instance Methods
+
+ public func value(forKey key: Key) -> Value? {
+ return wrapped.object(forKey: CacheKey(key))?.value
+ }
+
+ public func setValue(_ value: Value, forKey key: Key) {
+ wrapped.setObject(CacheValue(value), forKey: CacheKey(key))
+ }
+
+ public func removeValue(forKey key: Key) {
+ wrapped.removeObject(forKey: CacheKey(key))
+ }
+}
+
+private final class CacheKey: NSObject {
+
+ // MARK: - Instance Properties
+
+ let key: T
+
+ override var hash: Int {
+ return key.hashValue
+ }
+
+ // MARK: - Initializers
+
+ init(_ key: T) {
+ self.key = key
+ }
+
+ // MARK: - Instance Methods
+
+ override func isEqual(_ object: Any?) -> Bool {
+ guard let object = object as? CacheKey else {
+ return false
+ }
+
+ return key == object.key
+ }
+}
+
+private final class CacheValue {
+
+ // MARK: - Instance Properties
+
+ let value: T
+
+ // MARK: - Initializers
+
+ init(_ value: T) {
+ self.value = value
+ }
+}
diff --git a/Sources/AnalyticsGenTools/Shared/MessageError.swift b/Sources/AnalyticsGenTools/Shared/MessageError.swift
new file mode 100644
index 0000000..23295ad
--- /dev/null
+++ b/Sources/AnalyticsGenTools/Shared/MessageError.swift
@@ -0,0 +1,22 @@
+import Foundation
+
+public struct MessageError: Error, CustomStringConvertible, Hashable {
+
+ // MARK: - Instance Properties
+
+ public let message: String
+
+ public var localizedDescription: String {
+ message
+ }
+
+ public var description: String {
+ "\(type(of: self))(\"\(message)\")"
+ }
+
+ // MARK: - Initializers
+
+ public init(_ message: String) {
+ self.message = message
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLArrayEncodingStrategy.swift b/Sources/AnalyticsGenTools/URLEncoder/URLArrayEncodingStrategy.swift
new file mode 100644
index 0000000..a56a408
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLArrayEncodingStrategy.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+public enum URLArrayEncodingStrategy {
+
+ // MARK: - Enumeration Case
+
+ case brackets
+ case noBrackets
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLBoolEncodingStrategy.swift b/Sources/AnalyticsGenTools/URLEncoder/URLBoolEncodingStrategy.swift
new file mode 100644
index 0000000..5d578a4
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLBoolEncodingStrategy.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+public enum URLBoolEncodingStrategy {
+
+ // MARK: - Enumeration Case
+
+ case numeric
+ case literal
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLDateEncodingStrategy.swift b/Sources/AnalyticsGenTools/URLEncoder/URLDateEncodingStrategy.swift
new file mode 100644
index 0000000..6a4bafb
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLDateEncodingStrategy.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+public enum URLDateEncodingStrategy {
+
+ // MARK: - Enumeration Cases
+
+ case deferredToDate
+
+ case millisecondsSince1970
+ case secondsSince1970
+
+ @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+ case iso8601
+
+ case formatted(DateFormatter)
+ case custom((Date) throws -> String)
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedForm.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedForm.swift
new file mode 100644
index 0000000..4680d85
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedForm.swift
@@ -0,0 +1,3 @@
+import Foundation
+
+internal typealias URLEncodedForm = [String: URLEncodedFormComponent]
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormComponent.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormComponent.swift
new file mode 100644
index 0000000..538ebe4
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormComponent.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+internal enum URLEncodedFormComponent {
+
+ // MARK: - Enumeration Cases
+
+ case string(String)
+ case array([URLEncodedFormComponent])
+ case dictionary([String: URLEncodedFormComponent])
+
+ // MARK: - Instance Properties
+
+ internal var array: [URLEncodedFormComponent]? {
+ switch self {
+ case let .array(array):
+ return array
+
+ default:
+ return nil
+ }
+ }
+
+ internal var dictionary: [String: URLEncodedFormComponent]? {
+ switch self {
+ case let .dictionary(object):
+ return object
+
+ default:
+ return nil
+ }
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormContext.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormContext.swift
new file mode 100644
index 0000000..758b7be
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormContext.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+internal final class URLEncodedFormContext {
+
+ // MARK: - Instance Properties
+
+ internal var component: URLEncodedFormComponent
+
+ // MARK: - Initializers
+
+ internal init(component: URLEncodedFormComponent) {
+ self.component = component
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormEncoder.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormEncoder.swift
new file mode 100644
index 0000000..03cea55
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormEncoder.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+internal final class URLEncodedFormEncoder: Encoder {
+
+ // MARK: - Instance Properties
+
+ internal let context: URLEncodedFormContext
+ internal let boolEncodingStrategy: URLBoolEncodingStrategy
+ internal let dateEncodingStrategy: URLDateEncodingStrategy
+
+ // MARK: - Encoder
+
+ internal var codingPath: [CodingKey]
+
+ internal var userInfo: [CodingUserInfoKey: Any] {
+ return [:]
+ }
+
+ // MARK: - Initializers
+
+ internal init(
+ context: URLEncodedFormContext,
+ boolEncodingStrategy: URLBoolEncodingStrategy,
+ dateEncodingStrategy: URLDateEncodingStrategy,
+ codingPath: [CodingKey] = []
+ ) {
+ self.context = context
+ self.boolEncodingStrategy = boolEncodingStrategy
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.codingPath = codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ internal func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey {
+ let container = URLEncodedFormKeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+
+ return KeyedEncodingContainer(container)
+ }
+
+ internal func unkeyedContainer() -> UnkeyedEncodingContainer {
+ return URLEncodedFormUnkeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+ }
+
+ internal func singleValueContainer() -> SingleValueEncodingContainer {
+ return URLEncodedFormSingleValueEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift
new file mode 100644
index 0000000..a4115e9
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift
@@ -0,0 +1,91 @@
+import Foundation
+
+internal final class URLEncodedFormKeyedEncodingContainer: KeyedEncodingContainerProtocol {
+
+ // MARK: - Instance Properties
+
+ internal let context: URLEncodedFormContext
+ internal let boolEncodingStrategy: URLBoolEncodingStrategy
+ internal let dateEncodingStrategy: URLDateEncodingStrategy
+
+ // MARK: - KeyedEncodingContainerProtocol
+
+ internal var codingPath: [CodingKey]
+
+ // MARK: - Initializers
+
+ internal init(
+ context: URLEncodedFormContext,
+ boolEncodingStrategy: URLBoolEncodingStrategy,
+ dateEncodingStrategy: URLDateEncodingStrategy,
+ codingPath: [CodingKey]
+ ) {
+ self.context = context
+ self.boolEncodingStrategy = boolEncodingStrategy
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.codingPath = codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ internal func encodeNil(forKey key: Key) throws {
+ let errorContext = EncodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Nil values cannot be encoded in URL"
+ )
+
+ throw EncodingError.invalidValue("nil", errorContext)
+ }
+
+ internal func encode(_ value: T, forKey key: Key) throws {
+ let container = URLEncodedFormSingleValueEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(key)
+ )
+
+ try container.encode(value)
+ }
+
+ internal func nestedContainer(
+ keyedBy keyType: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let container = URLEncodedFormKeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(key)
+ )
+
+ return KeyedEncodingContainer(container)
+ }
+
+ internal func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
+ return URLEncodedFormUnkeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(key)
+ )
+ }
+
+ internal func superEncoder() -> Encoder {
+ return URLEncodedFormEncoder(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+ }
+
+ internal func superEncoder(forKey key: Key) -> Encoder {
+ return URLEncodedFormEncoder(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(key)
+ )
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSerializer.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSerializer.swift
new file mode 100644
index 0000000..f711cc1
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSerializer.swift
@@ -0,0 +1,108 @@
+import Foundation
+
+internal final class URLEncodedFormSerializer {
+
+ // MARK: - Instance Properties
+
+ internal let arrayEncodingStrategy: URLArrayEncodingStrategy
+ internal let spaceEncodingStrategy: URLSpaceEncodingStrategy
+
+ // MARK: - Initializers
+
+ internal init(
+ arrayEncodingStrategy: URLArrayEncodingStrategy,
+ spaceEncodingStrategy: URLSpaceEncodingStrategy
+ ) {
+ self.arrayEncodingStrategy = arrayEncodingStrategy
+ self.spaceEncodingStrategy = spaceEncodingStrategy
+ }
+
+ // MARK: - Instance Methods
+
+ private func escapeString(_ string: String) -> String {
+ var allowedCharacters = CharacterSet.urlQueryAllowed
+
+ allowedCharacters.remove(charactersIn: .urlDelimiters)
+ allowedCharacters.insert(charactersIn: .urlUnescapedSpace)
+
+ let escapedString = string.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? string
+
+ switch spaceEncodingStrategy {
+ case .percentEscaped:
+ return escapedString.replacingOccurrences(
+ of: String.urlUnescapedSpace,
+ with: String.urlPercentEscapedSpace
+ )
+
+ case .plusReplaced:
+ return escapedString.replacingOccurrences(
+ of: String.urlUnescapedSpace,
+ with: String.urlPlusReplacedSpace
+ )
+ }
+ }
+
+ private func serializeComponent(_ component: URLEncodedFormComponent, key: String) -> String {
+ switch component {
+ case let .string(value):
+ return .urlStringComponent(key: escapeString(key), value: escapeString(value))
+
+ case let .array(value):
+ return value
+ .map { element in
+ switch arrayEncodingStrategy {
+ case .brackets:
+ return serializeComponent(element, key: .urlBracketsArrayComponentKey(key))
+
+ case .noBrackets:
+ return serializeComponent(element, key: .urlNoBracketsArrayComponentKey(key))
+ }
+ }
+ .joined(separator: .urlComponentSeparator)
+
+ case let .dictionary(value):
+ return value
+ .map { serializeComponent($0.value, key: .urlDictionaryComponentKey(key, elementKey: $0.key)) }
+ .joined(separator: .urlComponentSeparator)
+ }
+ }
+
+ // MARK: -
+
+ internal func serialize(_ form: URLEncodedForm) -> String {
+ return form
+ .map { serializeComponent($0.value, key: $0.key) }
+ .joined(separator: .urlComponentSeparator)
+ }
+}
+
+private extension String {
+
+ // MARK: - Type Properties
+
+ static let urlDelimiters = ":#[]@!$&'()*+,;="
+ static let urlUnescapedSpace = " "
+
+ static let urlPercentEscapedSpace = "%20"
+ static let urlPlusReplacedSpace = "+"
+
+ static let urlComponentSeparator = "&"
+
+ // MARK: - Type Methods
+
+ static func urlStringComponent(key: String, value: String) -> String {
+ return "\(key)=\(value)"
+ }
+
+ static func urlBracketsArrayComponentKey(_ key: String) -> String {
+ return "\(key)[]"
+ }
+
+ static func urlNoBracketsArrayComponentKey(_ key: String) -> String {
+ return key
+ }
+
+ static func urlDictionaryComponentKey(_ key: String, elementKey: String) -> String {
+ return "\(key)[\(elementKey)]"
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift
new file mode 100644
index 0000000..34af99e
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift
@@ -0,0 +1,257 @@
+import Foundation
+
+internal final class URLEncodedFormSingleValueEncodingContainer: SingleValueEncodingContainer {
+
+ // MARK: - Instance Properties
+
+ private var canEncodeNextValue = true
+
+ // MARK: -
+
+ internal let context: URLEncodedFormContext
+ internal let boolEncodingStrategy: URLBoolEncodingStrategy
+ internal let dateEncodingStrategy: URLDateEncodingStrategy
+
+ // MARK: - SingleValueEncodingContainer
+
+ internal var codingPath: [CodingKey]
+
+ // MARK: - Initializers
+
+ internal init(
+ context: URLEncodedFormContext,
+ boolEncodingStrategy: URLBoolEncodingStrategy,
+ dateEncodingStrategy: URLDateEncodingStrategy,
+ codingPath: [CodingKey]
+ ) {
+ self.context = context
+ self.boolEncodingStrategy = boolEncodingStrategy
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.codingPath = codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ private func markAsEncoded(_ value: Any?) throws {
+ guard canEncodeNextValue else {
+ let errorContext = EncodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Single value container has already encoded value"
+ )
+
+ throw EncodingError.invalidValue(value as Any, errorContext)
+ }
+
+ canEncodeNextValue = false
+ }
+
+ private func updatedComponent(
+ _ component: URLEncodedFormComponent,
+ with value: URLEncodedFormComponent,
+ at path: [CodingKey]
+ ) -> URLEncodedFormComponent {
+ guard let pathKey = path.first else {
+ return value
+ }
+
+ var child: URLEncodedFormComponent
+
+ switch path.count {
+ case 1:
+ child = value
+
+ default:
+ if let index = pathKey.intValue {
+ let array = component.array ?? []
+
+ if index < array.count {
+ child = updatedComponent(array[index], with: value, at: Array(path[1...]))
+ } else {
+ child = updatedComponent(.array([]), with: value, at: Array(path[1...]))
+ }
+ } else {
+ child = updatedComponent(
+ component.dictionary?[pathKey.stringValue] ?? .dictionary([:]),
+ with: value,
+ at: Array(path[1...])
+ )
+ }
+ }
+
+ if let childIndex = pathKey.intValue {
+ guard var array = component.array else {
+ return .array([child])
+ }
+
+ if childIndex < array.count {
+ array[childIndex] = child
+ } else {
+ array.append(child)
+ }
+
+ return .array(array)
+ } else {
+ guard var dictionary = component.dictionary else {
+ return .dictionary([pathKey.stringValue: child])
+ }
+
+ dictionary[pathKey.stringValue] = child
+
+ return .dictionary(dictionary)
+ }
+ }
+
+ private func encode(_ value: T, as string: String) throws {
+ try markAsEncoded(value)
+
+ context.component = updatedComponent(context.component, with: .string(string), at: codingPath)
+ }
+
+ private func encode(_ date: Date) throws {
+ switch dateEncodingStrategy {
+ case .deferredToDate:
+ try markAsEncoded(date)
+
+ let encoder = URLEncodedFormEncoder(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+
+ try date.encode(to: encoder)
+
+ case .millisecondsSince1970:
+ try encode(date, as: String(date.timeIntervalSince1970 * 1000.0))
+
+ case .secondsSince1970:
+ try encode(date, as: String(date.timeIntervalSince1970))
+
+ case .iso8601:
+ guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
+ fatalError("ISO8601DateFormatter is unavailable on this platform.")
+ }
+
+ let formattedDate = ISO8601DateFormatter.string(
+ from: date,
+ timeZone: Contants.iso8601TimeZone,
+ formatOptions: .withInternetDateTime
+ )
+
+ try encode(date, as: formattedDate)
+
+ case let .formatted(dateFormatter):
+ try encode(date, as: dateFormatter.string(from: date))
+
+ case let .custom(closure):
+ try encode(date, as: try closure(date))
+ }
+ }
+
+ // MARK: - SingleValueEncodingContainer
+
+ internal func encodeNil() throws {
+ try markAsEncoded(nil)
+
+ let errorContext = EncodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Nil values cannot be encoded in URL"
+ )
+
+ throw EncodingError.invalidValue("nil", errorContext)
+ }
+
+ internal func encode(_ value: Bool) throws {
+ switch boolEncodingStrategy {
+ case .numeric:
+ try encode(value, as: value ? Contants.numericTrue : Contants.numericFalse)
+
+ case .literal:
+ try encode(value, as: value ? Contants.literalTrue : Contants.literalFalse)
+ }
+ }
+
+ internal func encode(_ value: String) throws {
+ try encode(value, as: value)
+ }
+
+ internal func encode(_ value: Double) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Float) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Int) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Int8) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Int16) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Int32) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: Int64) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: UInt) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: UInt8) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: UInt16) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: UInt32) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: UInt64) throws {
+ try encode(value, as: String(value))
+ }
+
+ internal func encode(_ value: T) throws {
+ switch value {
+ case let date as Date:
+ try encode(date)
+
+ default:
+ try markAsEncoded(value)
+
+ let encoder = URLEncodedFormEncoder(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+
+ try value.encode(to: encoder)
+ }
+ }
+}
+
+private enum Contants {
+
+ // MARK: - Type Properties
+
+ static let numericTrue = "1"
+ static let numericFalse = "0"
+
+ static let literalTrue = "true"
+ static let literalFalse = "false"
+
+ static let iso8601TimeZone = TimeZone(secondsFromGMT: 0)!
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift
new file mode 100644
index 0000000..d1d5c13
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift
@@ -0,0 +1,98 @@
+import Foundation
+
+internal final class URLEncodedFormUnkeyedEncodingContainer: UnkeyedEncodingContainer {
+
+ // MARK: - Instance Properties
+
+ internal let context: URLEncodedFormContext
+ internal let boolEncodingStrategy: URLBoolEncodingStrategy
+ internal let dateEncodingStrategy: URLDateEncodingStrategy
+
+ // MARK: - UnkeyedEncodingContainer
+
+ internal var codingPath: [CodingKey]
+ internal var count = 0
+
+ // MARK: - Initializers
+
+ internal init(
+ context: URLEncodedFormContext,
+ boolEncodingStrategy: URLBoolEncodingStrategy,
+ dateEncodingStrategy: URLDateEncodingStrategy,
+ codingPath: [CodingKey]
+ ) {
+ self.context = context
+ self.boolEncodingStrategy = boolEncodingStrategy
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.codingPath = codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ internal func encodeNil() throws {
+ let errorContext = EncodingError.Context(
+ codingPath: codingPath,
+ debugDescription: "Nil values cannot be encoded in URL"
+ )
+
+ throw EncodingError.invalidValue("nil", errorContext)
+ }
+
+ internal func encode(_ value: T) throws {
+ defer {
+ count += 1
+ }
+
+ let container = URLEncodedFormSingleValueEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(AnyCodingKey(count))
+ )
+
+ try container.encode(value)
+ }
+
+ internal func nestedContainer(
+ keyedBy keyType: NestedKey.Type
+ ) -> KeyedEncodingContainer {
+ defer {
+ count += 1
+ }
+
+ let container = URLEncodedFormKeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(AnyCodingKey(count))
+ )
+
+ return KeyedEncodingContainer(container)
+ }
+
+ internal func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
+ defer {
+ count += 1
+ }
+
+ return URLEncodedFormUnkeyedEncodingContainer(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath.appending(AnyCodingKey(count))
+ )
+ }
+
+ internal func superEncoder() -> Encoder {
+ defer {
+ count += 1
+ }
+
+ return URLEncodedFormEncoder(
+ context: context,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy,
+ codingPath: codingPath
+ )
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLEncoder.swift b/Sources/AnalyticsGenTools/URLEncoder/URLEncoder.swift
new file mode 100644
index 0000000..42d9207
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLEncoder.swift
@@ -0,0 +1,65 @@
+import Foundation
+
+public final class URLEncoder {
+
+ // MARK: - Type Properties
+
+ public static let `default` = URLEncoder()
+
+ // MARK: - Instance Properties
+
+ public var boolEncodingStrategy: URLBoolEncodingStrategy
+ public var dateEncodingStrategy: URLDateEncodingStrategy
+ public var arrayEncodingStrategy: URLArrayEncodingStrategy
+ public var spaceEncodingStrategy: URLSpaceEncodingStrategy
+
+ // MARK: - Initializers
+
+ public init(
+ boolEncodingStrategy: URLBoolEncodingStrategy = .numeric,
+ dateEncodingStrategy: URLDateEncodingStrategy = .deferredToDate,
+ arrayEncodingStrategy: URLArrayEncodingStrategy = .brackets,
+ spaceEncodingStrategy: URLSpaceEncodingStrategy = .percentEscaped
+ ) {
+ self.boolEncodingStrategy = boolEncodingStrategy
+ self.dateEncodingStrategy = dateEncodingStrategy
+ self.arrayEncodingStrategy = arrayEncodingStrategy
+ self.spaceEncodingStrategy = spaceEncodingStrategy
+ }
+
+ // MARK: - Instance Methods
+
+ public func encodeToQuery(_ value: T) throws -> String {
+ let urlEncodedFormContext = URLEncodedFormContext(component: .dictionary([:]))
+
+ let urlEncodedFormEncoder = URLEncodedFormEncoder(
+ context: urlEncodedFormContext,
+ boolEncodingStrategy: boolEncodingStrategy,
+ dateEncodingStrategy: dateEncodingStrategy
+ )
+
+ try value.encode(to: urlEncodedFormEncoder)
+
+ guard case let .dictionary(urlEncodedForm) = urlEncodedFormContext.component else {
+ let errorContext = EncodingError.Context(
+ codingPath: [],
+ debugDescription: "Root component cannot be encoded in URL"
+ )
+
+ throw EncodingError.invalidValue("\(value)", errorContext)
+ }
+
+ let serializer = URLEncodedFormSerializer(
+ arrayEncodingStrategy: arrayEncodingStrategy,
+ spaceEncodingStrategy: spaceEncodingStrategy
+ )
+
+ return serializer.serialize(urlEncodedForm)
+ }
+
+ public func encode(_ value: T) throws -> Data {
+ let query = try encodeToQuery(value)
+
+ return Data(query.utf8)
+ }
+}
diff --git a/Sources/AnalyticsGenTools/URLEncoder/URLSpaceEncodingStrategy.swift b/Sources/AnalyticsGenTools/URLEncoder/URLSpaceEncodingStrategy.swift
new file mode 100644
index 0000000..d9371bc
--- /dev/null
+++ b/Sources/AnalyticsGenTools/URLEncoder/URLSpaceEncodingStrategy.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+public enum URLSpaceEncodingStrategy {
+
+ // MARK: - Enumeration Cases
+
+ case percentEscaped
+ case plusReplaced
+}
diff --git a/Templates/ColorStyles.stencil b/Templates/ColorStyles.stencil
new file mode 100644
index 0000000..a2c12b8
--- /dev/null
+++ b/Templates/ColorStyles.stencil
@@ -0,0 +1,82 @@
+{% include "FileHeader.stencil" %}
+{% if colorStyles %}
+{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set styleTypeName %}{{ options.styleTypeName|default:"ColorStyle" }}{% endset %}
+{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %}
+{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %}
+{% macro styleMutator propertyName propertyTypeName %}
+ {% set methodName %}with{{ propertyName|upperFirstLetter }}{% endset %}
+ {{ accessModifier }} func {{ methodName }}(_ {{propertyName}}: {{ propertyTypeName }}) -> {{ styleTypeName }} {
+ return {{ styleTypeName }}(
+ red: red,
+ green: green,
+ blue: blue,
+ alpha: alpha
+ )
+ }
+{% endmacro %}
+
+#if canImport(UIKit)
+import UIKit
+#else
+import AppKit
+#endif
+
+{{ accessModifier }} struct {{ styleTypeName }}: Equatable {
+
+ // MARK: - Type Properties
+{% for style in colorStyles %}
+
+ /// {{ style.name }}
+ ///
+ /// {{ style.color|colorInfo }}
+ {{ accessModifier }} static let {% call propertyName style.name %} = {{ styleTypeName }}(
+ red: {{ style.color.red }},
+ green: {{ style.color.green }},
+ blue: {{ style.color.blue }},
+ alpha: {{ style.color.alpha }}
+ )
+{% endfor %}
+
+ // MARK: - Instance Properties
+
+ {{ accessModifier }} let red: CGFloat
+ {{ accessModifier }} let green: CGFloat
+ {{ accessModifier }} let blue: CGFloat
+ {{ accessModifier }} let alpha: CGFloat
+
+ {{ accessModifier }} var color: {{ colorTypeName }} {
+ return {{ colorTypeName }}(style: self)
+ }
+
+ // MARK: - Initializers
+
+ {{ accessModifier }} init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) {
+ self.red = red
+ self.green = green
+ self.blue = blue
+ self.alpha = alpha
+ }
+
+ // MARK: - Instance Methods
+
+ {% call styleMutator "red" "CGFloat" %}
+
+ {% call styleMutator "green" "CGFloat" %}
+
+ {% call styleMutator "blue" "CGFloat" %}
+
+ {% call styleMutator "alpha" "CGFloat" %}
+}
+
+{{ accessModifier }} extension {{ colorTypeName }} {
+
+ // MARK: - Initializers
+
+ convenience init(style: {{ styleTypeName }}) {
+ self.init(red: style.red, green: style.green, blue: style.blue, alpha: style.alpha)
+ }
+}
+{% else %}
+// No color style found
+{% endif %}
diff --git a/Templates/Events.stencil b/Templates/Events.stencil
new file mode 100644
index 0000000..d1870b6
--- /dev/null
+++ b/Templates/Events.stencil
@@ -0,0 +1,39 @@
+{% include "FileHeader.stencil" %}
+
+import Foundation
+
+{% for event in events %}
+
+// MARK: - {{ event.name }}Event
+
+/// {{ event.description }}
+struct {{ event.name }}Event {
+
+ // MARK: - Instance Properties
+
+ let name = "{{ event.name }}"
+
+ {% for parameter in {{ event.parameters }} %}
+ /// {{ parameter.description }}
+ let {{ parameter.name }}: {{ parameter.type }}{% if parameter.isOptional %}?{% endif %}
+ {% endfor %}
+}
+
+// MARK: - EncodableEvent
+
+extension {{ event.name }}Event: EncodableEvent {
+
+ // MARK: - Instance Methods
+
+ func encode() -> EventData {
+ let parameters: [String: Any] = [
+ {% for parameter in {{ event.parameters}} %}
+ "{{ parameter.name }}": self.{{ parameter.name }},
+ {% endfor %}
+ ]
+
+ return EventData(name: self.name, parameters: parameters)
+ }
+}
+
+{% endfor %}
diff --git a/Templates/FileHeader.stencil b/Templates/FileHeader.stencil
new file mode 100644
index 0000000..e4820ae
--- /dev/null
+++ b/Templates/FileHeader.stencil
@@ -0,0 +1,2 @@
+// swiftlint:disable all
+// Generated using AnalyticsGen
diff --git a/Templates/Flurry.stencil b/Templates/Flurry.stencil
new file mode 100644
index 0000000..e69de29
diff --git a/Templates/Trackers.stencil b/Templates/Trackers.stencil
new file mode 100644
index 0000000..d1c2358
--- /dev/null
+++ b/Templates/Trackers.stencil
@@ -0,0 +1,84 @@
+{% include "FileHeader.stencil" %}
+
+import Foundation
+
+// MARK: - EventData
+
+struct EventData {
+
+ // MARK: - Instance Properties
+
+ let name: String
+ let parameters: [String: Any]
+}
+
+// MARK: - EncodableEvent
+
+protocol EncodableEvent: AnalyticsEvent {
+
+ // MARK: - Instance Methods
+
+ func encode() -> EventData
+}
+
+{% for tracker in trackers %}
+
+// MARK: - {{ tracker.name }}EventTracker
+
+import {{ tracker.import }}
+
+class {{ tracker.name }}EventTracker: AnalyticsEventTracker {
+
+ // MARK: - AnalyticsEventTracker
+
+ func trackEvent(_ event: AnalyticsEvent) {
+ guard let event = event as? EncodableEvent else {
+ return
+ }
+
+ let data = event.encode()
+
+ {% if tracker.name == "Flurry" %}
+ Flurry.logEvent(data.name, withParameters: data.parameters)
+ {% elif tracker.name == "Firebase" %}
+ Analytics.logEvent(data.name, parameters: data.parameters)
+ {% endif %}
+ }
+}
+
+{% for event in tracker.events %}
+
+// MARK: - {{ event.name }}Event
+
+/// {{ event.description }}
+struct {{ event.name }}Event {
+
+ // MARK: - Instance Properties
+
+ let name = "{{ event.name }}"
+ {% for parameter in event.parameters %}
+
+ /// {{ parameter.description }}
+ let {{ parameter.name }}: {{ parameter.type }}{% if parameter.isOptional %}?{% endif %}
+ {% endfor %}
+}
+
+// MARK: - EncodableEvent
+
+extension {{ event.name }}Event: EncodableEvent {
+
+ // MARK: - Instance Methods
+
+ func encode() -> EventData {
+ let parameters: [String: Any] = [
+ {% for parameter in event.parameters %}
+ "{{ parameter.name }}": self.{{ parameter.name }},
+ {% endfor %}
+ ]
+
+ return EventData(name: self.name, parameters: parameters)
+ }
+}
+
+{% endfor %}
+{% endfor %}
\ No newline at end of file
diff --git a/Tests/AnalyticsGenTests/AnalyticsGenTests.swift b/Tests/AnalyticsGenTests/AnalyticsGenTests.swift
new file mode 100644
index 0000000..8120c60
--- /dev/null
+++ b/Tests/AnalyticsGenTests/AnalyticsGenTests.swift
@@ -0,0 +1,47 @@
+import XCTest
+import class Foundation.Bundle
+
+final class AnalyticsGenTests: 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.
+
+ // Some of the APIs that we use below are available in macOS 10.13 and above.
+ guard #available(macOS 10.13, *) else {
+ return
+ }
+
+ let fooBinary = productsDirectory.appendingPathComponent("AnalyticsGen")
+
+ let process = Process()
+ process.executableURL = fooBinary
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+
+ try process.run()
+ process.waitUntilExit()
+
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ let output = String(data: data, encoding: .utf8)
+
+ XCTAssertEqual(output, "Hello, world!\n")
+ }
+
+ /// Returns path to the built products directory.
+ var productsDirectory: URL {
+ #if os(macOS)
+ for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
+ return bundle.bundleURL.deletingLastPathComponent()
+ }
+ fatalError("couldn't find the products directory")
+ #else
+ return Bundle.main.bundleURL
+ #endif
+ }
+
+ static var allTests = [
+ ("testExample", testExample),
+ ]
+}
diff --git a/Tests/AnalyticsGenTests/XCTestManifests.swift b/Tests/AnalyticsGenTests/XCTestManifests.swift
new file mode 100644
index 0000000..be3f90e
--- /dev/null
+++ b/Tests/AnalyticsGenTests/XCTestManifests.swift
@@ -0,0 +1,9 @@
+import XCTest
+
+#if !canImport(ObjectiveC)
+public func allTests() -> [XCTestCaseEntry] {
+ return [
+ testCase(AnalyticsGenTests.allTests),
+ ]
+}
+#endif
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
new file mode 100644
index 0000000..d9603bb
--- /dev/null
+++ b/Tests/LinuxMain.swift
@@ -0,0 +1,7 @@
+import XCTest
+
+import AnalyticsGenTests
+
+var tests = [XCTestCaseEntry]()
+tests += AnalyticsGenTests.allTests()
+XCTMain(tests)