Compare commits

..

21 Commits

Author SHA1 Message Date
d79af06c38 docs: Add Analytics section in README
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:27:35 +01:00
d8937f2de6 docs: Add Analytics section in README 2023-12-13 10:23:31 +01:00
9b27f24197 docs: Add Analytics section in README 2023-12-13 10:20:53 +01:00
1d58fd5510 Edit sample file
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2023-12-12 16:58:25 +01:00
f6c49bf626 Add parse error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 11:19:19 +01:00
f1b62d83c4 Add missing print error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:29:19 +01:00
ee5055efa5 Retours sur la structure du code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:24:42 +01:00
6f8e3b6664 Add error handling
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:09:24 +01:00
1f2933950b Add \n at the end of R.swift
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 15:16:50 +01:00
3b90387e10 fix: Import + empty parameters
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 15:03:28 +01:00
1ee4998ec6 test: Edit test
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 14:22:17 +01:00
ca763cd5d0 fix: Rebase tags
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 11:30:05 +01:00
3fc2fd9bac fix: Tags -> Anlytics 2023-12-08 11:30:05 +01:00
09c153ba65 Actualiser Jenkinsfile
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 14:51:01 +01:00
2a144fc00e Test Tags generation
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 14:48:47 +01:00
6aef8bc2de Add FirebaseManager 2023-12-06 11:23:26 +01:00
3e133773a9 Add Manager + MatomoManager
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 10:34:27 +01:00
5fd680110c Fix forgotten code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-05 17:09:24 +01:00
ce274219fc Add new tag generation
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-05 16:56:44 +01:00
fa5bf192e8 Delete lang option and add target option 2023-12-05 16:55:45 +01:00
1a45ec7b0d New version of tags.txt 2023-12-05 16:54:44 +01:00
30 changed files with 2534 additions and 16 deletions

2
Jenkinsfile vendored
View File

@ -1,6 +1,6 @@
library "openiumpipeline" library "openiumpipeline"
env.DEVELOPER_DIR= "/Applications/Xcode-14.3.0.app/Contents/Developer" env.DEVELOPER_DIR="/Applications/Xcode-15.0.1.app/Contents/Developer"
//env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-" //env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-"
env.IS_PACKAGE_SWIFT=1 env.IS_PACKAGE_SWIFT=1
env.TARGETS_MACOS=1 env.TARGETS_MACOS=1

View File

