fix: Tags -> Anlytics

This commit is contained in:
Loris Perret 2023-12-08 11:29:29 +01:00
parent 09c153ba65
commit 3fc2fd9bac
21 changed files with 930 additions and 550 deletions

View File

@ -0,0 +1,190 @@
// 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: - Introduction
func logScreenIntroductionScreen(title: String) {
logScreen(
name: "Bienvenue \(title)",
path: "introduction/"
)
}
func logEventIntroductionScreen(test: String, data: Int) {
logEvent(
name: "Bienvenue",
action: "action",
category: "category",
params: [
"test": test,
"data": data
]
)
}
}

View File

@ -0,0 +1,24 @@
---
categories:
- id: Introduction
screens:
- id: introduction_screen
name: Bienvenue _TITLE_
path: introduction/
tags: droid,ios
parameters:
- name: title
type: String
replaceIn: name
events:
- id: introduction_screen
name: Bienvenue
category: category
action: action
tags: droid,ios
parameters:
- name: test
type: String
- name: data
type: Int

View File

@ -0,0 +1,67 @@
//
// 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)")
print("[\(Self.toolName)] Will generate analytics")
// 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")
}
}
extension Analytics {
enum TargetType: CaseIterable {
case matomo
case firebase
var value: String {
switch self {
case .matomo:
"matomo"
case .firebase:
"firebase"
}
}
}
}

View File

