Compare commits

..

No commits in common. 'main' and 'newoffer2025' have entirely different histories.

  1. BIN
      .DS_Store
  2. 4
      .gitignore
  3. 13
      PadelClubData/Data/Club.swift
  4. 2
      PadelClubData/Data/Court.swift
  5. 28
      PadelClubData/Data/DataStore.swift
  6. 2
      PadelClubData/Data/DateInterval.swift
  7. 2
      PadelClubData/Data/DrawLog.swift
  8. 50
      PadelClubData/Data/Event.swift
  9. 23
      PadelClubData/Data/Gen/BaseClub.swift
  10. 20
      PadelClubData/Data/Gen/BaseCourt.swift
  11. 20
      PadelClubData/Data/Gen/BaseCustomUser.swift
  12. 11
      PadelClubData/Data/Gen/BaseDateInterval.swift
  13. 20
      PadelClubData/Data/Gen/BaseDrawLog.swift
  14. 26
      PadelClubData/Data/Gen/BaseEvent.swift
  15. 23
      PadelClubData/Data/Gen/BaseGroupStage.swift
  16. 22
      PadelClubData/Data/Gen/BaseMatch.swift
  17. 20
      PadelClubData/Data/Gen/BaseMatchScheduler.swift
  18. 11
      PadelClubData/Data/Gen/BaseMonthData.swift
  19. 20
      PadelClubData/Data/Gen/BasePlayerRegistration.swift
  20. 20
      PadelClubData/Data/Gen/BasePurchase.swift
  21. 22
      PadelClubData/Data/Gen/BaseRound.swift
  22. 26
      PadelClubData/Data/Gen/BaseTeamRegistration.swift
  23. 20
      PadelClubData/Data/Gen/BaseTeamScore.swift
  24. 35
      PadelClubData/Data/Gen/BaseTournament.swift
  25. 3
      PadelClubData/Data/Gen/Drawlog.json
  26. 2
      PadelClubData/Data/Gen/GroupStage.json
  27. 4
      PadelClubData/Data/Gen/Match.json
  28. 2
      PadelClubData/Data/Gen/MatchScheduler.json
  29. 1
      PadelClubData/Data/Gen/PlayerRegistration.json
  30. 3
      PadelClubData/Data/Gen/Purchase.json
  31. 3
      PadelClubData/Data/Gen/Round.json
  32. 6
      PadelClubData/Data/Gen/TeamRegistration.json
  33. 5
      PadelClubData/Data/Gen/TeamScore.json
  34. 8
      PadelClubData/Data/Gen/Tournament.json
  35. 255
      PadelClubData/Data/Gen/generator.py
  36. 62
      PadelClubData/Data/GroupStage.swift
  37. 54
      PadelClubData/Data/Match.swift
  38. 13
      PadelClubData/Data/MatchScheduler.swift
  39. 34
      PadelClubData/Data/PlayerRegistration.swift
  40. 53
      PadelClubData/Data/Round.swift
  41. 88
      PadelClubData/Data/TeamRegistration.swift
  42. 8
      PadelClubData/Data/TeamScore.swift
  43. 185
      PadelClubData/Data/Tournament.swift
  44. 9
      PadelClubData/Data/TournamentLibrary.swift
  45. 10
      PadelClubData/Data/TournamentStore.swift
  46. 47
      PadelClubData/Extensions/Date+Extensions.swift
  47. 93
      PadelClubData/Extensions/String+Extensions.swift
  48. 60
      PadelClubData/Subscriptions/Guard.swift
  49. 4
      PadelClubData/Subscriptions/StoreManager.swift
  50. 95
      PadelClubData/Utils/ContactManager.swift
  51. 20
      PadelClubData/Utils/ExportFormat.swift
  52. 16
      PadelClubData/Utils/PListReader.swift
  53. 73
      PadelClubData/ViewModel/PadelRule.swift
  54. 34
      PadelClubDataTests/Config.swift
  55. 31
      PadelClubDataTests/DeletionTests.swift
  56. 101
      PadelClubDataTests/PadelClubDataTests.swift
  57. 1154
      PadelClubDataTests/SyncDataAccessTests.swift
  58. 73
      PadelClubDataTests/SynchronizationTests.swift

BIN
.DS_Store vendored

Binary file not shown.

4
.gitignore vendored

@ -3,12 +3,8 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
PadelClubDataTests/config.plist
## User settings
xcuserdata/
.DS_Store
config.plist
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint

@ -29,12 +29,15 @@ final public class Club: BaseClub {
DataStore.shared.courts.filter { $0.club == self.id }.sorted(by: \.index)
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Court.self, actionOption: actionOption) { $0.club == self.id }
}
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
store.deleteDependencies(type: Court.self, shouldBeSynchronized: shouldBeSynchronized) { $0.club == self.id }
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: Court.self) { $0.club == self.id }
// let customizedCourts = self.customizedCourts
// for customizedCourt in customizedCourts {
// customizedCourt.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// DataStore.shared.courts.deleteDependencies(customizedCourts, shouldBeSynchronized: shouldBeSynchronized)
}
}

@ -32,7 +32,7 @@ final public class Court: BaseCourt {
Store.main.findById(club)
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
}
}

