Compare commits
No commits in common. "d79af06c38b2c30c829100e2a0494e8da7d8b089" and "7d6bb4fcb92d883cf3a7397d8c00a0686ffb538d" have entirely different histories.
d79af06c38
...
7d6bb4fcb9
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -1,6 +1,6 @@
|
||||
library "openiumpipeline"
|
||||
|
||||
env.DEVELOPER_DIR="/Applications/Xcode-15.0.1.app/Contents/Developer"
|
||||
env.DEVELOPER_DIR= "/Applications/Xcode-14.3.0.app/Contents/Developer"
|
||||
//env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-"
|
||||
env.IS_PACKAGE_SWIFT=1
|
||||
env.TARGETS_MACOS=1
|
||||
|
65
README.md
65
README.md
@ -133,71 +133,6 @@ swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
|
||||
|
||||
> ⚠️ If extension name is not set or is `Tags`, it will generate the following typaloas `typealias Tags = String`.
|
||||
|
||||
|
||||
## Analytics
|
||||
|
||||
Analytics will generate all you need to analyze UX with Matomo or Firebase Analytics. Input files are formatted in YAML. This command will generate a manager for each target and an AnalyticsManager. This is this one you will need to use. And it will generate a method for all tags you have declared in the YAML file. Next, you will need to use the `configure()` method of AnalyticsManager and if you want to use matomo to set up the `siteId` and the `url` of the site.
|
||||
|
||||
```sh
|
||||
swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
|
||||
--target "matomo firebase" \
|
||||
--extension-output-path "./Analytics/Generated" \
|
||||
--extension-name "AppAnalytics" \
|
||||
--extension-suffix "GreatApp" \
|
||||
--static-members true
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
1. `-f`: force generation
|
||||
2. Input tags file (must be YAML formatted)
|
||||
3. `--target`: target with you will log UX
|
||||
4. `--extension-output-path`: path where to generate generated extension
|
||||
5. `--extension-name` *(optional)* : name of class to add the extension
|
||||
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppAnalytics+GreatApp.swift`)
|
||||
7. `--static-members` *(optional)*: generate static properties or not
|
||||
|
||||
> ⚠️ If extension name is not set or is `Analytics`, it will generate the following typaloas `typealias Analytics = String`.
|
||||
|
||||
### YAML
|
||||
|
||||
```
|
||||
- id: s1_def_one
|
||||
name: s1 def one _TITLE_
|
||||
path: s1_def_one/_TITLE_
|
||||
action: Tap
|
||||
category: User
|
||||
tags: ios,droid
|
||||
comments:
|
||||
parameters:
|
||||
- name: title
|
||||
type: String
|
||||
replaceIn: name,path
|
||||
```
|
||||
|
||||
1. `id`: name of the method (method name will be composed of `log` + `Event|Screen` + `id`)
|
||||
2. `name`: name of the tag
|
||||
3. `path` *(optional with firebase)* : needed for matomo but not with firebase (log screen)
|
||||
4. `action` *(optional with firebase)* : needed for matomo but not with firebase (log event)
|
||||
5. `category` *(optional with firebase)* : needed for matomo but not with firebase (log event)
|
||||
6. `tags`: which platform target
|
||||
7. `comments` *(optional)*
|
||||
8. `parameters` *(optional)*
|
||||
|
||||
**Parameters**
|
||||
|
||||
You can use parameters in generate methods.
|
||||
|
||||
1. `name`: name of the parameter
|
||||
2. `type`: type of the parameter (Int, String, Bool, Double)
|
||||
3. `replaceIn` *(optional)*
|
||||
|
||||
**Replace in**
|
||||
|
||||
This is section is equivalent of `%s | %d | %f | %@`. You can put the content of the parameter in *name*, *path*, *action*, *category*.
|
||||
You need to put `_` + `NAME OF THE PARAMETER` + `_` in the target and which target you want in the value of `replaceIn`. (name need to be in uppercase)
|
||||
|
||||
|
||||
## Images
|
||||
|
||||
Images generator will generate images assets along with extensions to access those images easily.
|
||||
|
@ -1,199 +0,0 @@
|
||||
// Generated by ResgenSwift.Analytics 1.2
|
||||
|
||||
import MatomoTracker
|
||||
import FirebaseAnalytics
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String)
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Matomo
|
||||
|
||||
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var tracker: MatomoTracker
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(siteId: String, url: String) {
|
||||
debugPrint("[Matomo service] Server URL: \(url)")
|
||||
debugPrint("[Matomo service] Site ID: \(siteId)")
|
||||
tracker = MatomoTracker(
|
||||
siteId: siteId,
|
||||
baseURL: URL(string: url)!
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
tracker.dispatchInterval = 5
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
tracker.logger = DefaultLogger(minLevel: .verbose)
|
||||
#endif
|
||||
|
||||
debugPrint("[Matomo service] Configured with content base: \(tracker.contentBase?.absoluteString ?? "-")")
|
||||
debugPrint("[Matomo service] Opt out: \(tracker.isOptedOut)")
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func logScreen(name: String, path: String) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
|
||||
|
||||
let urlString = URL(string: "\(trackerUrl)" + "/" + "\(path)" + "iOS")
|
||||
tracker.track(
|
||||
view: [name],
|
||||
url: urlString
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
|
||||
tracker.track(
|
||||
eventWithCategory: category,
|
||||
action: action,
|
||||
name: name,
|
||||
number: nil,
|
||||
url: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Firebase
|
||||
|
||||
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String) {
|
||||
var parameters = [
|
||||
AnalyticsParameterScreenName: name
|
||||
]
|
||||
|
||||
Analytics.logEvent(
|
||||
AnalyticsEventScreenView,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
var parameters: [String:Any] = [
|
||||
"action": action,
|
||||
"category": category,
|
||||
]
|
||||
|
||||
if let supplementaryParameters = params {
|
||||
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.logEvent(
|
||||
name,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manager
|
||||
|
||||
class AnalyticsManager {
|
||||
static var shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var managers: [AnalyticsManagerProtocol] = []
|
||||
|
||||
private var isEnabled: Bool = true
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setAnalyticsEnabled(_ enable: Bool) {
|
||||
isEnabled = enable
|
||||
}
|
||||
|
||||
func configure(siteId: String, url: String) {
|
||||
managers.append(
|
||||
MatomoAnalyticsManager(
|
||||
siteId: siteId,
|
||||
url: url
|
||||
)
|
||||
)
|
||||
managers.append(FirebaseAnalyticsManager())
|
||||
}
|
||||
|
||||
private func logScreen(name: String, path: String) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logScreen(name: name, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logEvent(
|
||||
name: name,
|
||||
action: action,
|
||||
category: category,
|
||||
params: params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
func logScreenS1DefOne(title: String) {
|
||||
logScreen(
|
||||
name: "s1 def one \(title)",
|
||||
path: "s1_def_one/\(title)"
|
||||
)
|
||||
}
|
||||
|
||||
func logEventS1DefTwo(title: String, count: String) {
|
||||
logEvent(
|
||||
name: "s1 def two",
|
||||
action: "test",
|
||||
category: "test",
|
||||
params: [
|
||||
"title": title,
|
||||
"count": count
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - section_two
|
||||
|
||||
func logScreenS2DefOne() {
|
||||
logScreen(
|
||||
name: "s2 def one",
|
||||
path: "s2_def_one/"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
---
|
||||
categories:
|
||||
- id: section_one
|
||||
screens:
|
||||
- id: s1_def_one
|
||||
name: s1 def one _TITLE_
|
||||
path: s1_def_one/_TITLE_
|
||||
tags: ios,droid
|
||||
parameters:
|
||||
- name: title
|
||||
type: String
|
||||
replaceIn: name,path
|
||||
|
||||
events:
|
||||
- id: s1_def_two
|
||||
name: s1 def two
|
||||
action: test
|
||||
category: test
|
||||
tags: ios
|
||||
parameters:
|
||||
- name: title
|
||||
type: String
|
||||
- name: count
|
||||
type: String
|
||||
|
||||
- id: section_two
|
||||
screens:
|
||||
- id: s2_def_one
|
||||
name: s2 def one
|
||||
path: s2_def_one/
|
||||
tags: ios
|
||||
|
||||
events:
|
||||
- id: s2_def_two
|
||||
name: s2 def two
|
||||
action: test
|
||||
category: test
|
||||
tags: droid
|
||||
|
||||
- id: section_three
|
||||
screens:
|
||||
- id: s3_def_one
|
||||
name: s3 def one
|
||||
path: s3_def_one/
|
||||
tags: droid
|
||||
|
||||
events:
|
||||
- id: s3_def_two
|
||||
name: s3 def two
|
||||
action: test
|
||||
category: test
|
||||
tags: droid
|
@ -30,18 +30,18 @@ FORCE_FLAG="$1"
|
||||
# --default-lang "en" \
|
||||
# --extension-output-path "./Twine/Generated"
|
||||
|
||||
#echo "\n-------------------------\n"
|
||||
echo "\n-------------------------\n"
|
||||
|
||||
## Strings
|
||||
#swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
|
||||
# --output-path "./Strings/Generated" \
|
||||
# --langs "fr en en-us" \
|
||||
# --default-lang "en" \
|
||||
# --extension-output-path "./Strings/Generated" \
|
||||
# --extension-name "String" \
|
||||
# --extension-suffix "GenAllScript"
|
||||
# Strings
|
||||
swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
|
||||
--output-path "./Strings/Generated" \
|
||||
--langs "fr en en-us" \
|
||||
--default-lang "en" \
|
||||
--extension-output-path "./Strings/Generated" \
|
||||
--extension-name "String" \
|
||||
--extension-suffix "GenAllScript"
|
||||
|
||||
#echo "\n-------------------------\n"
|
||||
echo "\n-------------------------\n"
|
||||
|
||||
## Tags
|
||||
#swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
|
||||
@ -50,16 +50,7 @@ FORCE_FLAG="$1"
|
||||
# --extension-name "Tags" \
|
||||
# --extension-suffix "GenAllScript"
|
||||
#
|
||||
echo "\n-------------------------\n"
|
||||
|
||||
# Analytics
|
||||
swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \
|
||||
--target "matomo firebase" \
|
||||
--extension-output-path "./Tags/Generated" \
|
||||
--extension-name "Analytics" \
|
||||
--extension-suffix "GenAllScript"
|
||||
|
||||
echo "\n-------------------------\n"
|
||||
#echo "\n-------------------------\n"
|
||||
#
|
||||
## Images
|
||||
#swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \
|
||||
|
@ -78,18 +78,6 @@ tags:
|
||||
extensionSuffix: GenAllScript
|
||||
|
||||
|
||||
#
|
||||
# Analytics
|
||||
#
|
||||
analytics:
|
||||
-
|
||||
inputFile: ./Tags/sampleTags.yml
|
||||
target: "matomo firebase"
|
||||
extensionOutputPath: ./Tags/Generated
|
||||
extensionName: Analytics
|
||||
extensionSuffix: GenAllScript
|
||||
|
||||
|
||||
#
|
||||
# Fonts
|
||||
#
|
||||
|
@ -1,87 +0,0 @@
|
||||
//
|
||||
// Analytics.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 08/12/2023.
|
||||
//
|
||||
|
||||
import ToolCore
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct Analytics: ParsableCommand {
|
||||
|
||||
// MARK: - Command Configuration
|
||||
|
||||
static var configuration = CommandConfiguration(
|
||||
abstract: "Generate analytics extension file.",
|
||||
version: ResgenSwiftVersion
|
||||
)
|
||||
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static let toolName = "Analytics"
|
||||
static let defaultExtensionName = "Analytics"
|
||||
|
||||
// MARK: - Command Options
|
||||
|
||||
@OptionGroup var options: AnalyticsOptions
|
||||
|
||||
// MARK: - Run
|
||||
|
||||
mutating func run() {
|
||||
print("[\(Self.toolName)] Starting analytics generation")
|
||||
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
print("[\(Self.toolName)] Will generate analytics")
|
||||
|
||||
// Check requirements
|
||||
guard checkRequirements() else { return }
|
||||
|
||||
// Parse input file
|
||||
let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target)
|
||||
|
||||
// Generate extension
|
||||
AnalyticsGenerator.writeExtensionFiles(sections: sections,
|
||||
target: options.target,
|
||||
tags: ["ios", "iosonly"],
|
||||
staticVar: options.staticMembers,
|
||||
extensionName: options.extensionName,
|
||||
extensionFilePath: options.extensionFilePath)
|
||||
|
||||
print("[\(Self.toolName)] Analytics generated")
|
||||
}
|
||||
|
||||
// MARK: - Requirements
|
||||
|
||||
private func checkRequirements() -> Bool {
|
||||
let fileManager = FileManager()
|
||||
|
||||
// Input file
|
||||
guard fileManager.fileExists(atPath: options.inputFile) else {
|
||||
let error = AnalyticsError.fileNotExists(options.inputFile)
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
guard TrackerType.hasValidTarget(in: options.target) else {
|
||||
let error = AnalyticsError.noValidTracker(options.target)
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
// Check if needed to regenerate
|
||||
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
|
||||
inputFilePath: options.inputFile,
|
||||
extensionFilePath: options.extensionFilePath) else {
|
||||
print("[\(Self.toolName)] Analytics are already up to date :) ")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
//
|
||||
// AnalyticsError.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 11/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AnalyticsError: Error {
|
||||
case noValidTracker(String)
|
||||
case fileNotExists(String)
|
||||
case missingElement(String)
|
||||
case invalidParameter(String)
|
||||
case parseFailed(String)
|
||||
case writeFile(String, String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .noValidTracker(let inputTargets):
|
||||
return "error: [\(Analytics.toolName)] '\(inputTargets)' ne contient aucun tracker valid"
|
||||
|
||||
case .fileNotExists(let filename):
|
||||
return "error: [\(Analytics.toolName)] File \(filename) does not exists"
|
||||
|
||||
case .missingElement(let element):
|
||||
return "error: [\(Analytics.toolName)] Missing \(element) for Matomo"
|
||||
|
||||
case .invalidParameter(let reason):
|
||||
return "error: [\(Analytics.toolName)] Invalid parameter \(reason)"
|
||||
|
||||
case .parseFailed(let baseError):
|
||||
return "error: [\(Analytics.toolName)] Parse input file failed: \(baseError)"
|
||||
|
||||
case .writeFile(let subErrorDescription, let filename):
|
||||
return "error: [\(Analytics.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
//
|
||||
// AnalyticsOptions.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 08/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
|
||||
struct AnalyticsOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
@Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
@Option(help: "Target(s) analytics to generate. (\"matomo\" | \"firebase\")")
|
||||
var target: String
|
||||
|
||||
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var extensionOutputPath: String
|
||||
|
||||
@Option(help: "Tell if it will generate static properties or not")
|
||||
var staticMembers: Bool = false
|
||||
|
||||
@Option(help: "Extension name. If not specified, it will generate a Analytics extension.")
|
||||
var extensionName: String = Analytics.defaultExtensionName
|
||||
|
||||
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift")
|
||||
var extensionSuffix: String?
|
||||
}
|
||||
|
||||
// MARK: - Computed var
|
||||
|
||||
extension AnalyticsOptions {
|
||||
var extensionFileName: String {
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
return "\(extensionName)+\(extensionSuffix).swift"
|
||||
}
|
||||
return "\(extensionName).swift"
|
||||
}
|
||||
|
||||
var extensionFilePath: String {
|
||||
"\(extensionOutputPath)/\(extensionFileName)"
|
||||
}
|
||||
}
|
@ -1,227 +0,0 @@
|
||||
//
|
||||
// AnalyticsGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 08/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
import CoreVideo
|
||||
|
||||
class AnalyticsGenerator {
|
||||
static var targets: [TrackerType] = []
|
||||
|
||||
static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
|
||||
// Get target type from enum
|
||||
let targetsString: [String] = target.components(separatedBy: " ")
|
||||
|
||||
TrackerType.allCases.forEach { enumTarget in
|
||||
if targetsString.contains(enumTarget.value) {
|
||||
targets.append(enumTarget)
|
||||
}
|
||||
}
|
||||
|
||||
// Get extension content
|
||||
let extensionFileContent = Self.getExtensionContent(sections: sections,
|
||||
tags: tags,
|
||||
staticVar: staticVar,
|
||||
extensionName: extensionName)
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
|
||||
} catch (let error) {
|
||||
let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription)
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension content
|
||||
|
||||
static func getExtensionContent(sections: [AnalyticsCategory], tags: [String], staticVar: Bool, extensionName: String) -> String {
|
||||
[
|
||||
Self.getHeader(extensionClassname: extensionName, staticVar: staticVar),
|
||||
Self.getProperties(sections: sections, tags: tags, staticVar: staticVar),
|
||||
Self.getFooter()
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Extension part
|
||||
|
||||
private static func getHeader(extensionClassname: String, staticVar: Bool) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.\(Analytics.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
\(Self.getImport())
|
||||
|
||||
\(Self.getAnalyticsProtocol())
|
||||
// MARK: - Manager
|
||||
|
||||
class AnalyticsManager {
|
||||
static var shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var managers: [AnalyticsManagerProtocol] = []
|
||||
|
||||
\(Self.getEnabledContent())
|
||||
|
||||
\(Self.getAnalyticsProperties())
|
||||
|
||||
\(Self.getPrivateLogFunction())
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getEnabledContent() -> String {
|
||||
"""
|
||||
private var isEnabled: Bool = true
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setAnalyticsEnabled(_ enable: Bool) {
|
||||
isEnabled = enable
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getImport() -> String {
|
||||
var result: [String] = []
|
||||
|
||||
if targets.contains(TrackerType.matomo) {
|
||||
result.append("import MatomoTracker")
|
||||
}
|
||||
if targets.contains(TrackerType.firebase) {
|
||||
result.append("import FirebaseAnalytics")
|
||||
}
|
||||
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getPrivateLogFunction() -> String {
|
||||
"""
|
||||
private func logScreen(name: String, path: String) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logScreen(name: name, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logEvent(
|
||||
name: name,
|
||||
action: action,
|
||||
category: category,
|
||||
params: params
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private static func getAnalyticsProperties() -> String {
|
||||
var header = ""
|
||||
var content: [String] = []
|
||||
let footer = " }"
|
||||
|
||||
if targets.contains(TrackerType.matomo) {
|
||||
header = "func configure(siteId: String, url: String) {"
|
||||
} else if targets.contains(TrackerType.firebase) {
|
||||
header = "func configure() {"
|
||||
}
|
||||
|
||||
if targets.contains(TrackerType.matomo) {
|
||||
content.append("""
|
||||
managers.append(
|
||||
MatomoAnalyticsManager(
|
||||
siteId: siteId,
|
||||
url: url
|
||||
)
|
||||
)
|
||||
""")
|
||||
}
|
||||
if targets.contains(TrackerType.firebase) {
|
||||
content.append(" managers.append(FirebaseAnalyticsManager())")
|
||||
}
|
||||
|
||||
return [
|
||||
header,
|
||||
content.joined(separator: "\n"),
|
||||
footer
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getAnalyticsProtocol() -> String {
|
||||
let proto = """
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String)
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
)
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
var result: [String] = [proto]
|
||||
|
||||
if targets.contains(TrackerType.matomo) {
|
||||
result.append(MatomoGenerator.service)
|
||||
}
|
||||
|
||||
if targets.contains(TrackerType.firebase) {
|
||||
result.append(FirebaseGenerator.service)
|
||||
}
|
||||
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getProperties(sections: [AnalyticsCategory], tags: [String], staticVar: Bool) -> String {
|
||||
sections
|
||||
.compactMap { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return nil// Go to next section
|
||||
}
|
||||
|
||||
var res = "\n // MARK: - \(section.id)"
|
||||
section.definitions.forEach { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return // Go to next definition
|
||||
}
|
||||
|
||||
if staticVar {
|
||||
res += "\n\n\(definition.getStaticProperty())"
|
||||
} else {
|
||||
res += "\n\n\(definition.getProperty())"
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// FirebaseGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 05/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FirebaseGenerator {
|
||||
|
||||
static var service: String {
|
||||
[
|
||||
FirebaseGenerator.header,
|
||||
FirebaseGenerator.logScreen,
|
||||
FirebaseGenerator.logEvent,
|
||||
FirebaseGenerator.footer
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Private vars
|
||||
|
||||
private static var header: String {
|
||||
"""
|
||||
// MARK: - Firebase
|
||||
|
||||
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
|
||||
"""
|
||||
}
|
||||
|
||||
private static var logScreen: String {
|
||||
"""
|
||||
func logScreen(name: String, path: String) {
|
||||
var parameters = [
|
||||
AnalyticsParameterScreenName: name
|
||||
]
|
||||
|
||||
Analytics.logEvent(
|
||||
AnalyticsEventScreenView,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
private static var logEvent: String {
|
||||
"""
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
var parameters: [String:Any] = [
|
||||
"action": action,
|
||||
"category": category,
|
||||
]
|
||||
|
||||
if let supplementaryParameters = params {
|
||||
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.logEvent(
|
||||
name,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private static var footer: String {
|
||||
"""
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
//
|
||||
// MatomoGenerator.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 05/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MatomoGenerator {
|
||||
|
||||
static var service: String {
|
||||
[
|
||||
MatomoGenerator.header,
|
||||
MatomoGenerator.setup,
|
||||
MatomoGenerator.logScreen,
|
||||
MatomoGenerator.logEvent,
|
||||
MatomoGenerator.footer
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Private vars
|
||||
|
||||
private static var header: String {
|
||||
"""
|
||||
// MARK: - Matomo
|
||||
|
||||
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var tracker: MatomoTracker
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
private static var setup: String {
|
||||
"""
|
||||
// MARK: - Init
|
||||
|
||||
init(siteId: String, url: String) {
|
||||
debugPrint("[Matomo service] Server URL: \\(url)")
|
||||
debugPrint("[Matomo service] Site ID: \\(siteId)")
|
||||
tracker = MatomoTracker(
|
||||
siteId: siteId,
|
||||
baseURL: URL(string: url)!
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
tracker.dispatchInterval = 5
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
tracker.logger = DefaultLogger(minLevel: .verbose)
|
||||
#endif
|
||||
|
||||
debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")")
|
||||
debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)")
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
private static var logScreen: String {
|
||||
"""
|
||||
func logScreen(name: String, path: String) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
|
||||
|
||||
let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
|
||||
tracker.track(
|
||||
view: [name],
|
||||
url: urlString
|
||||
)
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
private static var logEvent: String {
|
||||
"""
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
|
||||
tracker.track(
|
||||
eventWithCategory: category,
|
||||
action: action,
|
||||
name: name,
|
||||
number: nil,
|
||||
url: nil
|
||||
)
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private static var footer: String {
|
||||
"""
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// AnalyticsCategory.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 05/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AnalyticsCategory {
|
||||
let id: String // OnBoarding
|
||||
var definitions = [AnalyticsDefinition]()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func hasOneOrMoreMatchingTags(tags: [String]) -> Bool {
|
||||
let allTags = definitions.flatMap { $0.tags }
|
||||
let allTagsSet = Set(allTags)
|
||||
|
||||
let intersection = Set(tags).intersection(allTagsSet)
|
||||
if intersection.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
//
|
||||
// AnalyticsDefinition.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 05/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ToolCore
|
||||
|
||||
class AnalyticsDefinition {
|
||||
let id: String
|
||||
var name: String
|
||||
var path: String = ""
|
||||
var category: String = ""
|
||||
var action: String = ""
|
||||
var comments: String = ""
|
||||
var tags: [String] = []
|
||||
var parameters: [AnalyticsParameter] = []
|
||||
var type: TagType
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(id: String, name: String, type: TagType) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
|
||||
if Set(inputTags).intersection(Set(self.tags)).isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func getFuncName() -> String {
|
||||
var pascalCaseTitle: String = ""
|
||||
id.components(separatedBy: "_").forEach { word in
|
||||
pascalCaseTitle.append(contentsOf: word.uppercasedFirst())
|
||||
}
|
||||
|
||||
return "log\(type == .screen ? "Screen" : "Event")\(pascalCaseTitle)"
|
||||
}
|
||||
|
||||
private func getParameters() -> String {
|
||||
var params = parameters
|
||||
var result: String
|
||||
|
||||
if type == .screen {
|
||||
params = params.filter{ param in
|
||||
!param.replaceIn.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
let paramsString = params.map { parameter in
|
||||
"\(parameter.name): \(parameter.type)"
|
||||
}
|
||||
|
||||
if paramsString.count > 2 {
|
||||
result = """
|
||||
(
|
||||
\(paramsString.joined(separator: ",\n\t\t"))
|
||||
)
|
||||
"""
|
||||
} else {
|
||||
result = """
|
||||
(\(paramsString.joined(separator: ", ")))
|
||||
"""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func replaceIn(){
|
||||
for parameter in parameters {
|
||||
for rep in parameter.replaceIn {
|
||||
switch rep {
|
||||
case "name": name = name.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
|
||||
case "path": path = path.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
|
||||
case "category": category = category.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
|
||||
case "action": action = action.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getlogFunction() -> String {
|
||||
var params: [String] = []
|
||||
var result: String
|
||||
|
||||
let supplementaryParams = parameters.filter { param in
|
||||
param.replaceIn.isEmpty
|
||||
}
|
||||
|
||||
supplementaryParams.forEach { param in
|
||||
params.append("\"\(param.name)\": \(param.name)")
|
||||
}
|
||||
|
||||
if params.count > 1 {
|
||||
result = """
|
||||
[
|
||||
\(params.joined(separator: ",\n\t\t\t\t"))
|
||||
]
|
||||
"""
|
||||
} else if params.count == 1 {
|
||||
result = """
|
||||
[\(params.joined(separator: ", "))]
|
||||
"""
|
||||
} else {
|
||||
result = "[:]"
|
||||
}
|
||||
|
||||
if type == .screen {
|
||||
return """
|
||||
logScreen(
|
||||
name: "\(name)",
|
||||
path: "\(path)"
|
||||
)
|
||||
"""
|
||||
} else {
|
||||
return """
|
||||
logEvent(
|
||||
name: "\(name)",
|
||||
action: "\(action)",
|
||||
category: "\(category)",
|
||||
params: \(result)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Raw strings
|
||||
|
||||
func getProperty() -> String {
|
||||
replaceIn()
|
||||
return """
|
||||
func \(getFuncName())\(getParameters()) {
|
||||
\(getlogFunction())
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
func getStaticProperty() -> String {
|
||||
replaceIn()
|
||||
return """
|
||||
static func \(getFuncName())\(getParameters()) {
|
||||
\(getlogFunction())
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
//
|
||||
// AnalyticsFile.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AnalyticsFile: Codable {
|
||||
var categories: [AnalyticsCategoryDTO]
|
||||
}
|
||||
|
||||
struct AnalyticsCategoryDTO: Codable {
|
||||
var id: String
|
||||
var screens: [AnalyticsDefinitionScreenDTO]?
|
||||
var events: [AnalyticsDefinitionEventDTO]?
|
||||
}
|
||||
|
||||
struct AnalyticsDefinitionScreenDTO: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var tags: String
|
||||
var comments: String?
|
||||
var parameters: [AnalyticsParameterDTO]?
|
||||
|
||||
var path: String?
|
||||
}
|
||||
|
||||
struct AnalyticsDefinitionEventDTO: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var tags: String
|
||||
var comments: String?
|
||||
var parameters: [AnalyticsParameterDTO]?
|
||||
|
||||
var category: String?
|
||||
var action: String?
|
||||
}
|
||||
|
||||
struct AnalyticsParameterDTO: Codable {
|
||||
var name: String
|
||||
var type: String
|
||||
var replaceIn: String?
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// AnalyticsParameter.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AnalyticsParameter {
|
||||
var name: String
|
||||
var type: String
|
||||
var replaceIn: [String] = []
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(name: String, type: String) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
//
|
||||
// TagType.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 08/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension AnalyticsDefinition {
|
||||
|
||||
enum TagType {
|
||||
case screen
|
||||
case event
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
//
|
||||
// TargetType.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 08/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TrackerType: CaseIterable {
|
||||
case matomo
|
||||
case firebase
|
||||
|
||||
var value: String {
|
||||
switch self {
|
||||
case .matomo:
|
||||
"matomo"
|
||||
case .firebase:
|
||||
"firebase"
|
||||
}
|
||||
}
|
||||
|
||||
static func hasValidTarget(in targets: String) -> Bool {
|
||||
for tracker in Self.allCases where targets.contains(tracker.value) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
//
|
||||
// AnalyticsFileParser.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
class AnalyticsFileParser {
|
||||
private static var inputFile: String = ""
|
||||
private static var target: String = ""
|
||||
|
||||
private static func parseYaml() -> AnalyticsFile {
|
||||
guard let data = FileManager().contents(atPath: inputFile) else {
|
||||
let error = AnalyticsError.fileNotExists(inputFile)
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
do {
|
||||
let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data)
|
||||
return tagFile
|
||||
} catch {
|
||||
let error = AnalyticsError.parseFailed(error.localizedDescription)
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] {
|
||||
parameters.map { dtoParameter in
|
||||
// Type
|
||||
|
||||
let type = dtoParameter.type.uppercasedFirst()
|
||||
|
||||
guard
|
||||
type == "String" ||
|
||||
type == "Int" ||
|
||||
type == "Double" ||
|
||||
type == "Bool"
|
||||
else {
|
||||
let error = AnalyticsError.invalidParameter("type of \(dtoParameter.name)")
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
let parameter = AnalyticsParameter(
|
||||
name: dtoParameter.name,
|
||||
type: type
|
||||
)
|
||||
|
||||
if let replaceIn = dtoParameter.replaceIn {
|
||||
parameter.replaceIn = replaceIn.components(separatedBy: ",")
|
||||
}
|
||||
|
||||
return parameter
|
||||
}
|
||||
}
|
||||
|
||||
private static func getTagDefinition(
|
||||
id: String,
|
||||
name: String,
|
||||
type: AnalyticsDefinition.TagType,
|
||||
tags: String,
|
||||
comments: String?,
|
||||
parameters: [AnalyticsParameterDTO]?
|
||||
) -> AnalyticsDefinition {
|
||||
let definition = AnalyticsDefinition(id: id, name: name, type: type)
|
||||
definition.tags = tags
|
||||
.components(separatedBy: ",")
|
||||
.map { $0.removeLeadingTrailingWhitespace() }
|
||||
|
||||
if let comments = comments {
|
||||
definition.comments = comments
|
||||
}
|
||||
|
||||
if let parameters = parameters {
|
||||
definition.parameters = Self.getParameters(from: parameters)
|
||||
}
|
||||
|
||||
return definition
|
||||
}
|
||||
|
||||
private static func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] {
|
||||
screens.map { screen in
|
||||
let definition: AnalyticsDefinition = Self.getTagDefinition(
|
||||
id: screen.id,
|
||||
name: screen.name,
|
||||
type: .screen,
|
||||
tags: screen.tags,
|
||||
comments: screen.comments,
|
||||
parameters: screen.parameters
|
||||
)
|
||||
|
||||
if target.contains(TrackerType.matomo.value) {
|
||||
// Path
|
||||
|
||||
guard let path = screen.path else {
|
||||
let error = AnalyticsError.missingElement("screen path")
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
definition.path = path
|
||||
}
|
||||
|
||||
return definition
|
||||
}
|
||||
}
|
||||
|
||||
private static func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
|
||||
events.map { event in
|
||||
let definition: AnalyticsDefinition = Self.getTagDefinition(
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
type: .event,
|
||||
tags: event.tags,
|
||||
comments: event.comments,
|
||||
parameters: event.parameters
|
||||
)
|
||||
|
||||
if target.contains(TrackerType.matomo.value) {
|
||||
// Category
|
||||
guard let category = event.category else {
|
||||
let error = AnalyticsError.missingElement("event category")
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
definition.category = category
|
||||
|
||||
// Action
|
||||
guard let action = event.action else {
|
||||
let error = AnalyticsError.missingElement("event action")
|
||||
print(error.description)
|
||||
Analytics.exit(withError: error)
|
||||
}
|
||||
|
||||
definition.action = action
|
||||
}
|
||||
|
||||
return definition
|
||||
}
|
||||
}
|
||||
|
||||
static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] {
|
||||
self.inputFile = inputFile
|
||||
self.target = target
|
||||
|
||||
let tagFile = Self.parseYaml()
|
||||
|
||||
return tagFile
|
||||
.categories
|
||||
.map { categorie in
|
||||
let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id)
|
||||
|
||||
if let screens = categorie.screens {
|
||||
section
|
||||
.definitions
|
||||
.append(
|
||||
contentsOf: Self.getTagDefinitionScreen(from: screens)
|
||||
)
|
||||
}
|
||||
|
||||
if let events = categorie.events {
|
||||
section
|
||||
.definitions
|
||||
.append(
|
||||
contentsOf: Self.getTagDefinitionEvent(from: events)
|
||||
)
|
||||
}
|
||||
|
||||
return section
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ struct Generate: ParsableCommand {
|
||||
// Parse
|
||||
let configuration = ConfigurationFileParser.parse(options.configurationFile)
|
||||
print("Found configurations :")
|
||||
print(" - \(configuration.analytics.count) analytics configuration(s)")
|
||||
print(" - \(configuration.colors.count) colors configuration(s)")
|
||||
print(" - \(configuration.fonts.count) fonts configuration(s)")
|
||||
print(" - \(configuration.images.count) images configuration(s)")
|
||||
|
@ -11,14 +11,12 @@ import Foundation
|
||||
struct ArchitectureGenerator {
|
||||
static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) {
|
||||
// Create extension content
|
||||
var architectureContent = [
|
||||
let architectureContent = [
|
||||
"// Generated by ResgenSwift.\(Generate.toolName) \(ResgenSwiftVersion)",
|
||||
architecture.getClass()
|
||||
]
|
||||
.joined(separator: "\n\n")
|
||||
|
||||
architectureContent += "\n"
|
||||
|
||||
let filename = "\(architecture.classname).swift"
|
||||
guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else {
|
||||
let error = GenerateError.writeFile(filename, "Path of file is not defined.")
|
||||
|
@ -9,7 +9,6 @@ import Foundation
|
||||
|
||||
struct ConfigurationFile: Codable, CustomDebugStringConvertible {
|
||||
var architecture: ConfigurationArchitecture?
|
||||
var analytics: [AnalyticsConfiguration]
|
||||
var colors: [ColorsConfiguration]
|
||||
var fonts: [FontsConfiguration]
|
||||
var images: [ImagesConfiguration]
|
||||
@ -17,15 +16,12 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible {
|
||||
var tags: [TagsConfiguration]
|
||||
|
||||
var runnableConfigurations: [Runnable] {
|
||||
let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags]
|
||||
let runnables: [[Runnable]] = [colors, fonts, images, strings, tags]
|
||||
return Array(runnables.joined())
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
\(analytics)
|
||||
-----------
|
||||
-----------
|
||||
\(colors)
|
||||
-----------
|
||||
-----------
|
||||
@ -80,47 +76,6 @@ struct ConfigurationArchitecture: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let target: String
|
||||
let extensionOutputPath: String
|
||||
let extensionName: String?
|
||||
let extensionSuffix: String?
|
||||
private let staticMembers: Bool?
|
||||
|
||||
var staticMembersOptions: Bool {
|
||||
if let staticMembers = staticMembers {
|
||||
return staticMembers
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal init(inputFile: String,
|
||||
target: String,
|
||||
extensionOutputPath: String,
|
||||
extensionName: String?,
|
||||
extensionSuffix: String?,
|
||||
staticMembers: Bool?) {
|
||||
self.inputFile = inputFile
|
||||
self.target = target
|
||||
self.extensionOutputPath = extensionOutputPath
|
||||
self.extensionName = extensionName
|
||||
self.extensionSuffix = extensionSuffix
|
||||
self.staticMembers = staticMembers
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"""
|
||||
Analytics configuration:
|
||||
- Input file: \(inputFile)
|
||||
- Target: \(target)
|
||||
- Extension output path: \(extensionOutputPath)
|
||||
- Extension name: \(extensionName ?? "-")
|
||||
- Extension suffix: \(extensionSuffix ?? "-")
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
|
||||
let inputFile: String
|
||||
let style: String
|
||||
|
@ -1,43 +0,0 @@
|
||||
//
|
||||
// AnalyticsConfiguration+Runnable.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 08/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension AnalyticsConfiguration: Runnable {
|
||||
func run(projectDirectory: String, force: Bool) {
|
||||
var args = [String]()
|
||||
|
||||
if force {
|
||||
args += ["-f"]
|
||||
}
|
||||
|
||||
args += [
|
||||
inputFile.prependIfRelativePath(projectDirectory),
|
||||
"--target",
|
||||
target,
|
||||
"--extension-output-path",
|
||||
extensionOutputPath.prependIfRelativePath(projectDirectory),
|
||||
"--static-members",
|
||||
"\(staticMembersOptions)"
|
||||
]
|
||||
|
||||
if let extensionName = extensionName {
|
||||
args += [
|
||||
"--extension-name",
|
||||
extensionName
|
||||
]
|
||||
}
|
||||
if let extensionSuffix = extensionSuffix {
|
||||
args += [
|
||||
"--extension-suffix",
|
||||
extensionSuffix
|
||||
]
|
||||
}
|
||||
|
||||
Analytics.main(args)
|
||||
}
|
||||
}
|
@ -19,12 +19,10 @@ struct ResgenSwift: ParsableCommand {
|
||||
// With language support for type-level introspection, this could be
|
||||
// provided by automatically finding nested `ParsableCommand` types.
|
||||
subcommands: [
|
||||
Analytics.self,
|
||||
Colors.self,
|
||||
Fonts.self,
|
||||
Images.self,
|
||||
Strings.self,
|
||||
Tags.self,
|
||||
Generate.self
|
||||
]
|
||||
|
||||
|
@ -63,7 +63,7 @@ public extension String {
|
||||
replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)")
|
||||
}
|
||||
|
||||
func colorComponent() -> (alpha: String, red: String, green: String, blue: String) {
|
||||
func colorComponent() -> (alpha: String, red: String, green: String, blue: String) {
|
||||
var alpha: String = "FF"
|
||||
var red: String
|
||||
var green: String
|
||||
@ -89,19 +89,4 @@ public extension String {
|
||||
func uppercasedFirst() -> String {
|
||||
prefix(1).uppercased() + dropFirst()
|
||||
}
|
||||
|
||||
func replacingFirstOccurrence(of: String, with: String) -> Self {
|
||||
if let range = self.range(of: of) {
|
||||
let tmp = self.replacingOccurrences(
|
||||
of: of,
|
||||
with: with,
|
||||
options: .literal,
|
||||
range: range
|
||||
)
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
@ -1,137 +0,0 @@
|
||||
//
|
||||
// AnalyticsDefinitionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import ResgenSwift
|
||||
|
||||
final class AnalyticsDefinitionTests: XCTestCase {
|
||||
|
||||
// MARK: - Matching tags
|
||||
|
||||
func testMatchingAnalyticss() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
|
||||
definition.tags = ["ios","iosonly","notranslation"]
|
||||
|
||||
// When
|
||||
let match1 = definition.hasOneOrMoreMatchingTags(inputTags: ["ios"])
|
||||
let match2 = definition.hasOneOrMoreMatchingTags(inputTags: ["iosonly"])
|
||||
let match3 = definition.hasOneOrMoreMatchingTags(inputTags: ["notranslation"])
|
||||
|
||||
|
||||
// Expect
|
||||
XCTAssertTrue(match1)
|
||||
XCTAssertTrue(match2)
|
||||
XCTAssertTrue(match3)
|
||||
}
|
||||
|
||||
func testNotMatchingAnalyticss() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
|
||||
definition.tags = ["ios","iosonly","notranslation"]
|
||||
|
||||
// When
|
||||
let match1 = definition.hasOneOrMoreMatchingTags(inputTags: ["droid"])
|
||||
let match2 = definition.hasOneOrMoreMatchingTags(inputTags: ["droidonly"])
|
||||
let match3 = definition.hasOneOrMoreMatchingTags(inputTags: ["azerty"])
|
||||
|
||||
// Expect
|
||||
XCTAssertFalse(match1)
|
||||
XCTAssertFalse(match2)
|
||||
XCTAssertFalse(match3)
|
||||
}
|
||||
|
||||
// MARK: - Raw properties
|
||||
|
||||
func testGeneratedRawPropertyScreen() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
|
||||
definition.path = "ecran_un/"
|
||||
|
||||
// When
|
||||
let propertyScreen = definition.getProperty()
|
||||
|
||||
// Expect
|
||||
let expectScreen = """
|
||||
func logScreenDefinitionName() {
|
||||
logScreen(
|
||||
name: "Ecran un",
|
||||
path: "ecran_un/"
|
||||
)
|
||||
}
|
||||
"""
|
||||
|
||||
XCTAssertEqual(propertyScreen.adaptForXCTest(), expectScreen.adaptForXCTest())
|
||||
}
|
||||
|
||||
func testGeneratedRawPropertyEvent() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .event)
|
||||
|
||||
// When
|
||||
let propertyEvent = definition.getProperty()
|
||||
|
||||
// Expect
|
||||
let expectEvent = """
|
||||
func logEventDefinitionName() {
|
||||
logEvent(
|
||||
name: "Ecran un",
|
||||
action: "",
|
||||
category: "",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
"""
|
||||
|
||||
XCTAssertEqual(propertyEvent.adaptForXCTest(), expectEvent.adaptForXCTest())
|
||||
}
|
||||
|
||||
func testGeneratedRawStaticPropertyScreen() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
|
||||
definition.path = "ecran_un/"
|
||||
|
||||
// When
|
||||
let propertyScreen = definition.getStaticProperty()
|
||||
|
||||
// Expect
|
||||
let expectScreen = """
|
||||
static func logScreenDefinitionName() {
|
||||
logScreen(
|
||||
name: "Ecran un",
|
||||
path: "ecran_un/"
|
||||
)
|
||||
}
|
||||
"""
|
||||
|
||||
XCTAssertEqual(propertyScreen.adaptForXCTest(), expectScreen.adaptForXCTest())
|
||||
}
|
||||
|
||||
func testGeneratedRawStaticPropertyEvent() {
|
||||
// Given
|
||||
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .event)
|
||||
|
||||
// When
|
||||
let propertyEvent = definition.getStaticProperty()
|
||||
|
||||
// Expect
|
||||
let expectEvent = """
|
||||
static func logEventDefinitionName() {
|
||||
logEvent(
|
||||
name: "Ecran un",
|
||||
action: "",
|
||||
category: "",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
"""
|
||||
|
||||
XCTAssertEqual(propertyEvent.adaptForXCTest(), expectEvent.adaptForXCTest())
|
||||
}
|
||||
}
|
@ -1,623 +0,0 @@
|
||||
//
|
||||
// AnalyticsGeneratorTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 06/09/2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import ToolCore
|
||||
|
||||
@testable import ResgenSwift
|
||||
|
||||
final class AnalyticsGeneratorTests: XCTestCase {
|
||||
|
||||
private func getAnalyticsDefinition(
|
||||
id: String,
|
||||
path: String = "",
|
||||
action: String = "",
|
||||
category: String = "",
|
||||
name: String,
|
||||
type: AnalyticsDefinition.TagType,
|
||||
tags: [String]
|
||||
) -> AnalyticsDefinition {
|
||||
let definition = AnalyticsDefinition(id: id, name: name, type: type)
|
||||
definition.tags = tags
|
||||
definition.path = path
|
||||
definition.action = action
|
||||
definition.category = category
|
||||
return definition
|
||||
}
|
||||
|
||||
func testGeneratedExtensionContentFirebase() {
|
||||
// Given
|
||||
let sectionOne = AnalyticsCategory(id: "section_one")
|
||||
sectionOne.definitions = [
|
||||
getAnalyticsDefinition(id: "s1_def_one", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
|
||||
getAnalyticsDefinition(id: "s1_def_two", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
|
||||
]
|
||||
|
||||
let sectionTwo = AnalyticsCategory(id: "section_two")
|
||||
sectionTwo.definitions = [
|
||||
getAnalyticsDefinition(id: "s2_def_one", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
|
||||
getAnalyticsDefinition(id: "s2_def_two", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
let sectionThree = AnalyticsCategory(id: "section_three")
|
||||
sectionThree.definitions = [
|
||||
getAnalyticsDefinition(id: "s3_def_one", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
|
||||
getAnalyticsDefinition(id: "s3_def_two", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
// When
|
||||
AnalyticsGenerator.targets = [TrackerType.firebase]
|
||||
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
|
||||
tags: ["ios", "iosonly"],
|
||||
staticVar: false,
|
||||
extensionName: "GenAnalytics")
|
||||
// Expect Analytics
|
||||
let expect = """
|
||||
// Generated by ResgenSwift.Analytics 1.2
|
||||
|
||||
import Firebase
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String)
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Firebase
|
||||
|
||||
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String) {
|
||||
var parameters = [
|
||||
AnalyticsParameterScreenName: name
|
||||
]
|
||||
|
||||
Analytics.logEvent(
|
||||
AnalyticsEventScreenView,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
var parameters: [String:Any] = [
|
||||
"action": action,
|
||||
"category": category,
|
||||
]
|
||||
|
||||
if let supplementaryParameters = params {
|
||||
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.logEvent(
|
||||
name,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manager
|
||||
|
||||
class AnalyticsManager {
|
||||
static var shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var managers: [AnalyticsManagerProtocol] = []
|
||||
|
||||
private var isEnabled: Bool = true
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setAnalyticsEnabled(_ enable: Bool) {
|
||||
isEnabled = enable
|
||||
}
|
||||
|
||||
func configure() {
|
||||
managers.append(FirebaseAnalyticsManager())
|
||||
}
|
||||
|
||||
private func logScreen(name: String, path: String) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logScreen(name: name, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logEvent(
|
||||
name: name,
|
||||
action: action,
|
||||
category: category,
|
||||
params: params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
func logScreenS1DefOne() {
|
||||
logScreen(
|
||||
name: "s1 def one",
|
||||
path: ""
|
||||
)
|
||||
}
|
||||
|
||||
func logEventS1DefTwo() {
|
||||
logEvent(
|
||||
name: "s1 def two",
|
||||
action: "",
|
||||
category: "",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - section_two
|
||||
|
||||
func logScreenS2DefOne() {
|
||||
logScreen(
|
||||
name: "s2 def one",
|
||||
path: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
if extensionContent != expect {
|
||||
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
|
||||
}
|
||||
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
|
||||
}
|
||||
|
||||
func testGeneratedExtensionContentMatomo() {
|
||||
// Given
|
||||
let sectionOne = AnalyticsCategory(id: "section_one")
|
||||
sectionOne.definitions = [
|
||||
getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
|
||||
getAnalyticsDefinition(id: "s1_def_two", action: "test", category: "test", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
|
||||
]
|
||||
|
||||
let sectionTwo = AnalyticsCategory(id: "section_two")
|
||||
sectionTwo.definitions = [
|
||||
getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
|
||||
getAnalyticsDefinition(id: "s2_def_two", action: "test", category: "test", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
let sectionThree = AnalyticsCategory(id: "section_three")
|
||||
sectionThree.definitions = [
|
||||
getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
|
||||
getAnalyticsDefinition(id: "s3_def_two", action: "test", category: "test", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
// When
|
||||
AnalyticsGenerator.targets = [TrackerType.matomo]
|
||||
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
|
||||
tags: ["ios", "iosonly"],
|
||||
staticVar: false,
|
||||
extensionName: "GenAnalytics")
|
||||
// Expect Analytics
|
||||
let expect = """
|
||||
// Generated by ResgenSwift.Analytics 1.2
|
||||
|
||||
import MatomoTracker
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String)
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Matomo
|
||||
|
||||
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var tracker: MatomoTracker
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(siteId: String, url: String) {
|
||||
debugPrint("[Matomo service] Server URL: \\(url)")
|
||||
debugPrint("[Matomo service] Site ID: \\(siteId)")
|
||||
tracker = MatomoTracker(
|
||||
siteId: siteId,
|
||||
baseURL: URL(string: url)!
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
tracker.dispatchInterval = 5
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
tracker.logger = DefaultLogger(minLevel: .verbose)
|
||||
#endif
|
||||
|
||||
debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")")
|
||||
debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)")
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func logScreen(name: String, path: String) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
|
||||
|
||||
let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
|
||||
tracker.track(
|
||||
view: [name],
|
||||
url: urlString
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
|
||||
tracker.track(
|
||||
eventWithCategory: category,
|
||||
action: action,
|
||||
name: name,
|
||||
number: nil,
|
||||
url: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manager
|
||||
|
||||
class AnalyticsManager {
|
||||
static var shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var managers: [AnalyticsManagerProtocol] = []
|
||||
|
||||
private var isEnabled: Bool = true
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setAnalyticsEnabled(_ enable: Bool) {
|
||||
isEnabled = enable
|
||||
}
|
||||
|
||||
func configure(siteId: String, url: String) {
|
||||
managers.append(
|
||||
MatomoAnalyticsManager(
|
||||
siteId: siteId,
|
||||
url: url
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func logScreen(name: String, path: String) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logScreen(name: name, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logEvent(
|
||||
name: name,
|
||||
action: action,
|
||||
category: category,
|
||||
params: params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
func logScreenS1DefOne() {
|
||||
logScreen(
|
||||
name: "s1 def one",
|
||||
path: "s1_def_one/"
|
||||
)
|
||||
}
|
||||
|
||||
func logEventS1DefTwo() {
|
||||
logEvent(
|
||||
name: "s1 def two",
|
||||
action: "test",
|
||||
category: "test",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - section_two
|
||||
|
||||
func logScreenS2DefOne() {
|
||||
logScreen(
|
||||
name: "s2 def one",
|
||||
path: "s2_def_one/"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
if extensionContent != expect {
|
||||
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
|
||||
}
|
||||
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
|
||||
}
|
||||
|
||||
func testGeneratedExtensionContentMatomoAndFirebase() {
|
||||
// Given
|
||||
let sectionOne = AnalyticsCategory(id: "section_one")
|
||||
sectionOne.definitions = [
|
||||
getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: .screen, tags: ["ios", "iosonly"]),
|
||||
getAnalyticsDefinition(id: "s1_def_two", action: "test", category: "test", name: "s1 def two", type: .event, tags: ["ios", "iosonly"]),
|
||||
]
|
||||
|
||||
let sectionTwo = AnalyticsCategory(id: "section_two")
|
||||
sectionTwo.definitions = [
|
||||
getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: .screen, tags: ["ios","iosonly"]),
|
||||
getAnalyticsDefinition(id: "s2_def_two", action: "test", category: "test", name: "s2 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
let sectionThree = AnalyticsCategory(id: "section_three")
|
||||
sectionThree.definitions = [
|
||||
getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: .screen, tags: ["droid","droidonly"]),
|
||||
getAnalyticsDefinition(id: "s3_def_two", action: "test", category: "test", name: "s3 def two", type: .event, tags: ["droid","droidonly"]),
|
||||
]
|
||||
|
||||
// When
|
||||
AnalyticsGenerator.targets = [TrackerType.matomo, TrackerType.firebase]
|
||||
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
|
||||
tags: ["ios", "iosonly"],
|
||||
staticVar: false,
|
||||
extensionName: "GenAnalytics")
|
||||
// Expect Analytics
|
||||
let expect = """
|
||||
// Generated by ResgenSwift.Analytics 1.2
|
||||
|
||||
import MatomoTracker
|
||||
import Firebase
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String)
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Matomo
|
||||
|
||||
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var tracker: MatomoTracker
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(siteId: String, url: String) {
|
||||
debugPrint("[Matomo service] Server URL: \\(url)")
|
||||
debugPrint("[Matomo service] Site ID: \\(siteId)")
|
||||
tracker = MatomoTracker(
|
||||
siteId: siteId,
|
||||
baseURL: URL(string: url)!
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
tracker.dispatchInterval = 5
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
tracker.logger = DefaultLogger(minLevel: .verbose)
|
||||
#endif
|
||||
|
||||
debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")")
|
||||
debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)")
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func logScreen(name: String, path: String) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
|
||||
|
||||
let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
|
||||
tracker.track(
|
||||
view: [name],
|
||||
url: urlString
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard !tracker.isOptedOut else { return }
|
||||
|
||||
tracker.track(
|
||||
eventWithCategory: category,
|
||||
action: action,
|
||||
name: name,
|
||||
number: nil,
|
||||
url: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Firebase
|
||||
|
||||
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
|
||||
func logScreen(name: String, path: String) {
|
||||
var parameters = [
|
||||
AnalyticsParameterScreenName: name
|
||||
]
|
||||
|
||||
Analytics.logEvent(
|
||||
AnalyticsEventScreenView,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
|
||||
func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
var parameters: [String:Any] = [
|
||||
"action": action,
|
||||
"category": category,
|
||||
]
|
||||
|
||||
if let supplementaryParameters = params {
|
||||
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.logEvent(
|
||||
name,
|
||||
parameters: parameters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manager
|
||||
|
||||
class AnalyticsManager {
|
||||
static var shared = AnalyticsManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var managers: [AnalyticsManagerProtocol] = []
|
||||
|
||||
private var isEnabled: Bool = true
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setAnalyticsEnabled(_ enable: Bool) {
|
||||
isEnabled = enable
|
||||
}
|
||||
|
||||
func configure(siteId: String, url: String) {
|
||||
managers.append(
|
||||
MatomoAnalyticsManager(
|
||||
siteId: siteId,
|
||||
url: url
|
||||
)
|
||||
)
|
||||
managers.append(FirebaseAnalyticsManager())
|
||||
}
|
||||
|
||||
private func logScreen(name: String, path: String) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logScreen(name: name, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private func logEvent(
|
||||
name: String,
|
||||
action: String,
|
||||
category: String,
|
||||
params: [String: Any]?
|
||||
) {
|
||||
guard isEnabled else { return }
|
||||
|
||||
managers.forEach { manager in
|
||||
manager.logEvent(
|
||||
name: name,
|
||||
action: action,
|
||||
category: category,
|
||||
params: params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
func logScreenS1DefOne() {
|
||||
logScreen(
|
||||
name: "s1 def one",
|
||||
path: "s1_def_one/"
|
||||
)
|
||||
}
|
||||
|
||||
func logEventS1DefTwo() {
|
||||
logEvent(
|
||||
name: "s1 def two",
|
||||
action: "test",
|
||||
category: "test",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - section_two
|
||||
|
||||
func logScreenS2DefOne() {
|
||||
logScreen(
|
||||
name: "s2 def one",
|
||||
path: "s2_def_one/"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
if extensionContent != expect {
|
||||
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
|
||||
}
|
||||
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
//
|
||||
// AnalyticsSectionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import ResgenSwift
|
||||
|
||||
final class AnalyticsSectionTests: XCTestCase {
|
||||
|
||||
// MARK: - Matching tags
|
||||
|
||||
func testMatchingAnalytics() {
|
||||
// Given
|
||||
let section = AnalyticsCategory(id: "section_name")
|
||||
section.definitions = [
|
||||
{
|
||||
let def = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
|
||||
def.tags = ["ios","iosonly"]
|
||||
return def
|
||||
}(),
|
||||
{
|
||||
let def = AnalyticsDefinition(id: "definition_name_two", name: "", type: .screen)
|
||||
def.tags = ["droid","droidonly"]
|
||||
return def
|
||||
}()
|
||||
]
|
||||
|
||||
// When
|
||||
let match1 = section.hasOneOrMoreMatchingTags(tags: ["ios"])
|
||||
let match2 = section.hasOneOrMoreMatchingTags(tags: ["iosonly"])
|
||||
let match3 = section.hasOneOrMoreMatchingTags(tags: ["droid"])
|
||||
let match4 = section.hasOneOrMoreMatchingTags(tags: ["droidonly"])
|
||||
|
||||
// Expect
|
||||
XCTAssertTrue(match1)
|
||||
XCTAssertTrue(match2)
|
||||
XCTAssertTrue(match3)
|
||||
XCTAssertTrue(match4)
|
||||
}
|
||||
|
||||
func testNotMatchingAnalytics() {
|
||||
// Given
|
||||
let section = AnalyticsCategory(id: "section_name")
|
||||
section.definitions = [
|
||||
{
|
||||
let def = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
|
||||
def.tags = ["ios","iosonly"]
|
||||
return def
|
||||
}(),
|
||||
{
|
||||
let def = AnalyticsDefinition(id: "definition_name_two", name: "", type: .screen)
|
||||
def.tags = ["droid","droidonly"]
|
||||
return def
|
||||
}()
|
||||
]
|
||||
|
||||
// When
|
||||
let match1 = section.hasOneOrMoreMatchingTags(tags: ["web"])
|
||||
let match2 = section.hasOneOrMoreMatchingTags(tags: ["webonly"])
|
||||
let match3 = section.hasOneOrMoreMatchingTags(tags: ["azerty"])
|
||||
|
||||
// Expect
|
||||
XCTAssertFalse(match1)
|
||||
XCTAssertFalse(match2)
|
||||
XCTAssertFalse(match3)
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
//
|
||||
// DiffString.swift
|
||||
//
|
||||
//
|
||||
// Created by Loris Perret on 06/12/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Find first differing character between two strings
|
||||
///
|
||||
/// :param: s1 First String
|
||||
/// :param: s2 Second String
|
||||
///
|
||||
/// :returns: .DifferenceAtIndex(i) or .NoDifference
|
||||
public func firstDifferenceBetweenStrings(s1: NSString, s2: NSString) -> FirstDifferenceResult {
|
||||
let len1 = s1.length
|
||||
let len2 = s2.length
|
||||
|
||||
let lenMin = min(len1, len2)
|
||||
|
||||
for i in 0..<lenMin {
|
||||
if s1.character(at: i) != s2.character(at: i) {
|
||||
return .DifferenceAtIndex(i)
|
||||
}
|
||||
}
|
||||
|
||||
if len1 < len2 {
|
||||
return .DifferenceAtIndex(len1)
|
||||
}
|
||||
|
||||
if len2 < len1 {
|
||||
return .DifferenceAtIndex(len2)
|
||||
}
|
||||
|
||||
return .NoDifference
|
||||
}
|
||||
|
||||
|
||||
/// Create a formatted String representation of difference between strings
|
||||
///
|
||||
/// :param: s1 First string
|
||||
/// :param: s2 Second string
|
||||
///
|
||||
/// :returns: a string, possibly containing significant whitespace and newlines
|
||||
public func prettyFirstDifferenceBetweenStrings(s1: String, s2: String) -> String {
|
||||
let firstDifferenceResult = firstDifferenceBetweenStrings(s1: s1 as NSString, s2: s2 as NSString)
|
||||
return prettyDescriptionOfFirstDifferenceResult(firstDifferenceResult: firstDifferenceResult, s1: s1 as NSString, s2: s2 as NSString) as String
|
||||
}
|
||||
|
||||
|
||||
/// Create a formatted String representation of a FirstDifferenceResult for two strings
|
||||
///
|
||||
/// :param: firstDifferenceResult FirstDifferenceResult
|
||||
/// :param: s1 First string used in generation of firstDifferenceResult
|
||||
/// :param: s2 Second string used in generation of firstDifferenceResult
|
||||
///
|
||||
/// :returns: a printable string, possibly containing significant whitespace and newlines
|
||||
public func prettyDescriptionOfFirstDifferenceResult(firstDifferenceResult: FirstDifferenceResult, s1: NSString, s2: NSString) -> NSString {
|
||||
|
||||
func diffString(index: Int, s1: NSString, s2: NSString) -> NSString {
|
||||
let markerArrow = "\u{2b06}" // "⬆"
|
||||
let ellipsis = "\u{2026}" // "…"
|
||||
/// Given a string and a range, return a string representing that substring.
|
||||
///
|
||||
/// If the range starts at a position other than 0, an ellipsis
|
||||
/// will be included at the beginning.
|
||||
///
|
||||
/// If the range ends before the actual end of the string,
|
||||
/// an ellipsis is added at the end.
|
||||
func windowSubstring(s: NSString, range: NSRange) -> String {
|
||||
let validRange = NSMakeRange(range.location, min(range.length, s.length - range.location))
|
||||
let substring = s.substring(with: validRange)
|
||||
|
||||
let prefix = range.location > 0 ? ellipsis : ""
|
||||
let suffix = (s.length - range.location > range.length) ? ellipsis : ""
|
||||
|
||||
return "\(prefix)\(substring)\(suffix)"
|
||||
}
|
||||
|
||||
// Show this many characters before and after the first difference
|
||||
let windowPrefixLength = 10
|
||||
let windowSuffixLength = 10
|
||||
let windowLength = windowPrefixLength + 1 + windowSuffixLength
|
||||
|
||||
let windowIndex = max(index - windowPrefixLength, 0)
|
||||
let windowRange = NSMakeRange(windowIndex, windowLength)
|
||||
|
||||
let sub1 = windowSubstring(s: s1, range: windowRange)
|
||||
let sub2 = windowSubstring(s: s2, range: windowRange)
|
||||
|
||||
let markerPosition = min(windowSuffixLength, index) + (windowIndex > 0 ? 1 : 0)
|
||||
|
||||
let markerPrefix = String(repeating: " " as Character, count: markerPosition)
|
||||
let markerLine = "\(markerPrefix)\(markerArrow)"
|
||||
|
||||
return "Difference at index \(index):\n\(sub1)\n\(sub2)\n\(markerLine)" as NSString
|
||||
}
|
||||
|
||||
switch firstDifferenceResult {
|
||||
case .NoDifference: return "No difference"
|
||||
case .DifferenceAtIndex(let index): return diffString(index: index, s1: s1, s2: s2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Result type for firstDifferenceBetweenStrings()
|
||||
public enum FirstDifferenceResult {
|
||||
/// Strings are identical
|
||||
case NoDifference
|
||||
|
||||
/// Strings differ at the specified index.
|
||||
///
|
||||
/// This could mean that characters at the specified index are different,
|
||||
/// or that one string is longer than the other
|
||||
case DifferenceAtIndex(Int)
|
||||
}
|
||||
|
||||
extension FirstDifferenceResult {
|
||||
/// Textual representation of a FirstDifferenceResult
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .NoDifference:
|
||||
return "NoDifference"
|
||||
case .DifferenceAtIndex(let index):
|
||||
return "DifferenceAtIndex(\(index))"
|
||||
}
|
||||
}
|
||||
|
||||
/// Textual representation of a FirstDifferenceResult for debugging purposes
|
||||
public var debugDescription: String {
|
||||
return self.description
|
||||
}
|
||||
}
|
@ -144,22 +144,6 @@ final class StringsFileGeneratorTests: XCTestCase {
|
||||
|
||||
extension GenStrings {
|
||||
|
||||
enum KeyStrings: String {
|
||||
case s1_def_one = "s1_def_one"
|
||||
case s1_def_two = "s1_def_two"
|
||||
case s2_def_one = "s2_def_one"
|
||||
case s2_def_two = "s2_def_two"
|
||||
|
||||
var keyPath: KeyPath<GenStrings, String> {
|
||||
switch self {
|
||||
case .s1_def_one: return \\GenStrings.s1_def_one
|
||||
case .s1_def_two: return \\GenStrings.s1_def_two
|
||||
case .s2_def_one: return \\GenStrings.s2_def_one
|
||||
case .s2_def_two: return \\GenStrings.s2_def_two
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
/// Translation in fr :
|
||||
@ -190,9 +174,6 @@ final class StringsFileGeneratorTests: XCTestCase {
|
||||
}
|
||||
"""
|
||||
|
||||
if extensionContent != expect {
|
||||
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
|
||||
}
|
||||
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
|
||||
}
|
||||
|
||||
@ -240,22 +221,6 @@ final class StringsFileGeneratorTests: XCTestCase {
|
||||
|
||||
extension GenStrings {
|
||||
|
||||
enum KeyStrings: String {
|
||||
case s1_def_one = "s1_def_one"
|
||||
case s1_def_two = "s1_def_two"
|
||||
case s2_def_one = "s2_def_one"
|
||||
case s2_def_two = "s2_def_two"
|
||||
|
||||
var keyPath: KeyPath<GenStrings, String> {
|
||||
switch self {
|
||||
case .s1_def_one: return \\GenStrings.s1_def_one
|
||||
case .s1_def_two: return \\GenStrings.s1_def_two
|
||||
case .s2_def_one: return \\GenStrings.s2_def_one
|
||||
case .s2_def_two: return \\GenStrings.s2_def_two
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - section_one
|
||||
|
||||
/// Translation in fr :
|
||||
@ -286,9 +251,6 @@ final class StringsFileGeneratorTests: XCTestCase {
|
||||
}
|
||||
"""
|
||||
|
||||
if extensionContent != expect {
|
||||
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
|
||||
}
|
||||
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user