@ -133,6 +133,71 @@ 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`. > ⚠️ 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
Images generator will generate images assets along with extensions to access those images easily. Images generator will generate images assets along with extensions to access those images easily.

View File

@ -0,0 +1,199 @@
// 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/"
)
}
}

View File

@ -0,0 +1,52 @@
---
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

View File

@ -30,18 +30,18 @@ FORCE_FLAG="$1"
# --default-lang "en" \ # --default-lang "en" \
# --extension-output-path "./Twine/Generated" # --extension-output-path "./Twine/Generated"
echo "\n-------------------------\n" #echo "\n-------------------------\n"
# Strings ## Strings
swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \ #swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
--output-path "./Strings/Generated" \ # --output-path "./Strings/Generated" \
--langs "fr en en-us" \ # --langs "fr en en-us" \
--default-lang "en" \ # --default-lang "en" \
--extension-output-path "./Strings/Generated" \ # --extension-output-path "./Strings/Generated" \
--extension-name "String" \ # --extension-name "String" \
--extension-suffix "GenAllScript" # --extension-suffix "GenAllScript"
echo "\n-------------------------\n" #echo "\n-------------------------\n"
## Tags ## Tags
#swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \ #swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
@ -50,7 +50,16 @@ echo "\n-------------------------\n"
# --extension-name "Tags" \ # --extension-name "Tags" \
# --extension-suffix "GenAllScript" # --extension-suffix "GenAllScript"
# #
#echo "\n-------------------------\n" 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"
# #
## Images ## Images
#swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \ #swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \

View File

@ -70,7 +70,7 @@ colors:
# Tags # Tags
# #
tags: tags:
- -
inputFile: ./Tags/sampleTags.txt inputFile: ./Tags/sampleTags.txt
lang: ium lang: ium
extensionOutputPath: ./Tags/Generated extensionOutputPath: ./Tags/Generated
@ -78,6 +78,18 @@ tags:
extensionSuffix: GenAllScript extensionSuffix: GenAllScript
#
# Analytics
#
analytics:
-
inputFile: ./Tags/sampleTags.yml
target: "matomo firebase"
extensionOutputPath: ./Tags/Generated
extensionName: Analytics
extensionSuffix: GenAllScript
# #
# Fonts # Fonts
# #

View File

@ -0,0 +1,87 @@
//
// 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
}
}

View File

@ -0,0 +1,39 @@
//
// 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)"
}
}
}

View File

@ -0,0 +1,47 @@
//
// 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)"
}
}

View File

@ -0,0 +1,227 @@
//
// 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 {
"""
}
"""
}
}

View File

@ -0,0 +1,81 @@
//
// 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 {
"""
}
"""
}
}

View File

@ -0,0 +1,110 @@
//
// 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 {
"""
}
"""
}
}

View File

@ -0,0 +1,32 @@
//
// 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
}
}

View File

@ -0,0 +1,157 @@
//
// 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())
}
"""
}
}

View File

@ -0,0 +1,45 @@
//
// 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?
}

View File

@ -0,0 +1,21 @@
//
// 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
}
}

View File

@ -0,0 +1,16 @@
//
// TagType.swift
//
//
// Created by Thibaut Schmitt on 08/12/2023.
//
import Foundation
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}

View File

@ -0,0 +1,29 @@
//
// 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
}
}

View File

@ -0,0 +1,178 @@
//
// 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
}
}
}

View File

@ -34,6 +34,7 @@ struct Generate: ParsableCommand {
// Parse // Parse
let configuration = ConfigurationFileParser.parse(options.configurationFile) let configuration = ConfigurationFileParser.parse(options.configurationFile)
print("Found configurations :") print("Found configurations :")
print(" - \(configuration.analytics.count) analytics configuration(s)")
print(" - \(configuration.colors.count) colors configuration(s)") print(" - \(configuration.colors.count) colors configuration(s)")
print(" - \(configuration.fonts.count) fonts configuration(s)") print(" - \(configuration.fonts.count) fonts configuration(s)")
print(" - \(configuration.images.count) images configuration(s)") print(" - \(configuration.images.count) images configuration(s)")

View File

@ -11,12 +11,14 @@ import Foundation
struct ArchitectureGenerator { struct ArchitectureGenerator {
static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) { static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) {
// Create extension content // Create extension content
let architectureContent = [ var architectureContent = [
"// Generated by ResgenSwift.\(Generate.toolName) \(ResgenSwiftVersion)", "// Generated by ResgenSwift.\(Generate.toolName) \(ResgenSwiftVersion)",
architecture.getClass() architecture.getClass()
] ]
.joined(separator: "\n\n") .joined(separator: "\n\n")
architectureContent += "\n"
let filename = "\(architecture.classname).swift" let filename = "\(architecture.classname).swift"
guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else { guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else {
let error = GenerateError.writeFile(filename, "Path of file is not defined.") let error = GenerateError.writeFile(filename, "Path of file is not defined.")

View File

@ -9,6 +9,7 @@ import Foundation
struct ConfigurationFile: Codable, CustomDebugStringConvertible { struct ConfigurationFile: Codable, CustomDebugStringConvertible {
var architecture: ConfigurationArchitecture? var architecture: ConfigurationArchitecture?
var analytics: [AnalyticsConfiguration]
var colors: [ColorsConfiguration] var colors: [ColorsConfiguration]
var fonts: [FontsConfiguration] var fonts: [FontsConfiguration]
var images: [ImagesConfiguration] var images: [ImagesConfiguration]
@ -16,12 +17,15 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible {
var tags: [TagsConfiguration] var tags: [TagsConfiguration]
var runnableConfigurations: [Runnable] { var runnableConfigurations: [Runnable] {
let runnables: [[Runnable]] = [colors, fonts, images, strings, tags] let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags]
return Array(runnables.joined()) return Array(runnables.joined())
} }
var debugDescription: String { var debugDescription: String {
""" """
\(analytics)
-----------
-----------
\(colors) \(colors)
----------- -----------
----------- -----------
@ -76,6 +80,47 @@ 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 { struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String let inputFile: String
let style: String let style: String

View File

@ -0,0 +1,43 @@
//
// 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)
}
}

View File

@ -19,10 +19,12 @@ struct ResgenSwift: ParsableCommand {
// With language support for type-level introspection, this could be // With language support for type-level introspection, this could be
// provided by automatically finding nested `ParsableCommand` types. // provided by automatically finding nested `ParsableCommand` types.
subcommands: [ subcommands: [
Analytics.self,
Colors.self, Colors.self,
Fonts.self, Fonts.self,
Images.self, Images.self,
Strings.self, Strings.self,
Tags.self,
Generate.self Generate.self
] ]

View File

@ -63,7 +63,7 @@ public extension String {
replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)") 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 alpha: String = "FF"
var red: String var red: String
var green: String var green: String
@ -89,4 +89,19 @@ public extension String {
func uppercasedFirst() -> String { func uppercasedFirst() -> String {
prefix(1).uppercased() + dropFirst() 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
}
} }

View File

@ -0,0 +1,137 @@
//
// 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())
}
}

View File

@ -0,0 +1,623 @@
//
// 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())
}
}

View File

@ -0,0 +1,72 @@
//
// 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)
}
}

View File

@ -0,0 +1,134 @@
//
// 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
}
}

View File

@ -144,6 +144,22 @@ final class StringsFileGeneratorTests: XCTestCase {
extension GenStrings { 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 // MARK: - section_one
/// Translation in fr : /// Translation in fr :
@ -174,6 +190,9 @@ final class StringsFileGeneratorTests: XCTestCase {
} }
""" """
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest()) XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
} }
@ -221,6 +240,22 @@ final class StringsFileGeneratorTests: XCTestCase {
extension GenStrings { 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 // MARK: - section_one
/// Translation in fr : /// Translation in fr :
@ -251,6 +286,9 @@ final class StringsFileGeneratorTests: XCTestCase {
} }
""" """
if extensionContent != expect {
print(prettyFirstDifferenceBetweenStrings(s1: extensionContent, s2: expect))
}
XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest()) XCTAssertEqual(extensionContent.adaptForXCTest(), expect.adaptForXCTest())
} }
} }