@ -1,14 +1,14 @@
//
// TagOptions.swift
// AnalyticsOptions.swift
//
//
// Created by Thibaut Schmitt on 10/01/2022.
// Created by Loris Perret on 08/12/2023.
//
import Foundation
import ArgumentParser
struct TagsOptions: ParsableArguments {
struct AnalyticsOptions: ParsableArguments {
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
var forceGeneration = false
@ -24,16 +24,16 @@ struct TagsOptions: ParsableArguments {
@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 Tag extension.")
var extensionName: String = Tags.defaultExtensionName
@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}+Tag{extensionSuffix}.swift")
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift")
var extensionSuffix: String?
}
// MARK: - Computed var
extension TagsOptions {
extension AnalyticsOptions {
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"

View File

@ -1,22 +1,22 @@
//
// TagsGenerator.swift
// AnalyticsGenerator.swift
//
//
// Created by Thibaut Schmitt on 10/01/2022.
// Created by Loris Perret on 08/12/2023.
//
import Foundation
import ToolCore
import CoreVideo
class TagsGenerator {
static var targets: [Tags.TargetType] = []
class AnalyticsGenerator {
static var targets: [Analytics.TargetType] = []
static func writeExtensionFiles(sections: [TagSection], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
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: " ")
Tags.TargetType.allCases.forEach { enumTarget in
Analytics.TargetType.allCases.forEach { enumTarget in
if targetsString.contains(enumTarget.value) {
targets.append(enumTarget)
}
@ -43,7 +43,7 @@ class TagsGenerator {
// MARK: - Extension content
static func getExtensionContent(sections: [TagSection], tags: [String], staticVar: Bool, extensionName: String) -> String {
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),
@ -56,9 +56,8 @@ class TagsGenerator {
private static func getHeader(extensionClassname: String, staticVar: Bool) -> String {
"""
// Generated by ResgenSwift.\(Tags.toolName) \(ResgenSwiftVersion)
// Generated by ResgenSwift.\(Analytics.toolName) \(ResgenSwiftVersion)
\(staticVar ? "typelias Tags = String\n\n" : "")import UIKit
\(Self.getImport())
\(Self.getAnalytics())
@ -85,17 +84,19 @@ class TagsGenerator {
// MARK: - Methods
func setAnalyticsEnabled(_ enable: Bool) { isEnabled = enable }
func setAnalyticsEnabled(_ enable: Bool) {
isEnabled = enable
}
"""
}
private static func getImport() -> String {
var result: [String] = []
if targets.contains(Tags.TargetType.matomo) {
if targets.contains(Analytics.TargetType.matomo) {
result.append("import MatomoTracker")
}
if targets.contains(Tags.TargetType.firebase) {
if targets.contains(Analytics.TargetType.firebase) {
result.append("import Firebase")
}
@ -106,15 +107,27 @@ class TagsGenerator {
"""
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) {
private func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard isEnabled else { return }
managers.forEach { manager in
manager.logEvent(name: name)
manager.logEvent(
name: name,
action: action,
category: category,
params: params
)
}
}
"""
@ -125,16 +138,23 @@ class TagsGenerator {
var content: [String] = []
let footer = " }"
if targets.contains(Tags.TargetType.matomo) {
if targets.contains(Analytics.TargetType.matomo) {
header = "func configure(siteId: String, url: String) {"
} else if targets.contains(Tags.TargetType.firebase) {
} else if targets.contains(Analytics.TargetType.firebase) {
header = "func configure() {"
}
if targets.contains(Tags.TargetType.matomo) {
content.append(" managers.append(MatomoAnalyticsManager(siteId: siteId, url: url))")
if targets.contains(Analytics.TargetType.matomo) {
content.append("""
managers.append(
MatomoAnalyticsManager(
siteId: siteId,
url: url
)
)
""")
}
if targets.contains(Tags.TargetType.firebase) {
if targets.contains(Analytics.TargetType.firebase) {
content.append(" managers.append(FirebaseAnalyticsManager())")
}
@ -152,25 +172,30 @@ class TagsGenerator {
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(name: String)
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
)
}
"""
var result: [String] = [proto]
if targets.contains(Tags.TargetType.matomo) {
if targets.contains(Analytics.TargetType.matomo) {
result.append(MatomoGenerator.service.content)
}
if targets.contains(Tags.TargetType.firebase) {
if targets.contains(Analytics.TargetType.firebase) {
result.append(FirebaseGenerator.service.content)
}
return result.joined(separator: "\n")
}
private static func getProperties(sections: [TagSection], tags: [String], staticVar: Bool) -> String {
private static func getProperties(sections: [AnalyticsCategory], tags: [String], staticVar: Bool) -> String {
sections
.compactMap { section in
// Check that at least one string will be generated
@ -178,7 +203,7 @@ class TagsGenerator {
return nil// Go to next section
}
var res = "\n // MARK: - \(section.name)"
var res = "\n // MARK: - \(section.id)"
section.definitions.forEach { definition in
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
return // Go to next definition

View File

@ -31,7 +31,14 @@ enum FirebaseGenerator {
private var logScreen: String {
"""
func logScreen(name: String, path: String) {
Analytics.logEvent(AnalyticsEventScreenView, parameters: [AnalyticsParameterScreenName: name])
var parameters = [
AnalyticsParameterScreenName: name
]
Analytics.logEvent(
AnalyticsEventScreenView,
parameters: parameters
)
}
"""
@ -39,12 +46,27 @@ enum FirebaseGenerator {
private var logEvent: String {
"""
func logEvent(name: String) {
var parameters = [
AnalyticsParameterValue: name
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
var parameters: [String:Any] = [
"action": action,
"category": category,
]
Analytics.logEvent(AnalyticsEventSelectContent, parameters: parameters)
if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in
return origin
}
}
Analytics.logEvent(
name,
parameters: parameters
)
}
"""
}

View File

@ -41,7 +41,10 @@ enum MatomoGenerator {
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)!)
tracker = MatomoTracker(
siteId: siteId,
baseURL: URL(string: url)!
)
#if DEBUG
tracker.dispatchInterval = 5
@ -65,6 +68,7 @@ enum MatomoGenerator {
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],
@ -77,11 +81,17 @@ enum MatomoGenerator {
private var logEvent: String {
"""
func logEvent(name: String) {
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard !tracker.isOptedOut else { return }
tracker.track(
eventWithCategory: "category",
action: "action",
eventWithCategory: category,
action: action,
name: name,
number: nil,
url: nil

View File

@ -0,0 +1,28 @@
//
// AnalyticsCategory.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
class AnalyticsCategory {
let id: String // OnBoarding
var definitions = [AnalyticsDefinition]()
init(id: String) {
self.id = id
}
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,174 @@
//
// AnalyticsDefinition.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
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
init(id: String, name: String, type: TagType) {
self.id = id
self.name = name
self.type = type
}
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).intersection(Set(self.tags)).isEmpty {
return false
}
return true
}
// MARK: - 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 {
result = """
[\(params.joined(separator: ", "))]
"""
}
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())
}
"""
}
}
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}
extension String {
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,47 @@
//
// 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]?
}
protocol AnalyticsDefinitionDTO: Codable {}
struct AnalyticsDefinitionScreenDTO: AnalyticsDefinitionDTO {
var id: String
var name: String
var tags: String
var comments: String?
var parameters: [AnalyticsParameterDTO]?
var path: String?
}
struct AnalyticsDefinitionEventDTO: AnalyticsDefinitionDTO {
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,19 @@
//
// AnalyticsParameter.swift
//
//
// Created by Loris Perret on 06/12/2023.
//
import Foundation
class AnalyticsParameter {
var name: String
var type: String
var replaceIn: [String] = []
init(name: String, type: String) {
self.name = name
self.type = type
}
}

View File

@ -0,0 +1,173 @@
//
// 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 = GenerateError.fileNotExists(inputFile)
Generate.exit(withError: error)
}
do {
let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data)
return tagFile
} catch let error {
Generate.exit(withError: error)
}
}
private static func getParameters(fromData data: [AnalyticsParameterDTO]) -> [AnalyticsParameter] {
var parameters: [AnalyticsParameter] = []
data.forEach { value in
// Type
let type = value.type.uppercasedFirst()
guard
type == "String" ||
type == "Int" ||
type == "Double" ||
type == "Bool"
else {
let error = GenerateError.invalidParameter("type of \(value.name)")
Generate.exit(withError: error)
}
let parameter: AnalyticsParameter = AnalyticsParameter(name: value.name, type: type)
if let replaceIn = value.replaceIn {
parameter.replaceIn = replaceIn.components(separatedBy: ",")
}
parameters.append(parameter)
}
return parameters
}
private static func getTagDefinition(
id: String,
name: String,
type: AnalyticsDefinition.TagType,
tags: String,
comments: String?,
parameters: [AnalyticsParameterDTO]?
) -> AnalyticsDefinition {
let definition: AnalyticsDefinition = AnalyticsDefinition(id: id, name: name, type: type)
definition.tags = tags.components(separatedBy: ",")
if let comments = comments {
definition.comments = comments
}
if let parameters = parameters {
definition.parameters = Self.getParameters(fromData: parameters)
}
return definition
}
private static func getTagDefinitionScreen(fromData screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = []
for screen in screens {
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: screen.id,
name: screen.name,
type: .screen,
tags: screen.tags,
comments: screen.comments,
parameters: screen.parameters
)
guard target.contains(Analytics.TargetType.matomo.value) else { continue }
// Path
guard let path = screen.path else {
let error = GenerateError.missingElement("screen path")
Generate.exit(withError: error)
}
definition.path = path
definitions.append(definition)
}
return definitions
}
private static func getTagDefinitionEvent(fromData events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = []
for event in events {
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: event.id,
name: event.name,
type: .event,
tags: event.tags,
comments: event.comments,
parameters: event.parameters
)
guard target.contains(Analytics.TargetType.matomo.value) else { continue }
// Category
guard let category = event.category else {
let error = GenerateError.missingElement("event category")
Generate.exit(withError: error)
}
definition.category = category
// Action
guard let action = event.action else {
let error = GenerateError.missingElement("event action")
Generate.exit(withError: error)
}
definition.action = action
definitions.append(definition)
}
return definitions
}
static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] {
self.inputFile = inputFile
self.target = target
let tagFile: AnalyticsFile = Self.parseYaml()
var sections: [AnalyticsCategory] = []
tagFile.categories.forEach { categorie in
let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id)
if let screens = categorie.screens {
section.definitions.append(contentsOf: Self.getTagDefinitionScreen(fromData: screens))
}
if let events = categorie.events {
section.definitions.append(contentsOf: Self.getTagDefinitionEvent(fromData: events))
}
sections.append(section)
}
return sections
}
}

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

@ -1,138 +0,0 @@
//
// TagDefinition.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
class TagDefinition {
let title: String
var path: String = ""
var name: String = ""
var type: String = ""
var tags = [String]()
var comment: String?
var isValid: Bool {
title.isEmpty == false &&
name.isEmpty == false &&
(type == TagType.screen.value || type == TagType.event.value)
}
init(title: String) {
self.title = title
}
static func match(_ line: String) -> TagDefinition? {
guard line.range(of: "\\[(.*?)]$", options: .regularExpression, range: nil, locale: nil) != nil else {
return nil
}
let definitionTitle = line
.replacingOccurrences(of: ["[", "]"], with: "")
.removeLeadingTrailingWhitespace()
return TagDefinition(title: definitionTitle)
}
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).intersection(Set(self.tags)).isEmpty {
return false
}
return true
}
// MARK: -
private func getStringParameters(input: String) -> (inputParameters: [String], translationArguments: [String])? {
var methodsParameters = [String]()
let printfPlaceholderRegex = try! NSRegularExpression(pattern: "%(?:\\d+\\$)?[+-]?(?:[ 0]|'.{1})?-?\\d*(?:\\.\\d+)?[blcdeEufFgGosxX@]*")
printfPlaceholderRegex.enumerateMatches(in: input, options: [], range: NSRange(location: 0, length: input.count)) { match, _, stop in
guard let match = match else { return }
if let range = Range(match.range, in: input), let last = input[range].last {
switch last {
case "d", "u":
methodsParameters.append("Int")
case "f", "F":
methodsParameters.append("Double")
case "@", "s", "c":
methodsParameters.append("String")
case "%":
// if you need to print %, you have to add %%
break
default:
break
}
}
}
if methodsParameters.isEmpty {
return nil
}
var inputParameters = [String]()
var translationArguments = [String]()
for (index, paramType) in methodsParameters.enumerated() {
let paramName = "arg\(index)"
translationArguments.append(paramName)
inputParameters.append("\(paramName): \(paramType)")
}
return (inputParameters: inputParameters, translationArguments: translationArguments)
}
private func getFuncName() -> String {
var pascalCaseTitle: String = ""
name.components(separatedBy: " ").forEach { word in
pascalCaseTitle.append(contentsOf: word.uppercasedFirst())
}
return "log\(type == TagType.screen.value ? "Screen" : "Event")\(pascalCaseTitle)"
}
private func getlogFunction() -> String {
if type == TagType.screen.value {
"logScreen(name: \"\(name)\", path: \"\(path)\")"
} else {
"logEvent(name: \"\(name)\")"
}
}
// MARK: - Raw strings
func getProperty() -> String {
"""
func \(getFuncName())() {
\(getlogFunction())
}
"""
}
func getStaticProperty() -> String {
"""
static func \(getFuncName())() {
\(getlogFunction())
}
"""
}
}
extension TagDefinition {
enum TagType {
case screen
case event
var value: String {
switch self {
case .screen:
"screen"
case .event:
"event"
}
}
}
}

View File

@ -1,39 +0,0 @@
//
// TagSection.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
class TagSection {
let name: String // OnBoarding
var definitions = [TagDefinition]()
init(name: String) {
self.name = name
}
static func match(_ line: String) -> TagSection? {
guard line.range(of: "\\[\\[(.*?)]]$", options: .regularExpression, range: nil, locale: nil) != nil else {
return nil
}
let sectionName = line
.replacingOccurrences(of: ["[", "]"], with: "")
.removeLeadingTrailingWhitespace()
return TagSection(name: sectionName)
}
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

@ -1,93 +0,0 @@
//
// TagFileParser.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
class TagFileParser {
static func parse(_ inputFile: String) -> [TagSection] {
let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
let stringsByLines = inputFileContent.components(separatedBy: .newlines)
var sections = [TagSection]()
// Parse file
stringsByLines.forEach {
// TagSection
if let section = TagSection.match($0) {
sections.append(section)
return
}
// Definition
if let definition = TagDefinition.match($0) {
sections.last?.definitions.append(definition)
return
}
// Definition content
if $0.isEmpty == false {
// name = Test => ["name ", " Test"]
let splitLine = $0
.removeLeadingTrailingWhitespace()
.split(separator: "=")
guard let lastDefinition = sections.last?.definitions.last,
let leftElement = splitLine.first else {
return
}
let rightElement: String = splitLine.dropFirst().joined(separator: "=")
// "name " => "name"
let leftHand = String(leftElement).removeTrailingWhitespace()
// " Test" => "Test"
let rightHand = String(rightElement).removeLeadingWhitespace()
// Handle comments, tags and translation
switch leftHand {
case "comments":
lastDefinition.comment = rightHand
case "tags":
lastDefinition.tags = rightHand
.split(separator: ",")
.map { String($0) }
case "path":
lastDefinition.path = rightHand
case "name":
lastDefinition.name = rightHand
case "type":
lastDefinition.type = rightHand
default:
break
}
}
}
// Keep only valid definition
var invalidDefinitionNames = [String]()
sections.forEach { section in
section.definitions = section.definitions
.filter {
if $0.isValid == false {
invalidDefinitionNames.append($0.name)
return false
}
return true
}
}
if invalidDefinitionNames.count > 0 {
print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
}
return sections
}
}

View File

@ -1,95 +0,0 @@
//
// Tag.swift
//
//
// Created by Thibaut Schmitt on 10/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
struct Tags: ParsableCommand {
// MARK: - Command Configuration
static var configuration = CommandConfiguration(
abstract: "Generate tags extension file.",
version: ResgenSwiftVersion
)
// MARK: - Static
static let toolName = "Tags"
static let defaultExtensionName = "Tags"
static let noTranslationTag: String = "notranslation"
// MARK: - Command Options
@OptionGroup var options: TagsOptions
// MARK: - Run
mutating func run() {
print("[\(Self.toolName)] Starting tags generation")
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate tags for target: \(options.target)")
// Check requirements
guard checkRequirements() else { return }
print("[\(Self.toolName)] Will generate tags")
// Parse input file
let sections = TagFileParser.parse(options.inputFile)
// Generate extension
TagsGenerator.writeExtensionFiles(sections: sections,
target: options.target,
tags: ["ios", "iosonly", Self.noTranslationTag],
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
print("[\(Self.toolName)] Tags generated")
}
// MARK: - Requirements
private func checkRequirements() -> Bool {
let fileManager = FileManager()
// Input file
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = StringiumError.fileNotExists(options.inputFile)
print(error.description)
Stringium.exit(withError: error)
}
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
print("[\(Self.toolName)] Tags are already up to date :) ")
return false
}
return true
}
}
extension Tags {
enum TargetType: CaseIterable {
case matomo
case firebase
var value: String {
switch self {
case .matomo:
"matomo"
case .firebase:
"firebase"
}
}
}
}

View File

@ -1,5 +1,5 @@
//
// TagDefinitionTests.swift
// AnalyticsDefinitionTests.swift
//
//
// Created by Loris Perret on 06/12/2023.
@ -10,44 +10,13 @@ import XCTest
@testable import ResgenSwift
final class TagDefinitionTests: XCTestCase {
// MARK: - Match line
func testMatchingTagDefinition() {
// Given
let line = "[definition_name]"
// When
let definition = TagDefinition.match(line)
// Expect
XCTAssertNotNil(definition)
XCTAssertEqual(definition?.title, "definition_name")
}
func testNotMatchingTagDefinition() {
// Given
let line1 = "definition_name"
let line2 = "[definition_name"
let line3 = "definition_name]"
// When
let definition1 = TagDefinition.match(line1)
let definition2 = TagDefinition.match(line2)
let definition3 = TagDefinition.match(line3)
// Expect
XCTAssertNil(definition1)
XCTAssertNil(definition2)
XCTAssertNil(definition3)
}
final class AnalyticsDefinitionTests: XCTestCase {
// MARK: - Matching tags
func testMatchingTags() {
func testMatchingAnalyticss() {
// Given
let definition = TagDefinition(title: "definition_name")
let definition = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
definition.tags = ["ios","iosonly","notranslation"]
// When
@ -62,9 +31,9 @@ final class TagDefinitionTests: XCTestCase {
XCTAssertTrue(match3)
}
func testNotMatchingTags() {
func testNotMatchingAnalyticss() {
// Given
let definition = TagDefinition(title: "definition_name")
let definition = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
definition.tags = ["ios","iosonly","notranslation"]
// When
@ -82,10 +51,8 @@ final class TagDefinitionTests: XCTestCase {
func testGeneratedRawPropertyScreen() {
// Given
let definition = TagDefinition(title: "definition_name")
definition.path = "ecran_un/"
definition.name = "Ecran un"
definition.type = "screen"
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
// When
let propertyScreen = definition.getProperty()
@ -102,10 +69,7 @@ final class TagDefinitionTests: XCTestCase {
func testGeneratedRawPropertyEvent() {
// Given
let definition = TagDefinition(title: "definition_name")
definition.path = "ecran_un/"
definition.name = "Ecran un"
definition.type = "event"
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
// When
let propertyEvent = definition.getProperty()
@ -122,10 +86,7 @@ final class TagDefinitionTests: XCTestCase {
func testGeneratedRawStaticPropertyScreen() {
// Given
let definition = TagDefinition(title: "definition_name")
definition.path = "ecran_un/"
definition.name = "Ecran un"
definition.type = "screen"
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
// When
let propertyScreen = definition.getStaticProperty()
@ -142,10 +103,7 @@ final class TagDefinitionTests: XCTestCase {
func testGeneratedRawStaticPropertyEvent() {
// Given
let definition = TagDefinition(title: "definition_name")
definition.path = "ecran_un/"
definition.name = "Ecran un"
definition.type = "event"
let definition = AnalyticsDefinition(id: "definition_name", name: "Ecran un", type: .screen)
// When
let propertyEvent = definition.getStaticProperty()

View File

@ -1,5 +1,5 @@
//
// TagsGeneratorTests.swift
// AnalyticsGeneratorTests.swift
//
//
// Created by Thibaut Schmitt on 06/09/2022.
@ -11,46 +11,43 @@ import ToolCore
@testable import ResgenSwift
final class TagsGeneratorTests: XCTestCase {
final class AnalyticsGeneratorTests: XCTestCase {
private func getTagDefinition(title: String, path: String, name: String, type: String, tags: [String]) -> TagDefinition {
let definition = TagDefinition(title: title)
definition.path = path
definition.name = name
definition.type = type
private func getAnalyticsDefinition(id: String, path: String, name: String, type: AnalyticsDefinition.TagType, tags: [String]) -> AnalyticsDefinition {
let definition = AnalyticsDefinition(id: id, name: name, type: type)
definition.tags = tags
return definition
}
func testGeneratedExtensionContentFirebase() {
// Given
let sectionOne = TagSection(name: "section_one")
let sectionOne = AnalyticsCategory(id: "section_one")
sectionOne.definitions = [
getTagDefinition(title: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: "screen", tags: ["ios", "iosonly"]),
getTagDefinition(title: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: "event", tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: AnalyticsDefinition.TagType.event, tags: ["ios", "iosonly"]),
]
let sectionTwo = TagSection(name: "section_two")
let sectionTwo = AnalyticsCategory(id: "section_two")
sectionTwo.definitions = [
getTagDefinition(title: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: "screen", tags: ["ios","iosonly"]),
getTagDefinition(title: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios","iosonly"]),
getAnalyticsDefinition(id: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
let sectionThree = TagSection(name: "section_three")
let sectionThree = AnalyticsCategory(id: "section_three")
sectionThree.definitions = [
getTagDefinition(title: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: "screen", tags: ["droid","droidonly"]),
getTagDefinition(title: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: AnalyticsDefinition.TagType.screen, tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
// When
TagsGenerator.targets = [Tags.TargetType.firebase]
let extensionContent = TagsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
AnalyticsGenerator.targets = [Analytics.TargetType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenTags")
// Expect Tags
extensionName: "GenAnalytics")
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Tags 1.2
// Generated by ResgenSwift.Analytics 1.2
import UIKit
import Firebase
@ -138,33 +135,33 @@ final class TagsGeneratorTests: XCTestCase {
func testGeneratedExtensionContentMatomo() {
// Given
let sectionOne = TagSection(name: "section_one")
let sectionOne = AnalyticsCategory(id: "section_one")
sectionOne.definitions = [
getTagDefinition(title: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: "screen", tags: ["ios", "iosonly"]),
getTagDefinition(title: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: "event", tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: AnalyticsDefinition.TagType.event, tags: ["ios", "iosonly"]),
]
let sectionTwo = TagSection(name: "section_two")
let sectionTwo = AnalyticsCategory(id: "section_two")
sectionTwo.definitions = [
getTagDefinition(title: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: "screen", tags: ["ios","iosonly"]),
getTagDefinition(title: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios","iosonly"]),
getAnalyticsDefinition(id: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
let sectionThree = TagSection(name: "section_three")
let sectionThree = AnalyticsCategory(id: "section_three")
sectionThree.definitions = [
getTagDefinition(title: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: "screen", tags: ["droid","droidonly"]),
getTagDefinition(title: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: AnalyticsDefinition.TagType.screen, tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
// When
TagsGenerator.targets = [Tags.TargetType.matomo]
let extensionContent = TagsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
AnalyticsGenerator.targets = [Analytics.TargetType.matomo]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenTags")
// Expect Tags
extensionName: "GenAnalytics")
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Tags 1.2
// Generated by ResgenSwift.Analytics 1.2
import UIKit
import MatomoTracker
@ -287,33 +284,33 @@ final class TagsGeneratorTests: XCTestCase {
func testGeneratedExtensionContentMatomoAndFirebase() {
// Given
let sectionOne = TagSection(name: "section_one")
let sectionOne = AnalyticsCategory(id: "section_one")
sectionOne.definitions = [
getTagDefinition(title: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: "screen", tags: ["ios", "iosonly"]),
getTagDefinition(title: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: "event", tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_one", path: "s1_def_one/", name: "s1 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios", "iosonly"]),
getAnalyticsDefinition(id: "s1_def_two", path: "s1_def_two/", name: "s1 def two", type: AnalyticsDefinition.TagType.event, tags: ["ios", "iosonly"]),
]
let sectionTwo = TagSection(name: "section_two")
let sectionTwo = AnalyticsCategory(id: "section_two")
sectionTwo.definitions = [
getTagDefinition(title: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: "screen", tags: ["ios","iosonly"]),
getTagDefinition(title: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s2_def_one", path: "s2_def_one/", name: "s2 def one", type: AnalyticsDefinition.TagType.screen, tags: ["ios","iosonly"]),
getAnalyticsDefinition(id: "s2_def_two", path: "s2_def_two/", name: "s2 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
let sectionThree = TagSection(name: "section_three")
let sectionThree = AnalyticsCategory(id: "section_three")
sectionThree.definitions = [
getTagDefinition(title: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: "screen", tags: ["droid","droidonly"]),
getTagDefinition(title: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: "event", tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_one", path: "s3_def_one/", name: "s3 def one", type: AnalyticsDefinition.TagType.screen, tags: ["droid","droidonly"]),
getAnalyticsDefinition(id: "s3_def_two", path: "s3_def_two/", name: "s3 def two", type: AnalyticsDefinition.TagType.event, tags: ["droid","droidonly"]),
]
// When
TagsGenerator.targets = [Tags.TargetType.matomo, Tags.TargetType.firebase]
let extensionContent = TagsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
AnalyticsGenerator.targets = [Analytics.TargetType.matomo, Analytics.TargetType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"],
staticVar: false,
extensionName: "GenTags")
// Expect Tags
extensionName: "GenAnalytics")
// Expect Analytics
let expect = """
// Generated by ResgenSwift.Tags 1.2
// Generated by ResgenSwift.Analytics 1.2
import UIKit
import MatomoTracker

View File

@ -1,5 +1,5 @@
//
// TagSectionTests.swift
// AnalyticsSectionTests.swift
//
//
// Created by Loris Perret on 06/12/2023.
@ -10,53 +10,21 @@ import XCTest
@testable import ResgenSwift
final class TagSectionTests: XCTestCase {
// MARK: - Match line
func testMatchingTagSection() {
// Given
let line = "[[section_name]]"
// When
let section = TagSection.match(line)
// Expect
XCTAssertNotNil(section)
XCTAssertEqual(section?.name, "section_name")
}
func testNotMatchingTagSection() {
// Given
let lines = ["section_name",
"[section_name]",
"[section_name",
"[[section_name",
"[[section_name]",
"section_name]",
"section_name]]",
"[section_name]]"]
// When
let matches = lines.compactMap { TagSection.match($0) }
// Expect
XCTAssertEqual(matches.isEmpty, true)
}
final class AnalyticsSectionTests: XCTestCase {
// MARK: - Matching tags
func testMatchingTags() {
func testMatchingAnalytics() {
// Given
let section = TagSection(name: "section_name")
let section = AnalyticsCategory(id: "section_name")
section.definitions = [
{
let def = TagDefinition(title: "definition_name")
let def = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
def.tags = ["ios","iosonly"]
return def
}(),
{
let def = TagDefinition(title: "definition_name_two")
let def = AnalyticsDefinition(id: "definition_name_two", name: "", type: .screen)
def.tags = ["droid","droidonly"]
return def
}()
@ -75,17 +43,17 @@ final class TagSectionTests: XCTestCase {
XCTAssertTrue(match4)
}
func testNotMatchingTags() {
func testNotMatchingAnalytics() {
// Given
let section = TagSection(name: "section_name")
let section = AnalyticsCategory(id: "section_name")
section.definitions = [
{
let def = TagDefinition(title: "definition_name")
let def = AnalyticsDefinition(id: "definition_name", name: "", type: .screen)
def.tags = ["ios","iosonly"]
return def
}(),
{
let def = TagDefinition(title: "definition_name_two")
let def = AnalyticsDefinition(id: "definition_name_two", name: "", type: .screen)
def.tags = ["droid","droidonly"]
return def
}()