@ -22,8 +22,8 @@ public class DataStore: ObservableObject {
if self.user.id != self.userStorage.item()?.id {
self.userStorage.setItemNoSync(self.user)
StoreCenter.main.initialSynchronization(clear: false)
self._fixMissingClubCreatorIfNecessary()
self._fixMissingEventCreatorIfNecessary()
self._fixMissingClubCreatorIfNecessary(self.clubs)
self._fixMissingEventCreatorIfNecessary(self.events)
}
} else {
self._temporaryLocalUser.item = self.user
@ -59,7 +59,7 @@ public class DataStore: ObservableObject {
self.events = store.registerSynchronizedCollection(indexed: indexed)
self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed)
self.userStorage = store.registerObject(synchronized: true, shouldLoadDataFromServer: false)
self.purchases = store.registerSynchronizedCollection(inMemory: true)
self.purchases = Store.main.registerSynchronizedCollection(inMemory: true)
self.monthData = store.registerCollection(indexed: indexed)
@ -96,12 +96,12 @@ public class DataStore: ObservableObject {
@objc func collectionDidLoad(notification: Notification) {
if let userSingleton: StoredCollection<CustomUser> = notification.object as? StoredCollection<CustomUser> {
self.user = userSingleton.first ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if notification.object is StoredCollection<Club> {
self._fixMissingClubCreatorIfNecessary()
} else if notification.object is StoredCollection<Event> {
self._fixMissingEventCreatorIfNecessary()
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> {
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if let clubsCollection: SyncedCollection<Club> = notification.object as? SyncedCollection<Club> {
self._fixMissingClubCreatorIfNecessary(clubsCollection)
} else if let eventsCollection: SyncedCollection<Event> = notification.object as? SyncedCollection<Event> {
self._fixMissingEventCreatorIfNecessary(eventsCollection)
}
if Store.main.fileCollectionsAllLoaded() {
@ -111,11 +111,10 @@ public class DataStore: ObservableObject {
}
fileprivate func _fixMissingClubCreatorIfNecessary() {
fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: SyncedCollection<Club>) {
if self.user.clubs.count > 0 { return }
let clubsCollection = DataStore.shared.clubs
for club in clubsCollection {
if let userId = StoreCenter.main.userId, club.creator == nil {
club.creator = userId
@ -126,8 +125,7 @@ public class DataStore: ObservableObject {
}
}
fileprivate func _fixMissingEventCreatorIfNecessary() {
let eventsCollection = DataStore.shared.events
fileprivate func _fixMissingEventCreatorIfNecessary(_ eventsCollection: SyncedCollection<Event>) {
for event in eventsCollection {
if let userId = StoreCenter.main.userId, event.creator == nil {
event.creator = userId
@ -332,7 +330,7 @@ public class DataStore: ObservableObject {
_lastRunningAndNextCheckDate = nil
}
public func runningAndNextMatches(_ selectedTournaments: Set<String> = Set()) -> [Match] {
public func runningAndNextMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate,
let cachedMatches = _cachedRunningAndNextMatches,
@ -340,7 +338,7 @@ public class DataStore: ObservableObject {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { (selectedTournaments.isEmpty || selectedTournaments.contains($0.id) == false) && $0.isDeleted == false && $0.startDate <= dateNow.addingTimeInterval(86_400) && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {

@ -28,7 +28,7 @@ final public class DateInterval: BaseDateInterval {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
}
// enum CodingKeys: String, CodingKey {

@ -74,7 +74,7 @@ final public class DrawLog: BaseDrawLog, SideStorable {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
}
}

@ -25,16 +25,24 @@ final public class Event: BaseEvent {
super.init()
}
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: Tournament.self) { $0.event == self.id && $0.isDeleted == false }
store.deleteUnusedSharedDependencies(type: DateInterval.self) { $0.event == self.id }
}
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Tournament.self, shouldBeSynchronized: shouldBeSynchronized) { $0.event == self.id && $0.isDeleted == false }
store.deleteDependencies(type: DateInterval.self, shouldBeSynchronized: shouldBeSynchronized) { $0.event == self.id }
store.deleteDependencies(type: Tournament.self, actionOption: actionOption) { $0.event == self.id && $0.isDeleted == false }
store.deleteDependencies(type: DateInterval.self, actionOption: actionOption) { $0.event == self.id }
// let tournaments = self.tournaments
// for tournament in tournaments {
// tournament.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
//
// DataStore.shared.tournaments.deleteDependencies(tournaments, shouldBeSynchronized: shouldBeSynchronized)
//
// let courtsUnavailabilities = self.courtsUnavailability
// for courtsUnavailability in courtsUnavailabilities {
// courtsUnavailability.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities, shouldBeSynchronized: shouldBeSynchronized)
}
// MARK: - Computed dependencies
@ -184,34 +192,6 @@ final public class Event: BaseEvent {
return link.compactMap({ $0 }).joined(separator: "\n\n")
}
public func selectedTeams() -> [TeamRegistration] {
confirmedTournaments().flatMap({ $0.selectedSortedTeams() })
}
public func umpireMail() -> [String]? {
confirmedTournaments().first?.umpireMail()
}
public func mailSubject() -> String {
let tournaments = confirmedTournaments()
guard !tournaments.isEmpty else {
return eventTitle()
}
// Get the date range from all confirmed tournaments
let dates = tournaments.compactMap { $0.startDate }
guard let firstDate = dates.min(), let lastDate = dates.max() else {
return eventTitle()
}
let dateRange = firstDate == lastDate
? firstDate.formattedDate(.short)
: "\(firstDate.formatted(.dateTime.day())) au \(lastDate.formatted(.dateTime.day())) \(lastDate.formatted(.dateTime.month(.wide))) \(lastDate.formatted(.dateTime.year()))"
let subject = [eventTitle(), dateRange, clubObject()?.name].compactMap({ $0 }).joined(separator: " | ")
return subject
}
func insertOnServer() throws {
DataStore.shared.events.writeChangeAndInsertOnServer(instance: self)

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseClub: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "clubs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = true
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var creator: String? = nil
@ -121,7 +120,7 @@ public class BaseClub: SyncedModelObject, SyncedStorable {
func creatorValue() -> CustomUser? {
guard let creator = self.creator else { return nil }
return self.store?.findById(creator)
return Store.main.findById(creator)
}
public func copy(from other: any Storable) {
@ -142,24 +141,10 @@ public class BaseClub: SyncedModelObject, SyncedStorable {
self.timezone = club.timezone
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BaseClub.creator, storeLookup: .same),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Event.self, keyPath: \BaseEvent.club, storeLookup: .same),
Relationship(type: Court.self, keyPath: \BaseCourt.club, storeLookup: .same),
Relationship(type: CustomUser.self, keyPath: \BaseClub.creator),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseCourt: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "courts" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var index: Int = 0
@ -72,7 +71,7 @@ public class BaseCourt: SyncedModelObject, SyncedStorable {
}
func clubValue() -> Club? {
return self.store?.findById(club)
return Store.main.findById(club)
}
public func copy(from other: any Storable) {
@ -85,21 +84,10 @@ public class BaseCourt: SyncedModelObject, SyncedStorable {
self.indoor = court.indoor
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Club.self, keyPath: \BaseCourt.club, storeLookup: .same),
Relationship(type: Club.self, keyPath: \BaseCourt.club),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "users" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var username: String = ""
@ -256,23 +255,8 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.hideUmpirePhone = customuser.hideUmpirePhone
}
public static func parentRelationships() -> [Relationship] {
return []
}
public static func childrenRelationships() -> [Relationship] {
return [
Relationship(type: Club.self, keyPath: \BaseClub.creator, storeLookup: .same),
Relationship(type: Event.self, keyPath: \BaseEvent.creator, storeLookup: .same),
Relationship(type: Purchase.self, keyPath: \BasePurchase.user, storeLookup: .same),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
return []
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseDateInterval: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "date-intervals" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var event: String = ""
@ -74,14 +73,6 @@ public class BaseDateInterval: SyncedModelObject, SyncedStorable {
self.endDate = dateinterval.endDate
}
public static func parentRelationships() -> [Relationship] {
return []
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
return []
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseDrawLog: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "draw-logs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var tournament: String = ""
@ -78,7 +77,7 @@ public class BaseDrawLog: SyncedModelObject, SyncedStorable {
}
func tournamentValue() -> Tournament? {
return self.store?.storeCenter.mainStore.findById(tournament)
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
@ -92,21 +91,10 @@ public class BaseDrawLog: SyncedModelObject, SyncedStorable {
self.drawType = drawlog.drawType
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament, storeLookup: .main),
Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseEvent: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "events" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var creator: String? = nil
@ -73,12 +72,12 @@ public class BaseEvent: SyncedModelObject, SyncedStorable {
func creatorValue() -> CustomUser? {
guard let creator = self.creator else { return nil }
return self.store?.findById(creator)
return Store.main.findById(creator)
}
func clubValue() -> Club? {
guard let club = self.club else { return nil }
return self.store?.findById(club)
return Store.main.findById(club)
}
public func copy(from other: any Storable) {
@ -91,24 +90,11 @@ public class BaseEvent: SyncedModelObject, SyncedStorable {
self.tenupId = event.tenupId
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BaseEvent.creator, storeLookup: .same),
Relationship(type: Club.self, keyPath: \BaseEvent.club, storeLookup: .same),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseTournament.event, storeLookup: .same),
Relationship(type: CustomUser.self, keyPath: \BaseEvent.creator),
Relationship(type: Club.self, keyPath: \BaseEvent.club),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseGroupStage: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "group-stages" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var tournament: String = ""
@ -96,7 +95,7 @@ public class BaseGroupStage: SyncedModelObject, SyncedStorable {
}
func tournamentValue() -> Tournament? {
return self.store?.storeCenter.mainStore.findById(tournament)
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
@ -112,24 +111,10 @@ public class BaseGroupStage: SyncedModelObject, SyncedStorable {
self.plannedStartDate = groupstage.plannedStartDate
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseGroupStage.tournament, storeLookup: .main),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Match.self, keyPath: \BaseMatch.groupStage, storeLookup: .same),
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamRegistration.groupStage, storeLookup: .same),
Relationship(type: Tournament.self, keyPath: \BaseGroupStage.tournament),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseMatch: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "matches" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var round: String? = nil
@ -160,24 +159,11 @@ public class BaseMatch: SyncedModelObject, SyncedStorable {
self.plannedStartDate = match.plannedStartDate
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: Round.self, keyPath: \BaseMatch.round, storeLookup: .same),
Relationship(type: GroupStage.self, keyPath: \BaseMatch.groupStage, storeLookup: .same),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: TeamScore.self, keyPath: \BaseTeamScore.match, storeLookup: .same),
Relationship(type: Round.self, keyPath: \BaseMatch.round),
Relationship(type: GroupStage.self, keyPath: \BaseMatch.groupStage),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseMatchScheduler: BaseModelObject, Storable {
public static func resourceName() -> String { return "match-scheduler" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var tournament: String = ""
@ -144,7 +143,7 @@ public class BaseMatchScheduler: BaseModelObject, Storable {
}
func tournamentValue() -> Tournament? {
return self.store?.storeCenter.mainStore.findById(tournament)
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
@ -169,21 +168,10 @@ public class BaseMatchScheduler: BaseModelObject, Storable {
self.accountGroupStageBreakTime = matchscheduler.accountGroupStageBreakTime
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseMatchScheduler.tournament, storeLookup: .main),
Relationship(type: Tournament.self, keyPath: \BaseMatchScheduler.tournament),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseMonthData: BaseModelObject, Storable {
public static func resourceName() -> String { return "month-data" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var monthKey: String = ""
@ -116,14 +115,6 @@ public class BaseMonthData: BaseModelObject, Storable {
self.fileModelIdentifier = monthdata.fileModelIdentifier
}
public static func parentRelationships() -> [Relationship] {
return []
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
return []
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "player-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var teamRegistration: String? = nil
@ -217,7 +216,7 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
func teamRegistrationValue() -> TeamRegistration? {
guard let teamRegistration = self.teamRegistration else { return nil }
return self.store?.findById(teamRegistration)
return Store.main.findById(teamRegistration)
}
public func copy(from other: any Storable) {
@ -254,21 +253,10 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.contactEmail = playerregistration.contactEmail
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: TeamRegistration.self, keyPath: \BasePlayerRegistration.teamRegistration, storeLookup: .same),
Relationship(type: TeamRegistration.self, keyPath: \BasePlayerRegistration.teamRegistration),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -9,7 +9,6 @@ public class BasePurchase: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "purchases" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: UInt64 = 0
public var user: String = ""
@ -77,7 +76,7 @@ public class BasePurchase: SyncedModelObject, SyncedStorable {
}
func userValue() -> CustomUser? {
return self.store?.findById(user)
return Store.main.findById(user)
}
public func copy(from other: any Storable) {
@ -91,21 +90,10 @@ public class BasePurchase: SyncedModelObject, SyncedStorable {
self.expirationDate = purchase.expirationDate
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BasePurchase.user, storeLookup: .same),
Relationship(type: CustomUser.self, keyPath: \BasePurchase.user),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseRound: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "rounds" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var tournament: String = ""
@ -96,7 +95,7 @@ public class BaseRound: SyncedModelObject, SyncedStorable {
}
func tournamentValue() -> Tournament? {
return self.store?.storeCenter.mainStore.findById(tournament)
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
@ -112,23 +111,10 @@ public class BaseRound: SyncedModelObject, SyncedStorable {
self.plannedStartDate = round.plannedStartDate
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseRound.tournament, storeLookup: .main),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Match.self, keyPath: \BaseMatch.round, storeLookup: .same),
Relationship(type: Tournament.self, keyPath: \BaseRound.tournament),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var tournament: String = ""
@ -167,10 +166,6 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
try super.encode(to: encoder)
}
func tournamentValue() -> Tournament? {
return self.store?.storeCenter.mainStore.findById(tournament)
}
func groupStageValue() -> GroupStage? {
guard let groupStage = self.groupStage else { return nil }
return self.store?.findById(groupStage)
@ -202,25 +197,10 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
self.uniqueRandomIndex = teamregistration.uniqueRandomIndex
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseTeamRegistration.tournament, storeLookup: .main),
Relationship(type: GroupStage.self, keyPath: \BaseTeamRegistration.groupStage, storeLookup: .same),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: TeamScore.self, keyPath: \BaseTeamScore.teamRegistration, storeLookup: .same),
Relationship(type: PlayerRegistration.self, keyPath: \BasePlayerRegistration.teamRegistration, storeLookup: .same),
Relationship(type: GroupStage.self, keyPath: \BaseTeamRegistration.groupStage),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseTeamScore: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-scores" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return false }
public var id: String = Store.randomId()
public var match: String = ""
@ -90,22 +89,11 @@ public class BaseTeamScore: SyncedModelObject, SyncedStorable {
self.luckyLoser = teamscore.luckyLoser
}
public static func parentRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: Match.self, keyPath: \BaseTeamScore.match, storeLookup: .same),
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamScore.teamRegistration, storeLookup: .same),
Relationship(type: Match.self, keyPath: \BaseTeamScore.match),
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamScore.teamRegistration),
]
}
public static func childrenRelationships() -> [Relationship] {
return []
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -1,4 +1,4 @@
// Generated by LeStorageGenerator
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
@ -11,7 +11,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "tournaments" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public static func storeParent() -> Bool { return true }
public var id: String = Store.randomId()
public var event: String? = nil
@ -85,7 +84,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var clubMemberFeeDeduction: Double? = nil
public var unregisterDeltaInHours: Int = 24
public var currencyCode: String? = nil
public var customClubName: String? = nil
public init(
id: String = Store.randomId(),
@ -159,8 +157,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
showTeamsInProg: Bool = false,
clubMemberFeeDeduction: Double? = nil,
unregisterDeltaInHours: Int = 24,
currencyCode: String? = nil,
customClubName: String? = nil
currencyCode: String? = nil
) {
super.init()
self.id = id
@ -235,7 +232,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = clubMemberFeeDeduction
self.unregisterDeltaInHours = unregisterDeltaInHours
self.currencyCode = currencyCode
self.customClubName = customClubName
}
required public override init() {
super.init()
@ -316,7 +312,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
case _clubMemberFeeDeduction = "clubMemberFeeDeduction"
case _unregisterDeltaInHours = "unregisterDeltaInHours"
case _currencyCode = "currencyCode"
case _customClubName = "customClubName"
}
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -460,7 +455,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil
self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24
self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil
self.customClubName = try container.decodeIfPresent(String.self, forKey: ._customClubName) ?? nil
try super.init(from: decoder)
}
@ -538,13 +532,12 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction)
try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours)
try container.encode(self.currencyCode, forKey: ._currencyCode)
try container.encode(self.customClubName, forKey: ._customClubName)
try super.encode(to: encoder)
}
func eventValue() -> Event? {
guard let event = self.event else { return nil }
return self.store?.findById(event)
return Store.main.findById(event)
}
public func copy(from other: any Storable) {
@ -621,30 +614,12 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction
self.unregisterDeltaInHours = tournament.unregisterDeltaInHours
self.currencyCode = tournament.currencyCode
self.customClubName = tournament.customClubName
}
public static func parentRelationships() -> [Relationship] {
return [
Relationship(type: Event.self, keyPath: \BaseTournament.event, storeLookup: .same),
]
}
public static func childrenRelationships() -> [Relationship] {
public static func relationships() -> [Relationship] {
return [
Relationship(type: GroupStage.self, keyPath: \BaseGroupStage.tournament, storeLookup: .child),
Relationship(type: MatchScheduler.self, keyPath: \BaseMatchScheduler.tournament, storeLookup: .child),
Relationship(type: Round.self, keyPath: \BaseRound.tournament, storeLookup: .child),
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamRegistration.tournament, storeLookup: .child),
Relationship(type: DrawLog.self, keyPath: \BaseDrawLog.tournament, storeLookup: .child),
Relationship(type: Event.self, keyPath: \BaseTournament.event),
]
}
public static func relationships() -> [Relationship] {
var relationships: [Relationship] = []
relationships.append(contentsOf: parentRelationships())
relationships.append(contentsOf: childrenRelationships())
return relationships
}
}

@ -6,6 +6,7 @@
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
@ -15,7 +16,7 @@
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament###"
"foreignKey": "Tournament"
},
{
"name": "drawDate",

@ -13,7 +13,7 @@
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament###"
"foreignKey": "Tournament"
},
{
"name": "index",

@ -15,13 +15,13 @@
"name": "round",
"type": "String",
"optional": true,
"foreignKey": "Round"
"foreignKey": "Round*"
},
{
"name": "groupStage",
"type": "String",
"optional": true,
"foreignKey": "GroupStage"
"foreignKey": "GroupStage*"
},
{
"name": "startDate",

@ -15,7 +15,7 @@
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament###"
"foreignKey": "Tournament"
},
{
"name": "timeDifferenceLimit",

@ -5,6 +5,7 @@
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": ["teamRegistration"],
"properties": [
{
"name": "id",

@ -44,7 +44,8 @@
"defaultValue": "nil"
}
],
"tokenExemptedMethods": []
"tokenExemptedMethods": [],
"relationshipNames": []
}
]
}

@ -5,6 +5,7 @@
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
@ -14,7 +15,7 @@
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament###"
"foreignKey": "Tournament"
},
{
"name": "index",

@ -5,6 +5,7 @@
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
@ -13,14 +14,13 @@
},
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament###"
"type": "String"
},
{
"name": "groupStage",
"type": "String",
"optional": true,
"foreignKey": "GroupStage"
"foreignKey": "GroupStage*"
},
{
"name": "registrationDate",

@ -5,6 +5,7 @@
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": ["match"],
"properties": [
{
"name": "id",
@ -14,13 +15,13 @@
{
"name": "match",
"type": "String",
"foreignKey": "Match"
"foreignKey": "Match*"
},
{
"name": "teamRegistration",
"type": "String",
"optional": true,
"foreignKey": "TeamRegistration"
"foreignKey": "TeamRegistration*"
},
{
"name": "score",

@ -3,8 +3,9 @@
{
"name": "Tournament",
"synchronizable": true,
"copyable": true,
"observable": true,
"storeParent": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
@ -372,11 +373,6 @@
"name": "currencyCode",
"type": "String",
"optional": true
},
{
"name": "customClubName",
"type": "String",
"optional": true
}
]
}

@ -3,85 +3,21 @@ import re
import os
from pathlib import Path
from typing import Dict, List, Any
from collections import defaultdict
import argparse
import sys
import logging
from datetime import datetime
import inflect
class RelationshipAnalyzer:
def __init__(self):
self.parent_relationships = defaultdict(list) # model_name -> list of parent relationships
self.children_relationships = defaultdict(list) # model_name -> list of children relationships
def analyze_all_models(self, input_dir: str) -> None:
"""Analyze all JSON files to build complete relationship map."""
input_path = Path(input_dir)
json_files = list(input_path.glob("*.json"))
for json_file in json_files:
with open(json_file, 'r') as f:
json_data = json.load(f)
for model in json_data["models"]:
model_name = model["name"]
properties = model.get("properties", [])
# Find foreign key properties (parents)
for prop in properties:
if "foreignKey" in prop:
foreign_key = prop["foreignKey"].rstrip('###')
located_on_main_store = prop["foreignKey"].endswith('###')
if located_on_main_store:
# Store parent relationship
self.parent_relationships[model_name].append({
"name": prop["name"],
"foreignKey": foreign_key,
"storeLookup": ".main"
})
# Store children relationship (reverse)
self.children_relationships[foreign_key].append({
"name": prop["name"],
"childModel": model_name,
"storeLookup": ".child"
})
else:
# Store parent relationship
self.parent_relationships[model_name].append({
"name": prop["name"],
"foreignKey": foreign_key,
"storeLookup": ".same"
})
# Store children relationship (reverse)
self.children_relationships[foreign_key].append({
"name": prop["name"],
"childModel": model_name,
"storeLookup": ".same"
})
def get_parent_relationships(self, model_name: str) -> List[Dict[str, Any]]:
"""Get parent relationships for a model."""
return self.parent_relationships.get(model_name, [])
def get_children_relationships(self, model_name: str) -> List[Dict[str, Any]]:
"""Get children relationships for a model."""
return self.children_relationships.get(model_name, [])
class LeStorageGenerator:
def __init__(self, json_data: Dict[str, Any], relationship_analyzer: RelationshipAnalyzer = None):
self.json_data = json_data
self.relationship_analyzer = relationship_analyzer
class SwiftModelGenerator:
def __init__(self, json_data: Dict[str, Any]):
self.data = json_data
self.pluralizer = inflect.engine()
def generate_model(self, model_data: Dict[str, Any]) -> str:
model_name = model_data["name"]
is_sync = model_data.get("synchronizable", False)
is_observable = model_data.get("observable", False)
store_parent = model_data.get("storeParent", False)
properties = model_data["properties"]
did_set_properties = []
@ -91,7 +27,7 @@ class LeStorageGenerator:
token_exempted = model_data.get("tokenExemptedMethods", [])
copy_server_response = model_data.get("copy_server_response", "false")
lines = ["// Generated by LeStorageGenerator", "// Do not modify this file manually", ""]
lines = ["// Generated by SwiftModelGenerator", "// Do not modify this file manually", ""]
# Import statement
lines.append("import Foundation")
@ -109,7 +45,7 @@ class LeStorageGenerator:
lines.append("")
# Add SyncedStorable protocol requirements
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, copy_server_response, store_parent))
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, copy_server_response))
lines.append("")
# Properties
@ -166,11 +102,6 @@ class LeStorageGenerator:
lines.extend(self._generate_copy_method(model_name, properties))
lines.append("")
# Copy method
# if is_sync:
# lines.extend(self._generate_copy_for_update_method(model_name, properties))
# lines.append("")
# Add relationships function
lines.extend(self._generate_relationships(model_name, properties))
lines.append("")
@ -247,21 +178,21 @@ class LeStorageGenerator:
method_name = f"{prop_name}Value"
is_optional = prop.get("optional", False)
lines.extend([f" func {method_name}() -> {foreign_key.rstrip('###')}? {{"])
lines.extend([f" func {method_name}() -> {foreign_key.rstrip('*')}? {{"])
if is_optional:
lines.extend([
f" guard let {prop_name} = self.{prop_name} else {{ return nil }}"
])
if foreign_key.endswith("###"):
if foreign_key.endswith("*"):
foreign_key = foreign_key[:-1] # Remove the asterisk
lines.extend([
f" return self.store?.storeCenter.mainStore.findById({prop_name})"
f" return self.store?.findById({prop_name})"
])
else:
lines.extend([
f" return self.store?.findById({prop_name})"
f" return Store.main.findById({prop_name})"
])
lines.extend([" }", ""]) # Close the method and add a blank line
@ -462,114 +393,43 @@ class LeStorageGenerator:
lines.append(" }")
return lines
def _generate_copy_for_update_method(self, model_name: str, properties: List[Dict[str, Any]]) -> List[str]:
model_variable = model_name.lower()
lines = [f" public func copyForUpdate(from other: any Storable) {{"]
lines.append(f" guard let {model_variable} = other as? Base{model_name} else {{ return }}")
# First handle foreign key properties with special deletion logic
for prop in properties:
if "foreignKey" in prop and prop["foreignKey"] != "CustomUser":
name = prop["name"]
foreign_key = prop["foreignKey"].rstrip('###') # Remove asterisk if present
foreign_variable = name + "Value"
# Generate the foreign key check and deletion logic
lines.append(f" if {model_variable}.{name} != self.{name}, let {name}Object = self.{foreign_variable}(), {name}Object.shared == true, let store = {name}Object.store {{")
lines.append(f" store.deleteUnusedShared({name}Object)")
lines.append(f" }}")
# Then copy all properties
for prop in properties:
name = prop['name']
lines.append(f" self.{name} = {model_variable}.{name}")
lines.append(" }")
return lines
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str], copy_server_response: str, store_parent: bool) -> List[str]:
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str], copy_server_response: str) -> List[str]:
"""Generate the static functions required by SyncedStorable protocol."""
# Convert HTTP methods to proper format
formatted_methods = [f".{method.lower()}" for method in token_exempted]
methods_str = ", ".join(formatted_methods) if formatted_methods else ""
store_parent_swift = "true" if store_parent else "false"
return [
f" public static func resourceName() -> String {{ return \"{resource_name}\" }}",
f" public static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}",
f" public static var copyServerResponse: Bool = {copy_server_response}",
f" public static func storeParent() -> Bool {{ return {store_parent_swift} }}",
]
def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]:
# if not self.relationship_analyzer:
# # Fallback to old behavior if no analyzer provided
# return self._generate_legacy_relationships(model_name, properties)
lines = []
# Generate parentRelationships method
lines.extend(self._generate_parent_relationships(model_name))
lines.append("")
# Generate childrenRelationships method
lines.extend(self._generate_children_relationships(model_name))
lines.append("")
# Generate combined relationships method
lines.extend(self._generate_combined_relationships(model_name))
return lines
def _generate_parent_relationships(self, model_name: str) -> List[str]:
"""Generate parentRelationships() method."""
parent_rels = self.relationship_analyzer.get_parent_relationships(model_name)
if not parent_rels:
return [
" public static func parentRelationships() -> [Relationship] {",
" return []",
" }"
]
lines = [
" public static func parentRelationships() -> [Relationship] {",
" return ["
]
for rel in parent_rels:
# main_store = "true" if rel["storeLookup"] else "false"
lines.append(f" Relationship(type: {rel['foreignKey']}.self, keyPath: \\Base{model_name}.{rel['name']}, storeLookup: {rel["storeLookup"]}),")
lines.extend([
" ]",
" }"
])
return lines
def _generate_children_relationships(self, model_name: str) -> List[str]:
"""Generate childrenRelationships() method."""
children_rels = self.relationship_analyzer.get_children_relationships(model_name)
# Find all properties with foreign keys
foreign_key_props = [p for p in properties if "foreignKey" in p]
if not children_rels:
if not foreign_key_props:
# If no foreign keys, return empty array
return [
" public static func childrenRelationships() -> [Relationship] {",
" public static func relationships() -> [Relationship] {",
" return []",
" }"
]
lines = [
" public static func childrenRelationships() -> [Relationship] {",
" public static func relationships() -> [Relationship] {",
" return ["
]
for rel in children_rels:
# main_store = "true" if rel["storeLookup"] else "false"
lines.append(f" Relationship(type: {rel['childModel']}.self, keyPath: \\Base{rel['childModel']}.{rel['name']}, storeLookup: {rel["storeLookup"]}),")
# Generate relationship entries
for prop in foreign_key_props:
name = prop["name"]
foreign_key = prop["foreignKey"].rstrip('*') # Remove asterisk if present
lines.append(f" Relationship(type: {foreign_key}.self, keyPath: \\Base{model_name}.{name}),")
# Close the array and function
lines.extend([
" ]",
" }"
@ -577,66 +437,6 @@ class LeStorageGenerator:
return lines
def _generate_combined_relationships(self, model_name: str) -> List[str]:
"""Generate relationships() method that combines parent and children."""
parent_rels = self.relationship_analyzer.get_parent_relationships(model_name)
children_rels = self.relationship_analyzer.get_children_relationships(model_name)
if not parent_rels and not children_rels:
return [
" public static func relationships() -> [Relationship] {",
" return []",
" }"
]
lines = [
" public static func relationships() -> [Relationship] {",
" var relationships: [Relationship] = []",
" relationships.append(contentsOf: parentRelationships())",
" relationships.append(contentsOf: childrenRelationships())",
" return relationships",
" }"
]
return lines
# def _generate_legacy_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]:
# """Legacy relationship generation for backward compatibility."""
# # Find all properties with foreign keys
# foreign_key_props = [p for p in properties if "foreignKey" in p]
#
# if not foreign_key_props:
# # If no foreign keys, return empty array
# return [
# " public static func relationships() -> [Relationship] {",
# " return []",
# " }"
# ]
#
# lines = [
# " public static func relationships() -> [Relationship] {",
# " return ["
# ]
#
# # Generate relationship entries
# for prop in foreign_key_props:
# name = prop["name"]
# located_on_main_store = "true" if prop["foreignKey"].endswith('###') else "false"
# foreign_key = prop["foreignKey"].rstrip('###') # Remove asterisk if present
#
# if located_on_main_store:
# lines.append(f" Relationship(type: {foreign_key}.self, keyPath: \\Base{model_name}.{name}, storeLookup: {located_on_main_store}),")
# else:
# lines.append(f" Relationship(type: {foreign_key}.self, keyPath: \\Base{model_name}.{name}, storeLookup: {located_on_main_store}),")
#
# # Close the array and function
# lines.extend([
# " ]",
# " }"
# ])
#
# return lines
def _get_default_value(self, type_name: str) -> str:
"""Get default value for non-optional types"""
if "String" in type_name:
@ -681,21 +481,14 @@ def process_directory(input_dir: str, output_dir: str, logger: logging.Logger, d
return 0
logger.info(f"Found {len(json_files)} JSON files to process")
# First pass: Analyze all relationships
logger.info("Analyzing relationships across all models...")
relationship_analyzer = RelationshipAnalyzer()
relationship_analyzer.analyze_all_models(input_dir)
successful_files = 0
# Second pass: Generate models with complete relationship information
for json_file in json_files:
try:
with open(json_file, 'r') as f:
json_data = json.load(f)
generator = LeStorageGenerator(json_data, relationship_analyzer)
generator = SwiftModelGenerator(json_data)
# Generate each model in the JSON file
for model in json_data["models"]:
@ -731,7 +524,7 @@ def process_directory(input_dir: str, output_dir: str, logger: logging.Logger, d
def setup_logging(verbose: bool) -> logging.Logger:
"""Configure logging based on verbosity level."""
logger = logging.getLogger('LeStorageGenerator')
logger = logging.getLogger('SwiftModelGenerator')
handler = logging.StreamHandler()
if verbose:

@ -90,7 +90,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return _matches.anySatisfy { $0.hasEnded() == false } == false
}
func createMatch(index: Int) -> Match {
fileprivate func _createMatch(index: Int) -> Match {
let match: Match = Match(groupStage: self.id,
index: index,
format: self.matchFormat,
@ -128,7 +128,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
var matches = [Match]()
let matchPhaseCount = matchPhaseCount
for i in 0..<_numberOfMatchesToBuild() {
let newMatch = self.createMatch(index: i + matchCount * matchPhaseCount)
let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount)
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
teamScores.append(contentsOf: newMatch.createTeamScores())
matches.append(newMatch)
@ -147,13 +147,13 @@ final public class GroupStage: BaseGroupStage, SideStorable {
_removeMatches()
for i in 0..<_numberOfMatchesToBuild() {
let newMatch = self.createMatch(index: i)
let newMatch = self._createMatch(index: i)
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
teamScores.append(contentsOf: newMatch.createTeamScores())
matches.append(newMatch)
}
} else {
for match in self._matches() {
for match in _matches() {
match.resetTeamScores(outsideOf: [])
teamScores.append(contentsOf: match.createTeamScores())
}
@ -298,7 +298,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
}
public func readyMatches(playedMatches: [Match], runningMatches: [Match]) -> [Match] {
public func readyMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
@ -306,9 +306,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id })
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false })
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
}
public func finishedMatches(playedMatches: [Match]) -> [Match] {
@ -337,7 +335,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
case 5:
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6:
order = [4, 6, 10, 1, 8, 12, 2, 7, 11, 3, 5, 13, 14, 9, 0]
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
case 7:
order = [6, 15, 20, 1, 16, 19, 2, 10, 18, 3, 9, 14, 4, 7, 12, 5, 8, 11, 0, 13, 17]
case 8:
@ -402,11 +400,11 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? []
}
func _removeMatches() {
private func _removeMatches() {
self.tournamentStore?.matches.delete(contentOfs: _matches())
}
func _numberOfMatchesToBuild() -> Int {
private func _numberOfMatchesToBuild() -> Int {
(size * (size - 1)) / 2
}
@ -449,7 +447,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return teamsSorted.first == teamPosition
} else {
if let matchIndex = combos.firstIndex(of: indexes), let match = self._matches().first(where: { $0.index == matchIndex }) {
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId
} else {
return false
@ -616,41 +614,19 @@ final public class GroupStage: BaseGroupStage, SideStorable {
}
public func computedStartDate() -> Date? {
return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate
return _matches().sorted(by: \.computedStartDateForSorting).first?.startDate
}
public func removeAllTeams() {
let teams = teams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
self._matches().forEach({ $0.updateTeamScores() })
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
public func setData(from correspondingGroupStage: GroupStage, tournamentStartDate: Date, previousTournamentStartDate: Date) {
store.deleteDependencies(type: Match.self, shouldBeSynchronized: shouldBeSynchronized) { $0.groupStage == self.id }
self.matchFormat = correspondingGroupStage.matchFormat
if let correspondingPlannedStartDate = correspondingGroupStage.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.size = correspondingGroupStage.size
self.name = correspondingGroupStage.name
let matches = correspondingGroupStage._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id }
}
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: Match.self) { $0.groupStage == self.id }
//
// let matches = self._matches()
// for match in matches {
// match.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized)
}
func insertOnServer() {

@ -22,25 +22,6 @@ final public class Match: BaseMatch, SideStorable {
public var byeState: Bool = false
//<<<<<<< HEAD
// public init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) {
//
// super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed)
//
// }
//
// required init(from decoder: Decoder) throws {
// try super.init(from: decoder)
// }
//
// required public init() {
// super.init()
//
// let drawLog = DrawLog()
// }
//
//=======
//>>>>>>> main
// MARK: - DidSet
public override func didSetStartDate() {
@ -95,12 +76,15 @@ final public class Match: BaseMatch, SideStorable {
// MARK: -
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: TeamScore.self, actionOption: actionOption) { $0.match == self.id }
}
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: TeamScore.self) { $0.match == self.id }
store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.match == self.id }
// let teamScores = self.teamScores
// for teamScore in teamScores {
// teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized)
}
public func indexInRound(in matches: [Match]? = nil) -> Int {
@ -494,15 +478,7 @@ defer {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
}
guard let roundObject else { return index }
// primary: theoryCumulativeMatchCount (final = 1, semis = 3, quarters = 7, ...)
// multiply by a factor big enough to separate match indexes
let primary = roundObject.theoryCumulativeMatchCount * 1_000
// tie-breaker: match position inside the round (0..N-1)
let secondary = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
return primary + secondary
return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index)
}
public func previousMatches() -> [Match] {
@ -1140,18 +1116,6 @@ defer {
plannedStartDate ?? startDate
}
public func setData(from correspondingMatch: Match, tournamentStartDate: Date, previousTournamentStartDate: Date) {
if let correspondingMatchPlannedStartDate = correspondingMatch.plannedStartDate {
let offset = correspondingMatchPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.disabled = correspondingMatch.disabled
self.matchFormat = correspondingMatch.matchFormat
self.courtIndex = correspondingMatch.courtIndex
self.name = correspondingMatch.name
}
func insertOnServer() {
self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)
for teamScore in self.teamScores {

@ -420,7 +420,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
rotationIndex += 1
}
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, matchRank: match.computedOrder, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
}
@ -534,7 +534,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
for i in 0..<rotationIndex {
let courtsSorted = slots.filter { $0.rotationIndex == i }.map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.matchRank))
var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.courtIndex))
for j in 0..<matches.count {
matches[j].courtIndex = courts[j]
@ -621,7 +621,6 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
let timeMatch = TimeMatch(
matchID: firstMatch.id,
rotationIndex: rotationIndex,
matchRank: firstMatch.computedOrder,
courtIndex: courtIndex,
startDate: rotationStartDate,
durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration),
@ -840,10 +839,8 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) {
if tournament.groupStageCount > 0 {
bracketStartDate = lastDate.tomorrowAtNine
}
}
return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate)
}
@ -859,7 +856,6 @@ struct GroupStageTimeMatch {
public struct TimeMatch {
let matchID: String
let rotationIndex: Int
let matchRank: Int
var courtIndex: Int
var startDate: Date
var durationLeft: Int //in minutes
@ -898,11 +894,6 @@ extension Match {
return teamIds().contains(id)
}
public func containsTeamIds(_ ids: [String]) -> Bool {
let teamIds = teamIds()
return !Set(ids).isDisjoint(with: teamIds)
}
public func containsTeamIndex(_ id: String) -> Bool {
matchUp().contains(id)
}

@ -56,23 +56,12 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
return nil
}
public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
switch type {
case .payment:
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, hasPaidOnline() ? "Payé [X]" : "Payé  [ ]"].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
case .sharing:
public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized, hasPaid() ? "Payé" : "", hasPaidOnline() ? "En ligne" : ""]
.joined(separator: exportFormat.separator())
}
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
}
@ -81,13 +70,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
public func contains(_ searchField: String) -> Bool {
if let paymentId, paymentId.localizedCaseInsensitiveContains(searchField) { return true }
if let email, email.lowercased().localizedCaseInsensitiveContains(searchField) { return true }
if let contactEmail, contactEmail.localizedCaseInsensitiveContains(searchField) { return true }
if let licenceId, licenceId.localizedCaseInsensitiveContains(searchField) { return true }
if searchField.isPhoneNumber(), let phoneNumber, phoneNumber.isSamePhoneNumber(as: searchField) { return true }
if searchField.isPhoneNumber(), let contactPhoneNumber, contactPhoneNumber.isSamePhoneNumber(as: searchField) { return true }
let nameComponents = searchField.canonicalVersion.split(separator: " ")
if nameComponents.count > 1 {
@ -185,7 +167,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
public func setComputedRank(in tournament: Tournament) {
let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 92_327
let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 90_415
let femaleUnranked = tournament.unrankValue(for: false) ?? 0
let currentRank = rank ?? maleUnranked
switch tournament.tournamentCategory {
@ -230,16 +212,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
registrationStatus = .confirmed
}
public func hasMail() -> Bool {
let mails = [email, contactEmail].compactMap({ $0 })
return mails.isEmpty == false && mails.anySatisfy({ $0.isValidEmail() })
}
public func hasMobilePhone() -> Bool {
let phones = [phoneNumber, contactPhoneNumber].compactMap({ $0 })
return phones.isEmpty == false && phones.anySatisfy({ $0.isPhoneNumber() })
}
public func paidAmount(_ tournament: Tournament, accountForGiftOrForfeit: Bool = false) -> Double {
if accountForGiftOrForfeit == false, paymentType == .gift {
return 0.0

@ -56,30 +56,6 @@ final public class Round: BaseRound, SideStorable {
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true }
}
public func setData(from correspondingRound: Round, tournamentStartDate: Date, previousTournamentStartDate: Date) {
let matches = correspondingRound._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
self.matchFormat = correspondingRound.matchFormat
if let correspondingPlannedStartDate = correspondingRound.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.loserBracketMode = correspondingRound.loserBracketMode
self.groupStageLoserBracket = correspondingRound.groupStageLoserBracket
loserRounds().forEach { round in
if let pRound = correspondingRound.loserRounds().first(where: { r in
r.index == round.index
}) {
round.setData(from: pRound, tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
}
// MARK: -
public var matchFormat: MatchFormat {
@ -785,7 +761,10 @@ defer {
guard currentRoundMatchCount > 1 else { return }
guard let tournamentStore else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
let loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat()
let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
// if let parentRound {
// loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat(parentRound.index)
// }
var titles = [String: String]()
@ -912,18 +891,28 @@ defer {
groupStageLoserBracket == true
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.round == self.id }
store.deleteDependencies(type: Round.self, actionOption: actionOption) { $0.parent == self.id }
store.deleteDependencies(type: Match.self, shouldBeSynchronized: shouldBeSynchronized) { $0.round == self.id }
store.deleteDependencies(type: Round.self, shouldBeSynchronized: shouldBeSynchronized) { $0.parent == self.id }
}
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: Match.self) { $0.round == self.id }
store.deleteUnusedSharedDependencies(type: Round.self) { $0.parent == self.id }
// let matches = self._matches()
// for match in matches {
// match.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
//
// self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized)
//
// let loserRounds = self.loserRounds()
// for round in loserRounds {
// round.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
//
// self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized)
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _storeId = "storeId"

@ -50,10 +50,6 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
players().anySatisfy({ $0.registeredOnline })
}
public func hasPaid() -> Bool {
players().allSatisfy({ $0.hasPaid() })
}
public func hasPaidOnline() -> Bool {
players().anySatisfy({ $0.hasPaidOnline() })
}
@ -97,16 +93,22 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
tournamentStore.teamScores.delete(contentOfs: ts)
}
public override func deleteUnusedSharedDependencies(store: Store) {
store.deleteUnusedSharedDependencies(type: TeamScore.self) { $0.teamRegistration == self.id }
store.deleteUnusedSharedDependencies(type: PlayerRegistration.self) { $0.teamRegistration == self.id }
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
store.deleteDependencies(type: TeamScore.self, actionOption: actionOption) { $0.teamRegistration == self.id }
store.deleteDependencies(type: PlayerRegistration.self, actionOption: actionOption) { $0.teamRegistration == self.id }
store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.teamRegistration == self.id }
store.deleteDependencies(type: PlayerRegistration.self, shouldBeSynchronized: shouldBeSynchronized) { $0.teamRegistration == self.id }
// let unsortedPlayers = unsortedPlayers()
// for player in unsortedPlayers {
// player.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers, shouldBeSynchronized: shouldBeSynchronized)
//
// let teamScores = teamScores()
// for teamScore in teamScores {
// teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
// }
// self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized)
}
public func hasArrived(isHere: Bool = false) {
@ -171,7 +173,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} else if let roundMatchStartDate = initialMatch()?.startDate {
return roundMatchStartDate
}
return callDate
return nil
}
public var initialWeight: Int {
@ -211,7 +213,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
public func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchValue() }.first(where: { $0.isRunning() })
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
public func teamScores() -> [TeamScore] {
@ -254,7 +256,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
public func teamLabel(
_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&"
) -> String {
if let name, name.isEmpty == false { return name }
if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(
separator: twoLines ? "\n" : " \(separator) ")
}
@ -378,20 +380,14 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
resetBracketPosition()
}
public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType, _ index: Int = 0) -> String {
public func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
switch exportFormat {
case .rawText:
switch type {
case .sharing:
return [playersPasteData(exportFormat, type: type), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .payment:
return [playersPasteData(exportFormat, type: type), name]
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
}
case .csv:
return [
index.formatted(), playersPasteData(exportFormat, type: type),
index.formatted(), playersPasteData(exportFormat),
isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator())
}
@ -441,20 +437,15 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
public func playersPasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
public func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return players().map { $0.pasteData(exportFormat, type: type) }.joined(
return players().map { $0.pasteData(exportFormat) }.joined(
separator: exportFormat.newLineSeparator())
case .csv:
return players().map {
switch type {
case .sharing:
[$0.pasteData(exportFormat, type: type), isWildCard() ? "WC" : $0.computedRank.formatted()]
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
case .payment:
$0.pasteData(exportFormat, type: type)
}
}.joined(separator: exportFormat.separator())
}
}
@ -551,7 +542,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
public func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 92_327
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_415
}
public func groupStageObject() -> GroupStage? {
@ -688,37 +679,6 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil
}
public func followingMatches() -> [Match] {
guard let tournamentStore else { return [] }
let allTeamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == self.id })
let ids = allTeamScores.compactMap({ $0.match })
let matches = tournamentStore.matches.filter({ match in
ids.contains(match.id)
})
return matches.sorted(by: \.computedStartDateForSorting)
}
public func nextMatch(in followingMatches: [Match]) -> Match? {
return followingMatches.filter({ $0.hasEnded() == false }).first
}
public func lastMatchPlayed(in followingMatches: [Match]) -> Match? {
return followingMatches.first(where: { $0.hasEnded() })
}
public func numberOfRotation(in followingMatches: [Match]) -> (Int, Int)? {
if let nextMatch = nextMatch(in: followingMatches), let nextMatchPlannedStartDate = nextMatch.plannedStartDate, let lastMatchPlayed = lastMatchPlayed(in: followingMatches), let lastMatchPlayedPlannedStartDate = lastMatchPlayed.plannedStartDate {
let courtCount = self.tournamentStore?.matches.filter({ $0.plannedStartDate == nextMatchPlannedStartDate && $0.disabled == false && $0.hasEnded() == false && $0.confirmed == true && $0.id != nextMatch.id && $0.hasStarted() == false }).count ?? 0
let interval = nextMatchPlannedStartDate.timeIntervalSince(lastMatchPlayedPlannedStartDate)
let matchDuration = lastMatchPlayed.matchFormat.defaultEstimatedDuration * 60
let rotation = Int(interval) / matchDuration
print("numberOfRotation", interval, matchDuration, courtCount, rotation)
return (rotation, (rotation - 1) * lastMatchPlayed.courtCount() + courtCount + 1)
}
return nil
}
func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {

@ -49,10 +49,10 @@ final public class TeamScore: BaseTeamScore, SideStorable {
// MARK: - Computed dependencies
// public func matchObject() -> Match? {
// return self.tournamentStore?.matches.findById(self.match)
// }
//
public func matchObject() -> Match? {
return self.tournamentStore?.matches.findById(self.match)
}
public var team: TeamRegistration? {
guard let teamRegistration else {
return nil

@ -32,32 +32,16 @@ final public class Tournament: BaseTournament {
return TournamentLibrary.shared.store(tournamentId: self.id)
}
public override func deleteUnusedSharedDependencies(store: Store) {
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
do {
let tournamentStore = try store.alternateStore(identifier: self.id)
tournamentStore.deleteUnusedSharedDependencies(type: DrawLog.self)
tournamentStore.deleteUnusedSharedDependencies(type: TeamRegistration.self)
tournamentStore.deleteUnusedSharedDependencies(type: GroupStage.self)
tournamentStore.deleteUnusedSharedDependencies(type: Round.self)
} catch {
Logger.error(error)
}
store.deleteUnusedSharedDependencies(type: Court.self) { $0.club == self.id }
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
do {
let tournamentStore = try store.alternateStore(identifier: self.id)
tournamentStore.deleteAllDependencies(type: DrawLog.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: TeamRegistration.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: GroupStage.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: Round.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: MatchScheduler.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: DrawLog.self, shouldBeSynchronized: shouldBeSynchronized)
tournamentStore.deleteAllDependencies(type: TeamRegistration.self, shouldBeSynchronized: shouldBeSynchronized)
tournamentStore.deleteAllDependencies(type: GroupStage.self, shouldBeSynchronized: shouldBeSynchronized)
tournamentStore.deleteAllDependencies(type: Round.self, shouldBeSynchronized: shouldBeSynchronized)
tournamentStore.deleteAllDependencies(type: MatchScheduler.self, shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
@ -104,37 +88,12 @@ final public class Tournament: BaseTournament {
return self.tournamentStore?.teamRegistrations.count ?? 0
}
public func deleteGroupStage(_ groupStage: GroupStage) {
groupStage.removeAllTeams()
let index = groupStage.index
self.tournamentStore?.groupStages.delete(instance: groupStage)
self.groupStageCount -= 1
let groupStages = self.groupStages()
groupStages.filter({ $0.index > index }).forEach { gs in
gs.index -= 1
}
self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groupStages)
}
public func addGroupStage() {
let groupStage = GroupStage(tournament: id, index: groupStageCount, size: teamsPerGroupStage, format: groupStageFormat)
self.tournamentStore?.groupStages.addOrUpdate(instance: groupStage)
groupStage.buildMatches(keepExistingMatches: false)
self.groupStageCount += 1
}
public func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.step == step }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
return groupStages.sorted(by: \.index)
}
public func hasGroupeStages() -> Bool {
if groupStageCount > 0 { return true }
guard let tournamentStore = self.tournamentStore else { return false }
return tournamentStore.groupStages.isEmpty == false
}
public func allGroupStages() -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
@ -277,24 +236,17 @@ defer {
return Store.main.findById(event)
}
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat {
case .rawText:
let waitingList = waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
var stats = [String]()
if type == .payment, isAnimation(), minimumPlayerPerTeam == 1 {
stats += ["\(self.selectedPlayers().count.formatted()) personnes"]
} else {
stats += [selectedSortedTeams.count.formatted() + " équipes"]
}
return (stats + selectedSortedTeams.compactMap { $0.pasteData(exportFormat, type: type) } + (waitingList.isEmpty == false ? ["Liste d'attente"] : []) + waitingList.compactMap { $0.pasteData(exportFormat, type: type) }).joined(separator: exportFormat.newLineSeparator(1))
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
case .csv:
let headers = ["", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids", "Paire"].joined(separator: exportFormat.separator())
var teamPaste = [headers]
for (index, team) in selectedSortedTeams.enumerated() {
var teamData = team.pasteData(exportFormat, type: type, index + 1)
var teamData = team.pasteData(exportFormat, index + 1)
teamData.append(exportFormat.separator())
teamData.append(team.teamLastNames().joined(separator: " / "))
teamPaste.append(teamData)
@ -393,7 +345,6 @@ defer {
public func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
}
public func availableSeedGroups(includeAll: Bool = false) -> [SeedInterval] {
let seeds = seeds()
var availableSeedGroup = Set<SeedInterval>()
@ -411,24 +362,6 @@ defer {
return availableSeedGroup.sorted(by: <)
}
public func generateSeedGroups(base: Int, teamCount: Int) -> [SeedInterval] {
let start = base + 1
let root = SeedInterval(first: start, last: start + teamCount - 1)
var groups: [SeedInterval] = []
func split(interval: SeedInterval) {
groups.append(interval)
if let chunks = interval.chunks() {
for chunk in chunks {
split(interval: chunk)
}
}
}
split(interval: root)
return groups.sorted(by: <)
}
public func chunksBy(in chunks: [SeedInterval], availableSeedGroup: inout Set<SeedInterval>) {
chunks.forEach { chunk in
availableSeedGroup.insert(chunk)
@ -721,7 +654,7 @@ defer {
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
let clubName = self.clubName
if prioritizeClubMembers {
var bracketTeams: [TeamRegistration] = []
@ -904,7 +837,7 @@ defer {
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
}
public static func readyMatches(_ allMatches: [Match], runningMatches: [Match]) -> [Match] {
public static func readyMatches(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
@ -912,10 +845,7 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id })
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }).sorted(using: defaultSorting, order: .ascending)
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
}
public static func matchesLeft(_ allMatches: [Match]) -> [Match] {
@ -1179,10 +1109,10 @@ defer {
}
public func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String {
if tournamentLevel == .unlisted {
if tournamentLevel == .unlisted, displayStyle == .title {
if let name {
return name
} else if displayStyle == .title {
} else {
return tournamentLevel.localizedLevelLabel(.title)
}
}
@ -1222,7 +1152,14 @@ defer {
}
public func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
startDate.formattedDate(displayStyle)
switch displayStyle {
case .title:
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
startDate.formatted(date: .numeric, time: .omitted)
}
}
public func qualifiedFromGroupStage() -> Int {
@ -1472,7 +1409,7 @@ defer {
var _groupStages = [GroupStage]()
for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageSmartMatchFormat())
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageFormat)
_groupStages.append(groupStage)
}
@ -1491,7 +1428,7 @@ defer {
let matchCount = RoundRule.numberOfMatches(forTeams: minimalBracketTeamCount ?? bracketTeamCount())
let rounds = (0..<roundCount).map { //index 0 is the final
return Round(tournament: id, index: $0, format: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
return Round(tournament: id, index: $0, format: matchFormat, loserBracketMode: loserBracketMode)
}
if rounds.isEmpty {
@ -1553,9 +1490,6 @@ defer {
public func deleteGroupStages() {
self.tournamentStore?.groupStages.delete(contentOfs: allGroupStages())
if let gs = self.groupStageLoserBracket() {
self.tournamentStore?.rounds.delete(instance: gs)
}
}
public func refreshGroupStages(keepExistingMatches: Bool = false) {
@ -1693,9 +1627,9 @@ defer {
set {
federalLevelCategory = newValue
teamSorting = newValue.defaultTeamSortingType
groupStageMatchFormat = DataStore.shared.user.groupStageMatchFormatPreference ?? groupStageSmartMatchFormat()
loserBracketMatchFormat = DataStore.shared.user.loserBracketMatchFormatPreference ?? loserBracketSmartMatchFormat()
matchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? roundSmartMatchFormat(5)
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(5)
}
}
@ -1708,8 +1642,8 @@ defer {
}
}
public func loserBracketSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForLoserBracketRound()
public func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex)
if tournamentLevel == .p25 { return .superTie }
if format.rank < loserBracketMatchFormat.rank {
return format
@ -1720,6 +1654,7 @@ defer {
public func groupStageSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForGroupStage()
if tournamentLevel == .p25 { return .superTie }
if format.rank < groupStageMatchFormat.rank {
return format
} else {
@ -1728,21 +1663,19 @@ defer {
}
public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) {
courtCount = eventObject()?.clubObject()?.courtCount ?? 2
setupDefaultPrivateSettings(templateTournament: templateTournament)
setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings
if let templateTournament {
setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount)
}
setupFederalSettings()
customizeUsingPreferences()
}
public func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = DataStore.shared.user.groupStageMatchFormatPreference ?? groupStageSmartMatchFormat()
loserBracketMatchFormat = DataStore.shared.user.loserBracketMatchFormatPreference ?? loserBracketSmartMatchFormat()
matchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? roundSmartMatchFormat(5)
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
registrationDateLimit = deadline(for: .inscription)
if enableOnlineRegistration, isAnimation() == false {
@ -1751,23 +1684,6 @@ defer {
}
}
public func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.entryFee = lastTournamentWithSameBuild.entryFee
self.clubMemberFeeDeduction = lastTournamentWithSameBuild.clubMemberFeeDeduction
}
public func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
@ -1857,6 +1773,7 @@ defer {
public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if tournamentLevel == .p25 { return .superTie }
if format.rank < matchFormat.rank {
return format
} else {
@ -2124,17 +2041,6 @@ defer {
}
}
public func removeRound(_ round: Round) async {
await MainActor.run {
let teams = round.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: round)
}
}
public func addNewRound(_ roundIndex: Int) async {
await MainActor.run {
let round = Round(tournament: id, index: roundIndex, format: matchFormat)
@ -2320,11 +2226,7 @@ defer {
}
public func onlineTeams() -> [TeamRegistration] {
// guard let teamRegistrations = tournamentStore?.teamRegistrations else { return [] }
// return teamRegistrations.cached(key: "online") { collection in
// collection.filter { $0.hasRegisteredOnline() }
// }
return unsortedTeams().filter({ $0.hasRegisteredOnline() })
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
public func paidOnlineTeams() -> [TeamRegistration] {
@ -2359,7 +2261,7 @@ defer {
}
public func mailSubject() -> String {
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), customClubName ?? clubName].compactMap({ $0 }).joined(separator: " | ")
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), clubName].compactMap({ $0 }).joined(separator: " | ")
return subject
}
@ -2464,9 +2366,6 @@ defer {
}
public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
if tournamentCategory != .men {
return 0
}
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
@ -2506,16 +2405,6 @@ defer {
self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
}
public func formatSummary() -> String {
var label = [String]()
if groupStageCount > 0 {
label.append("Poules " + groupStageMatchFormat.format)
}
label.append("Tableau " + matchFormat.format)
label.append("Classement " + loserBracketMatchFormat.format)
return label.joined(separator: ", ")
}
// MARK: -
func insertOnServer() throws {

@ -20,15 +20,10 @@ public class TournamentLibrary {
if let store = self._stores[tournamentId] {
return store
}
do {
let store = try StoreCenter.main.store(identifier: tournamentId)
let tournamentStore = TournamentStore(store: store, tournamentId: tournamentId)
let store = StoreCenter.main.requestStore(identifier: tournamentId)
let tournamentStore = TournamentStore(store: store)
self._stores[tournamentId] = tournamentStore
return tournamentStore
} catch {
Logger.error(error)
return nil
}
}
func reset() {

@ -13,7 +13,6 @@ import Combine
public class TournamentStore: ObservableObject {
var store: Store
let tournamentId: String
public fileprivate(set) var groupStages: SyncedCollection<GroupStage> = SyncedCollection.placeholder()
public fileprivate(set) var matches: SyncedCollection<Match> = SyncedCollection.placeholder()
@ -31,18 +30,13 @@ public class TournamentStore: ObservableObject {
// self._initialize()
// }
init(store: Store, tournamentId: String) {
init(store: Store) {
self.store = store
self.tournamentId = tournamentId
self._initialize()
}
fileprivate func _initialize() {
guard let tournament = DataStore.shared.tournaments.findById(self.tournamentId) else {
return
}
let indexed: Bool = true
self.groupStages = self.store.registerSynchronizedCollection(indexed: indexed)
@ -54,9 +48,7 @@ public class TournamentStore: ObservableObject {
self.matchSchedulers = self.store.registerCollection(indexed: indexed)
self.drawLogs = self.store.registerSynchronizedCollection(indexed: indexed)
if tournament.sharing == nil {
self.store.loadCollectionsFromServerIfNoFile()
}
NotificationCenter.default.addObserver(
self,

@ -118,14 +118,6 @@ public extension Date {
}
}
var nextDay: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: self)!
}
var weekDay: Int {
Calendar.current.component(.weekday, from: self)
}
func atBeginningOfDay(hourInt: Int = 9) -> Date {
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)!
}
@ -152,28 +144,6 @@ public extension Date {
return weekdays.map { $0.capitalized }
}()
static var weekdays: [String] = {
let calendar = Calendar.current
// let weekdays = calendar.shortWeekdaySymbols
// return weekdays.map { weekday in
// guard let firstLetter = weekday.first else { return "" }
// return String(firstLetter).capitalized
// }
// Adjusted for the different weekday starts
var weekdays = calendar.weekdaySymbols
if firstDayOfWeek > 1 {
for _ in 1..<firstDayOfWeek {
if let first = weekdays.first {
weekdays.append(first)
weekdays.removeFirst()
}
}
}
return weekdays.map { $0.capitalized }
}()
static var fullMonthNames: [String] = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
@ -194,11 +164,6 @@ public extension Date {
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)!
}
var endOfWeek: Date {
let lastDay = Calendar.current.dateInterval(of: .weekOfMonth, for: self)!.end
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)!
}
var startOfPreviousMonth: Date {
let dayInPreviousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self)!
return dayInPreviousMonth.startOfMonth
@ -311,18 +276,6 @@ public extension Date {
let calendar = Calendar.current
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
}
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .title:
self.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
self.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
self.formatted(date: .numeric, time: .omitted)
}
}
}
public extension Date {

@ -213,96 +213,16 @@ public extension String {
// MARK: - FFT Source Importing
public extension String {
enum RegexStatic {
// Patterns for France only
static let phoneNumber = /^(\+33|0033|33|0)[1-9][0-9]{8}$/
static let phoneNumberWithExtra0 = /^33[0][1-9][0-9]{8}$/
static let mobileNumber = /^(\+33|0033|33|0)[6-7][0-9]{8}$/
static let mobileNumberWithExtra0 = /^33[0][6-7][0-9]{8}$/
}
private func cleanedNumberForValidation() -> String {
// Keep leading '+' if present, remove all other non-digit characters
var cleaned = self.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned.hasPrefix("+") {
// Preserve '+' at start, remove all other non-digit characters
let digitsOnly = cleaned.dropFirst().components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
cleaned = "+" + digitsOnly
} else {
// Remove all non-digit characters
cleaned = cleaned.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
return cleaned
}
// MARK: - Phone Number Validation
/// Validate if the string is a mobile number for the specified locale.
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the mobile number pattern for the locale.
func isMobileNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.mobileNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.mobileNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/
}
/// Validate if the string is a phone number for the specified locale.
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the phone number pattern for the locale.
func isPhoneNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.phoneNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.phoneNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
}
func normalize(_ phone: String) -> String {
var normalized = phone.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove all non-digit characters
normalized = normalized.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
// Remove leading country code for France (33) if present
if normalized.hasPrefix("33") {
if normalized.dropFirst(2).hasPrefix("0") {
// Keep as is, don't strip the zero after 33
} else {
normalized = "0" + normalized.dropFirst(2)
}
} else if normalized.hasPrefix("0033") {
normalized = "0" + normalized.dropFirst(4)
}
return normalized
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
func isSamePhoneNumber(as other: String) -> Bool {
return normalize(self) == normalize(other)
func isPhoneNumber() -> Bool {
firstMatch(of: RegexStatic.phoneNumber) != nil
}
func cleanSearchText() -> String {
@ -392,4 +312,3 @@ public extension String {
return self // Return the original string if parsing fails
}
}

@ -30,7 +30,11 @@ import Combine
self.updateListenerTask = self.listenForTransactions()
Task {
await self.refreshPurchases()
do {
try await self.refreshPurchasedAppleProducts()
} catch {
Logger.error(error)
}
Logger.log("plan = \(String(describing: currentBestPurchase?.productId))")
}
@ -44,7 +48,7 @@ import Combine
}
@objc func collectionDidLoad(notification: Notification) {
if let _ = notification.object as? StoredCollection<Purchase> {
if let _ = notification.object as? BaseCollection<Purchase> {
self._updateBestPlan()
}
}
@ -62,46 +66,19 @@ import Combine
return productIds
}
public func refreshPurchases() async {
await _refreshUnfinishedTransactions()
await _refreshPurchasedAppleProducts()
}
fileprivate func _refreshPurchasedAppleProducts() async {
public func refreshPurchasedAppleProducts() async throws {
// Iterate through the user's purchased products.
for await verificationResult in Transaction.currentEntitlements {
do {
let transaction = try await self.processTransactionResult(verificationResult)
print("processs product id = \(transaction.productID)")
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
}
await transaction.finish()
} catch {
Logger.error(error)
}
}
}
public func _refreshUnfinishedTransactions() async {
// Iterate through the user's purchased products.
for await verificationResult in Transaction.unfinished {
do {
let transaction = try await self.processTransactionResult(verificationResult)
print("processs product id = \(transaction.productID)")
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
}
await transaction.finish()
} catch {
Logger.error(error)
}
}
}
func listenForTransactions() -> Task<Void, Never> {
return Task(priority: .background) {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
@ -242,7 +219,7 @@ import Combine
purchases.append(contentsOf: userPurchases)
let validPurchases = DataStore.shared.purchases.filter { $0.isValid() }
// Logger.log("valid purchases = \(validPurchases.count)")
Logger.log("valid purchases = \(validPurchases.count)")
purchases.append(contentsOf: validPurchases)
if let purchase = purchases.first(where: { $0.productId == StoreItem.monthlyUnlimited.rawValue }) {
@ -266,26 +243,7 @@ import Combine
// return units.reduce(0) { $0 + $1.purchasedQuantity }
}
struct CanCreateResponse: Decodable { var canCreate: Bool }
public func paymentForNewTournament() async -> TournamentPayment? {
if let payment = self.localPaymentForNewTournament() {
return payment
} else if let services = try? StoreCenter.main.service() {
do {
let response: CanCreateResponse = try await services.run(path: "is_granted_unlimited_access/", method: .get, requiresToken: true)
if response.canCreate {
return .unlimited
}
} catch {
Logger.error(error)
}
}
return nil
}
public func localPaymentForNewTournament() -> TournamentPayment? {
public func paymentForNewTournament() -> TournamentPayment? {
switch self.currentPlan {
case .monthlyUnlimited:

@ -71,8 +71,10 @@ public class StoreManager {
fileprivate func _productIdentifiers() -> [String] {
var items: [StoreItem] = []
switch Guard.main.currentPlan {
case .fivePerMonth:
items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited]
case .monthlyUnlimited:
items = [StoreItem.unit, StoreItem.unit10Pack]
break
default:
items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited]
}

@ -55,89 +55,6 @@ public enum ContactManagerError: LocalizedError {
}
}
public enum SummonType: Int, Identifiable {
case contact
case contactWithoutSignature
case summon
case summonWalkoutFollowUp
case summonErrorFollowUp
public func isRecall() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return false
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func mainWord() -> String {
switch self {
case .contact:
return "Contacter"
case .contactWithoutSignature:
return "Contacter"
case .summon:
return "Convoquer"
case .summonWalkoutFollowUp:
return "Reconvoquer"
case .summonErrorFollowUp:
return "Reconvoquer"
}
}
public func caption() -> String? {
switch self {
case .contact:
return nil
case .contactWithoutSignature:
return "Sans texte par défaut"
case .summon:
return nil
case .summonWalkoutFollowUp:
return "Suite à un forfait"
case .summonErrorFollowUp:
return "Suite à une erreur"
}
}
public func shouldConfirm() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return true
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func intro() -> String {
switch self {
case .contactWithoutSignature:
return ""
case .contact:
return "Vous êtes"
case .summon:
return "Vous êtes"
case .summonWalkoutFollowUp:
return "Suite à des forfaits, vous êtes finalement"
case .summonErrorFollowUp:
return "Suite à une erreur, vous êtes finalement"
}
}
public var id: Int {
self.rawValue
}
}
public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
@ -159,7 +76,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage
let date = startDate ?? tournament?.startDate ?? Date()
@ -180,10 +97,8 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
return text
}
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, summonType: SummonType = .summon) -> String {
if summonType == .contactWithoutSignature {
return ""
}
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String {
let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage
if useFullCustomMessage {
@ -192,7 +107,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
let clubName = tournament?.clubName ?? ""
let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)
@ -214,7 +129,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
[entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
}
let intro = summonType.intro()
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"

@ -35,23 +35,3 @@ public enum ExportFormat: Int, Identifiable, CaseIterable {
return Array(repeating: "\n", count: count).joined()
}
}
public enum ExportType: Int, Identifiable, CaseIterable {
public var id: Int { self.rawValue }
case sharing
case payment
public func localizedString() -> String {
switch self {
case .sharing:
return "inscriptions"
case .payment:
return "pointage"
}
}
public func newLineSeparator(_ count: Int = 1) -> String {
return Array(repeating: "\n", count: count).joined()
}
}

@ -10,11 +10,7 @@ import Foundation
public class PListReader {
static func dictionary(plist: String) -> [String: Any]? {
return self.dictionary(plist: plist, bundle: Bundle.main)
}
static func dictionary(plist: String, bundle: Bundle) -> [String: Any]? {
if let plistPath = bundle.path(forResource: plist, ofType: "plist") {
if let plistPath = Bundle.main.path(forResource: plist, ofType: "plist") {
// Read plist file into Data
if let plistData = FileManager.default.contents(atPath: plistPath) {
do {
@ -29,21 +25,21 @@ public class PListReader {
print("Failed to read plist file at path: \(plistPath)")
}
} else {
print("Plist file '\(plist)' not found in bundle")
print("Plist file 'Data.plist' not found in bundle")
}
return nil
}
public static func readString(plist: String, key: String, bundle: Bundle = Bundle.main) -> String? {
if let dictionary = self.dictionary(plist: plist, bundle: bundle) {
public static func readString(plist: String, key: String) -> String? {
if let dictionary = self.dictionary(plist: plist) {
return dictionary[key] as? String
}
return nil
}
public static func readBool(plist: String, key: String, bundle: Bundle = Bundle.main) -> Bool? {
if let dictionary = self.dictionary(plist: plist, bundle: bundle) {
public static func readBool(plist: String, key: String) -> Bool? {
if let dictionary = self.dictionary(plist: plist) {
return dictionary[key] as? Bool
}
return nil

@ -25,7 +25,7 @@ enum RankSource: Hashable {
}
}
public protocol TournamentBuildHolder: Identifiable, Hashable, Equatable {
public protocol TournamentBuildHolder: Identifiable {
var id: String { get }
var category: TournamentCategory { get }
var level: TournamentLevel { get }
@ -349,9 +349,7 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
return 4
} else {
switch level {
case .p25:
return 4
case .p100, .p250:
case .p25, .p100, .p250:
if category == .women {
return 4
}
@ -552,7 +550,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
}
case .p1500, .p2000:
if roundIndex <= 3 { //demi / finale / quart / 8eme
return .twoSets
return .twoSetsDecisivePoint
} else {
return .twoSetsSuperTie
}
@ -570,7 +568,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
}
}
public func federalFormatForLoserBracketRound() -> MatchFormat {
public func federalFormatForLoserBracketRound(_ roundIndex: Int) -> MatchFormat {
switch self {
case .p25:
return .superTie
@ -712,7 +710,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
case 13...16:
return [175,150,138,113,100,88,75,63,53,45,38,25,13,8,3]
case 17...20:
return [188,163,150,138,125,113,100,88,75,63,58,50,45,38,30,25,13,8,3]
return [188,163,150,138,123,113,100,88,75,63,58,50,45,38,30,25,13,8,3]
case 21...24:
return [188,175,163,150,138,125,118,108,100,93,83,75,70,63,58,50,45,38,30,25,13,8,3]
case 25...28:
@ -1201,7 +1199,6 @@ public enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
public enum SetFormat: Int, Hashable, Codable {
case nine
case four
case three
case six
case superTieBreak
case megaTieBreak
@ -1228,10 +1225,6 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOne == 5 || teamTwo == 5 {
return true
}
case .three:
if teamOne == 4 || teamTwo == 4 {
return true
}
case .six:
if teamOne == 7 || teamTwo == 7 {
return true
@ -1250,8 +1243,6 @@ public enum SetFormat: Int, Hashable, Codable {
return 8
case .four:
return 4
case .three:
return 3
case .six:
return 6
case .superTieBreak, .megaTieBreak:
@ -1269,10 +1260,6 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOneScore == 4 {
return []
}
case .three:
if teamOneScore == 3 {
return []
}
case .six:
if teamOneScore == 6 {
return []
@ -1295,8 +1282,6 @@ public enum SetFormat: Int, Hashable, Codable {
return [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
case .four:
return [5, 4, 3, 2, 1, 0]
case .three:
return [4, 3, 2, 1, 0]
case .six:
return [7, 6, 5, 4, 3, 2, 1, 0]
case .superTieBreak:
@ -1312,8 +1297,6 @@ public enum SetFormat: Int, Hashable, Codable {
return 9
case .four:
return 4
case .three:
return 4
case .six:
return 6
case .superTieBreak:
@ -1381,15 +1364,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 2
case .nineGames, .nineGamesDecisivePoint:
return 3
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4
case .superTie:
return 5
return 4
case .megaTie:
return 6
return 5
case .twoSetsOfSuperTie:
return 7
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
}
}
@ -1412,15 +1395,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 3
case .nineGamesDecisivePoint:
return 3
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4
case .superTie:
return 5
return 4
case .megaTie:
return 6
return 5
case .twoSetsOfSuperTie:
return 7
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
}
}
@ -1658,7 +1641,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .megaTie:
return "supertie de 15 points"
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return "1 set de 4 jeux, tiebreak à 3/3"
return "1 set de 4 jeux, tiebreak à 4/4"
}
}
@ -1687,10 +1670,8 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint:
return .six
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint:
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .four
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .three
case .nineGames, .nineGamesDecisivePoint:
return .nine
case .superTie, .twoSetsOfSuperTie:
@ -1869,16 +1850,6 @@ public enum RoundRule {
}
}
public static func cumulatedNumberOfMatches(forTeams teams: Int) -> Int {
var i = teams / 2
var loserTeams = teams / 2
while loserTeams > 1 {
i += Self.cumulatedNumberOfMatches(forTeams: loserTeams)
loserTeams = loserTeams / 2
}
return i
}
public static func teamsInFirstRound(forTeams teams: Int) -> Int {
Int(pow(2.0, ceil(log2(Double(teams)))))
}
@ -1909,16 +1880,12 @@ public enum RoundRule {
}
public static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int {
(1 << roundIndex)
}
public static func baseIndex(forRoundIndex roundIndex: Int) -> Int {
numberOfMatches(forRoundIndex: roundIndex) - 1
Int(pow(2.0, Double(roundIndex)))
}
static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int {
let roundIndex = roundIndex(fromMatchIndex: matchIndex)
let matchIndexWithinRound = matchIndex - baseIndex(forRoundIndex: roundIndex)
let matchIndexWithinRound = matchIndex - (Int(pow(2.0, Double(roundIndex))) - 1)
return matchIndexWithinRound
}
@ -1942,7 +1909,7 @@ public enum RoundRule {
}
return "Quart de finale"
default:
return "\((1 << roundIndex))ème"
return "\(Int(pow(2.0, Double(roundIndex))))ème"
}
}
}

@ -1,34 +0,0 @@
//
// Config.swift
// PadelClubData
//
// Created by Laurent Morvillier on 21/05/2025.
//
@testable import PadelClubData
class Config {
var secure: Bool
var domain: String
init(secure: Bool, domain: String) {
self.secure = secure
self.domain = domain
}
static var server: Config {
let bundle = Bundle(for: self)
let secure = PListReader.readBool(plist: "config", key: "secure_server", bundle: bundle)
let domain = PListReader.readString(plist: "config", key: "server_domain", bundle: bundle)
if let secure, let domain {
return Config(secure: secure, domain: domain)
}
fatalError("no server configuration")
}
}

@ -18,14 +18,11 @@ struct DeletionTests {
init() async throws {
let conf = Config.server
FileManager.default.deleteDirectoryInDocuments(directoryName: "storage")
FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2")
self.secondStoreCenter = StoreCenter(directoryName: "storage-2")
self.secondStoreCenter.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true)
self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true)
self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json")
self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json")
try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString)
@ -37,7 +34,7 @@ struct DeletionTests {
try await self.login(storeCenter: self.secondStoreCenter, username: self.username1, password: self.password1)
}
StoreCenter.main.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true)
StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true)
StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json")
StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json")
try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString)
@ -250,20 +247,12 @@ struct DeletionTests {
#expect(clubColB.count == 1)
#expect(eventColB.count == 0)
#expect(tournamentColB.count == 0)
do {
let _ = try self.secondStoreCenter.store(identifier: tournament.id)
Issue.record("should go in the catch because the store has been destroyed")
} catch {
#expect(1 == 1)
}
// #expect(groupStageColB.count == 0)
// #expect(roundColB.count == 0)
// #expect(matchColB.count == 0)
// #expect(teamRegistrationColB.count == 0)
// #expect(teamScoreColB.count == 0)
// #expect(playerRegistrationColB.count == 0)
#expect(groupStageColB.count == 0)
#expect(roundColB.count == 0)
#expect(matchColB.count == 0)
#expect(teamRegistrationColB.count == 0)
#expect(teamScoreColB.count == 0)
#expect(playerRegistrationColB.count == 0)
}
}
@ -290,10 +279,10 @@ actor BoolChecker {
return
}
// print("sleep...")
print("sleep...")
// Wait for 100ms before next check
try? await Task.sleep(for: .milliseconds(1000))
try? await Task.sleep(for: .milliseconds(100))
}
// Throw error if timeout is reached

@ -1,101 +0,0 @@
//
// PadelClubDataTests.swift
// PadelClubDataTests
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Testing
@testable import PadelClubData
@testable import LeStorage
enum TestError: Error {
case notAuthenticated
case sameDeviceId
case missingEvent
case missingTournament
}
struct PadelClubDataTests {
let username: String = "UserDataTests"
let password: String = "MyPass1234--"
init() async throws {
let conf = Config.server
StoreCenter.main.configureURLs(secureScheme: conf.secure, domain: conf.domain)
StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "token.json")
try await self.login()
}
mutating func login() async throws {
let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password)
}
@Test func testAuthentication() {
#expect(StoreCenter.main.isAuthenticated)
}
@Test func createTournament() async throws {
guard let userId = StoreCenter.main.userId else {
throw TestError.notAuthenticated
}
// Cleanup
let eventCol: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
// let events: [Event] = try await StoreCenter.main.service().get()
try await eventCol.deleteAsync(contentOfs: Array(eventCol))
#expect(eventCol.count == 0)
let tournamentCol: SyncedCollection<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
// let tournaments: [Tournament] = try await StoreCenter.main.service().get()
try await tournamentCol.deleteAsync(contentOfs: Array(tournamentCol))
#expect(tournamentCol.count == 0)
// Create
let event: Event = Event(creator: userId, club: nil, name: "test_createTournament")
try await eventCol.addOrUpdateAsync(instance: event)
let tournament: Tournament = Tournament.fake()
tournament.event = event.id
try await tournamentCol.addOrUpdateAsync(instance: tournament)
// Test server content
try await eventCol.loadOnceAsync()
#expect(eventCol.count == 1)
try await tournamentCol.loadOnceAsync()
#expect(tournamentCol.count == 1)
}
@Test func dualStoreCenter() async throws {
let conf = Config.server
let secondStoreServer = StoreCenter()
secondStoreServer.configureURLs(secureScheme: conf.secure, domain: conf.domain)
secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json")
let _: CustomUser = try await secondStoreServer.service().login(username: self.username, password: self.password)
#expect(StoreCenter.main.isAuthenticated)
#expect(secondStoreServer.isAuthenticated)
}
@Test func testWebsocketSynchronization() async throws {
let secondStoreServer = StoreCenter()
secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000")
secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json")
let events = DataStore.shared.events
try await DataStore.shared.events.deleteAsync(contentOfs: Array(events))
}
}

File diff suppressed because it is too large Load Diff

@ -23,13 +23,11 @@ struct SynchronizationTests {
init() async throws {
let conf = Config.server
FileManager.default.deleteDirectoryInDocuments(directoryName: "storage")
FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2")
self.secondStoreCenter = StoreCenter(directoryName: "storage-2")
self.secondStoreCenter.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true)
self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true)
self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json")
self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json")
try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString)
@ -41,7 +39,7 @@ struct SynchronizationTests {
try await self.login(storeCenter: self.secondStoreCenter)
}
StoreCenter.main.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true)
StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true)
StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json")
StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json")
try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString)
@ -75,14 +73,13 @@ struct SynchronizationTests {
// Cleanup
let eventCollection1: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await eventCollection1.loadOnceAsync()
#expect(eventCollection1.hasLoaded == true)
try await eventCollection1.deleteAsync(contentOfs: Array(eventCollection1))
let eventCollection2: SyncedCollection<Event> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
#expect(eventCollection2.hasLoaded == true)
eventCollection2.reset()
eventCollection2.clear()
// cleanup sync residues
let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync()
@ -123,13 +120,13 @@ struct SynchronizationTests {
let eventCollectionA: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA))
let eventCollectionB: SyncedCollection<Event> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
eventCollectionB.reset()
eventCollectionB.clear()
// Setup clubs collections
let clubCollectionA: SyncedCollection<Club> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await clubCollectionA.deleteAsync(contentOfs: Array(clubCollectionA))
let clubCollectionB: SyncedCollection<Club> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
clubCollectionB.reset()
clubCollectionB.clear()
// cleanup sync residues
let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync()
@ -187,7 +184,7 @@ struct SynchronizationTests {
let teamRegColA: SyncedCollection<TeamRegistration> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await teamRegColA.deleteAsync(contentOfs: Array(teamRegColA))
let teamRegColB: SyncedCollection<TeamRegistration> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
teamRegColB.reset()
teamRegColB.clear()
// cleanup sync residues
let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync()
@ -215,7 +212,7 @@ struct SynchronizationTests {
try await eventCollectionA.deleteAsync(contentOfs: Array(eventCollectionA))
let eventCollectionB: SyncedCollection<Event> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
eventCollectionB.reset()
eventCollectionB.clear()
// cleanup sync residues
let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync()
@ -311,61 +308,5 @@ struct SynchronizationTests {
}
// needs to run on a postgreSQL, otherwise fails because of sqlite database locks
@Test func testBuildEverything() async throws {
// Cleanup
let tournamentColA: SyncedCollection<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let tournamentColB: SyncedCollection<Tournament> = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection()
let tournamentsToDelete: [Tournament] = try await StoreCenter.main.service().get()
tournamentColA.delete(contentOfs: tournamentsToDelete)
// Setup tournament + build everything
let tournament = Tournament()
try await tournamentColA.addOrUpdateAsync(instance: tournament)
try await tournament.deleteAndBuildEverythingAsync()
let tourStore = try StoreCenter.main.store(identifier: tournament.id)
let gsColA: SyncedCollection<GroupStage> = try tourStore.syncedCollection()
let roundColA: SyncedCollection<Round> = try tourStore.syncedCollection()
let matchesColA: SyncedCollection<Match> = try tourStore.syncedCollection()
#expect(gsColA.count == 4)
#expect(roundColA.count == 15)
#expect(matchesColA.count == 56)
// Sync with 2nd store
try await secondStoreCenter.testSynchronizeOnceAsync()
#expect(tournamentColB.count == 1)
let tourStoreB = try secondStoreCenter.store(identifier: tournament.id)
let gsColB: SyncedCollection<GroupStage> = try tourStoreB.syncedCollection()
let roundColB: SyncedCollection<Round> = try tourStoreB.syncedCollection()
let matchesColB: SyncedCollection<Match> = try tourStoreB.syncedCollection()
#expect(gsColB.count == 4)
#expect(roundColB.count == 15)
#expect(matchesColB.count == 56)
// change setup + build everything
tournament.groupStageCount = 2
tournament.teamCount = 20
try await tournamentColA.addOrUpdateAsync(instance: tournament)
try await tournament.deleteAndBuildEverythingAsync()
#expect(gsColA.count == 2)
#expect(roundColA.count == 15)
#expect(matchesColA.count == 44)
// Sync with 2nd store
try await secondStoreCenter.testSynchronizeOnceAsync()
#expect(gsColB.count == 2)
#expect(roundColB.count == 15)
#expect(matchesColB.count == 44)
}
}

Loading…
Cancel
Save