diff --git a/swift/Package.swift b/swift/Package.swift index d34ae2c0d..250c34132 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -18,6 +18,20 @@ let package = Package( .swiftLanguageMode(.v6), ] ), + // Models a customer: depends on the `Basecamp` target and imports it + // plainly, so only the public surface is visible (#735). Deliberately a + // non-test target — `swift build` compiles it, which is broader cover + // than `swift test`, and it is deliberately absent from `products` so it + // is never built by a package that depends on this one. Its sources must + // never use `@testable`; PublicInitCoverageTests enforces both properties. + .target( + name: "BasecampPublicAPIConsumer", + dependencies: ["Basecamp"], + path: "Sources/BasecampPublicAPIConsumer", + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), .executableTarget( name: "BasecampGenerator", path: "Sources/BasecampGenerator", diff --git a/swift/Sources/Basecamp/Generated/Models/AccountLimits.swift b/swift/Sources/Basecamp/Generated/Models/AccountLimits.swift index f272d690d..7308a2734 100644 --- a/swift/Sources/Basecamp/Generated/Models/AccountLimits.swift +++ b/swift/Sources/Basecamp/Generated/Models/AccountLimits.swift @@ -6,4 +6,16 @@ public struct AccountLimits: Codable, Sendable { public var canCreateUsers: Bool? public var canPinProjects: Bool? public var canUploadFiles: Bool? + + public init( + canCreateProjects: Bool? = nil, + canCreateUsers: Bool? = nil, + canPinProjects: Bool? = nil, + canUploadFiles: Bool? = nil + ) { + self.canCreateProjects = canCreateProjects + self.canCreateUsers = canCreateUsers + self.canPinProjects = canPinProjects + self.canUploadFiles = canUploadFiles + } } diff --git a/swift/Sources/Basecamp/Generated/Models/AccountLogo.swift b/swift/Sources/Basecamp/Generated/Models/AccountLogo.swift index 464733975..2ffedbf8d 100644 --- a/swift/Sources/Basecamp/Generated/Models/AccountLogo.swift +++ b/swift/Sources/Basecamp/Generated/Models/AccountLogo.swift @@ -3,4 +3,8 @@ import Foundation public struct AccountLogo: Codable, Sendable { public var url: String? + + public init(url: String? = nil) { + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/AccountSettings.swift b/swift/Sources/Basecamp/Generated/Models/AccountSettings.swift index 38feebb52..b151dc696 100644 --- a/swift/Sources/Basecamp/Generated/Models/AccountSettings.swift +++ b/swift/Sources/Basecamp/Generated/Models/AccountSettings.swift @@ -5,4 +5,10 @@ public struct AccountSettings: Codable, Sendable { public var companyHqEnabled: Bool? public var projectsEnabled: Bool? public var teamsEnabled: Bool? + + public init(companyHqEnabled: Bool? = nil, projectsEnabled: Bool? = nil, teamsEnabled: Bool? = nil) { + self.companyHqEnabled = companyHqEnabled + self.projectsEnabled = projectsEnabled + self.teamsEnabled = teamsEnabled + } } diff --git a/swift/Sources/Basecamp/Generated/Models/AccountSubscription.swift b/swift/Sources/Basecamp/Generated/Models/AccountSubscription.swift index 7a162e690..2a6601450 100644 --- a/swift/Sources/Basecamp/Generated/Models/AccountSubscription.swift +++ b/swift/Sources/Basecamp/Generated/Models/AccountSubscription.swift @@ -10,4 +10,24 @@ public struct AccountSubscription: Codable, Sendable { public var teams: Bool? public var templates: Bool? public var timesheet: Bool? + + public init( + clients: Bool? = nil, + logo: Bool? = nil, + projectLimit: Int32? = nil, + properName: String? = nil, + shortName: String? = nil, + teams: Bool? = nil, + templates: Bool? = nil, + timesheet: Bool? = nil + ) { + self.clients = clients + self.logo = logo + self.projectLimit = projectLimit + self.properName = properName + self.shortName = shortName + self.teams = teams + self.templates = templates + self.timesheet = timesheet + } } diff --git a/swift/Sources/Basecamp/Generated/Models/CampfireLineAttachment.swift b/swift/Sources/Basecamp/Generated/Models/CampfireLineAttachment.swift index 3019df3f2..fb8dd21e4 100644 --- a/swift/Sources/Basecamp/Generated/Models/CampfireLineAttachment.swift +++ b/swift/Sources/Basecamp/Generated/Models/CampfireLineAttachment.swift @@ -8,4 +8,20 @@ public struct CampfireLineAttachment: Codable, Sendable { public var filename: String? public var title: String? public var url: String? + + public init( + byteSize: Int? = nil, + contentType: String? = nil, + downloadUrl: String? = nil, + filename: String? = nil, + title: String? = nil, + url: String? = nil + ) { + self.byteSize = byteSize + self.contentType = contentType + self.downloadUrl = downloadUrl + self.filename = filename + self.title = title + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/ClientApprovalResponse.swift b/swift/Sources/Basecamp/Generated/Models/ClientApprovalResponse.swift index aca4ffd24..59f9d1f71 100644 --- a/swift/Sources/Basecamp/Generated/Models/ClientApprovalResponse.swift +++ b/swift/Sources/Basecamp/Generated/Models/ClientApprovalResponse.swift @@ -17,4 +17,38 @@ public struct ClientApprovalResponse: Codable, Sendable { public var type: String? public var updatedAt: String? public var visibleToClients: Bool? + + public init( + appUrl: String? = nil, + approved: Bool? = nil, + bookmarkUrl: String? = nil, + bucket: RecordingBucket? = nil, + content: String? = nil, + createdAt: String? = nil, + creator: Person? = nil, + id: Int? = nil, + inheritsStatus: Bool? = nil, + parent: RecordingParent? = nil, + status: String? = nil, + title: String? = nil, + type: String? = nil, + updatedAt: String? = nil, + visibleToClients: Bool? = nil + ) { + self.appUrl = appUrl + self.approved = approved + self.bookmarkUrl = bookmarkUrl + self.bucket = bucket + self.content = content + self.createdAt = createdAt + self.creator = creator + self.id = id + self.inheritsStatus = inheritsStatus + self.parent = parent + self.status = status + self.title = title + self.type = type + self.updatedAt = updatedAt + self.visibleToClients = visibleToClients + } } diff --git a/swift/Sources/Basecamp/Generated/Models/ClientSide.swift b/swift/Sources/Basecamp/Generated/Models/ClientSide.swift index 51b16846b..b63254132 100644 --- a/swift/Sources/Basecamp/Generated/Models/ClientSide.swift +++ b/swift/Sources/Basecamp/Generated/Models/ClientSide.swift @@ -5,4 +5,9 @@ import Foundation public struct ClientSide: Codable, Sendable { public var appUrl: String? public var url: String? + + public init(appUrl: String? = nil, url: String? = nil) { + self.appUrl = appUrl + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/CreateAttachmentResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/CreateAttachmentResponseContent.swift index 2d1961ce1..d23fd6643 100644 --- a/swift/Sources/Basecamp/Generated/Models/CreateAttachmentResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/CreateAttachmentResponseContent.swift @@ -3,4 +3,8 @@ import Foundation public struct CreateAttachmentResponseContent: Codable, Sendable { public var attachableSgid: String? + + public init(attachableSgid: String? = nil) { + self.attachableSgid = attachableSgid + } } diff --git a/swift/Sources/Basecamp/Generated/Models/DoorService.swift b/swift/Sources/Basecamp/Generated/Models/DoorService.swift index 39379de58..31da97ece 100644 --- a/swift/Sources/Basecamp/Generated/Models/DoorService.swift +++ b/swift/Sources/Basecamp/Generated/Models/DoorService.swift @@ -7,4 +7,18 @@ public struct DoorService: Codable, Sendable { public var name: String? public var supportingText: String? public var validPatterns: [String]? + + public init( + code: String? = nil, + exampleUrl: String? = nil, + name: String? = nil, + supportingText: String? = nil, + validPatterns: [String]? = nil + ) { + self.code = code + self.exampleUrl = exampleUrl + self.name = name + self.supportingText = supportingText + self.validPatterns = validPatterns + } } diff --git a/swift/Sources/Basecamp/Generated/Models/EventDetails.swift b/swift/Sources/Basecamp/Generated/Models/EventDetails.swift index 32569e4c5..4e55e6b58 100644 --- a/swift/Sources/Basecamp/Generated/Models/EventDetails.swift +++ b/swift/Sources/Basecamp/Generated/Models/EventDetails.swift @@ -5,4 +5,10 @@ public struct EventDetails: Codable, Sendable { public var addedPersonIds: [Int]? public var notifiedRecipientIds: [Int]? public var removedPersonIds: [Int]? + + public init(addedPersonIds: [Int]? = nil, notifiedRecipientIds: [Int]? = nil, removedPersonIds: [Int]? = nil) { + self.addedPersonIds = addedPersonIds + self.notifiedRecipientIds = notifiedRecipientIds + self.removedPersonIds = removedPersonIds + } } diff --git a/swift/Sources/Basecamp/Generated/Models/EverythingFile.swift b/swift/Sources/Basecamp/Generated/Models/EverythingFile.swift index ba824a7ef..dc3e5d291 100644 --- a/swift/Sources/Basecamp/Generated/Models/EverythingFile.swift +++ b/swift/Sources/Basecamp/Generated/Models/EverythingFile.swift @@ -34,4 +34,72 @@ public struct EverythingFile: Codable, Sendable { public var url: String? public var visibleToClients: Bool? public var width: Int32? + + public init( + appDownloadUrl: String? = nil, + appUrl: String? = nil, + attachableSgid: String? = nil, + bookmarkUrl: String? = nil, + boostsCount: Int32? = nil, + boostsUrl: String? = nil, + bucket: RecordingBucket? = nil, + byteSize: Int? = nil, + commentsCount: Int32? = nil, + commentsUrl: String? = nil, + content: String? = nil, + contentAttachments: [RichTextAttachment]? = nil, + contentType: String? = nil, + createdAt: String? = nil, + creator: Person? = nil, + description: String? = nil, + descriptionAttachments: [RichTextAttachment]? = nil, + downloadUrl: String? = nil, + filename: String? = nil, + height: Int32? = nil, + id: Int? = nil, + inheritsStatus: Bool? = nil, + parent: RecordingParent? = nil, + position: Int32? = nil, + status: String? = nil, + subscriptionUrl: String? = nil, + title: String? = nil, + type: String? = nil, + updatedAt: String? = nil, + url: String? = nil, + visibleToClients: Bool? = nil, + width: Int32? = nil + ) { + self.appDownloadUrl = appDownloadUrl + self.appUrl = appUrl + self.attachableSgid = attachableSgid + self.bookmarkUrl = bookmarkUrl + self.boostsCount = boostsCount + self.boostsUrl = boostsUrl + self.bucket = bucket + self.byteSize = byteSize + self.commentsCount = commentsCount + self.commentsUrl = commentsUrl + self.content = content + self.contentAttachments = contentAttachments + self.contentType = contentType + self.createdAt = createdAt + self.creator = creator + self.description = description + self.descriptionAttachments = descriptionAttachments + self.downloadUrl = downloadUrl + self.filename = filename + self.height = height + self.id = id + self.inheritsStatus = inheritsStatus + self.parent = parent + self.position = position + self.status = status + self.subscriptionUrl = subscriptionUrl + self.title = title + self.type = type + self.updatedAt = updatedAt + self.url = url + self.visibleToClients = visibleToClients + self.width = width + } } diff --git a/swift/Sources/Basecamp/Generated/Models/GaugeNeedleUpdatePayload.swift b/swift/Sources/Basecamp/Generated/Models/GaugeNeedleUpdatePayload.swift index fdaa765aa..7897fb764 100644 --- a/swift/Sources/Basecamp/Generated/Models/GaugeNeedleUpdatePayload.swift +++ b/swift/Sources/Basecamp/Generated/Models/GaugeNeedleUpdatePayload.swift @@ -3,4 +3,8 @@ import Foundation public struct GaugeNeedleUpdatePayload: Codable, Sendable { public var description: String? + + public init(description: String? = nil) { + self.description = description + } } diff --git a/swift/Sources/Basecamp/Generated/Models/GetAssignedTodosResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/GetAssignedTodosResponseContent.swift index 1d11af31a..202e095cc 100644 --- a/swift/Sources/Basecamp/Generated/Models/GetAssignedTodosResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/GetAssignedTodosResponseContent.swift @@ -5,4 +5,10 @@ public struct GetAssignedTodosResponseContent: Codable, Sendable { public var groupedBy: String? public var person: Person? public var todos: [Todo]? + + public init(groupedBy: String? = nil, person: Person? = nil, todos: [Todo]? = nil) { + self.groupedBy = groupedBy + self.person = person + self.todos = todos + } } diff --git a/swift/Sources/Basecamp/Generated/Models/GetMyAssignmentsResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/GetMyAssignmentsResponseContent.swift index 113514e2d..8b8e7d1af 100644 --- a/swift/Sources/Basecamp/Generated/Models/GetMyAssignmentsResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/GetMyAssignmentsResponseContent.swift @@ -4,4 +4,9 @@ import Foundation public struct GetMyAssignmentsResponseContent: Codable, Sendable { public var nonPriorities: [MyAssignment]? public var priorities: [MyAssignment]? + + public init(nonPriorities: [MyAssignment]? = nil, priorities: [MyAssignment]? = nil) { + self.nonPriorities = nonPriorities + self.priorities = priorities + } } diff --git a/swift/Sources/Basecamp/Generated/Models/GetOverdueTodosResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/GetOverdueTodosResponseContent.swift index 4776ab1b0..7f06faedf 100644 --- a/swift/Sources/Basecamp/Generated/Models/GetOverdueTodosResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/GetOverdueTodosResponseContent.swift @@ -6,4 +6,16 @@ public struct GetOverdueTodosResponseContent: Codable, Sendable { public var overAWeekLate: [Todo]? public var overThreeMonthsLate: [Todo]? public var underAWeekLate: [Todo]? + + public init( + overAMonthLate: [Todo]? = nil, + overAWeekLate: [Todo]? = nil, + overThreeMonthsLate: [Todo]? = nil, + underAWeekLate: [Todo]? = nil + ) { + self.overAMonthLate = overAMonthLate + self.overAWeekLate = overAWeekLate + self.overThreeMonthsLate = overThreeMonthsLate + self.underAWeekLate = underAWeekLate + } } diff --git a/swift/Sources/Basecamp/Generated/Models/GetPersonProgressResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/GetPersonProgressResponseContent.swift index 1d51857f5..7ebc5ad80 100644 --- a/swift/Sources/Basecamp/Generated/Models/GetPersonProgressResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/GetPersonProgressResponseContent.swift @@ -4,4 +4,9 @@ import Foundation public struct GetPersonProgressResponseContent: Codable, Sendable { public var events: [TimelineEvent]? public var person: Person? + + public init(events: [TimelineEvent]? = nil, person: Person? = nil) { + self.events = events + self.person = person + } } diff --git a/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift b/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift index fcc738d8e..296e1fa58 100644 --- a/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift +++ b/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift @@ -8,4 +8,20 @@ public struct OutOfOffice: Codable, Sendable { public var ongoing: Bool? public var person: OutOfOfficePerson? public var startDate: String? + + public init( + backOnDate: String? = nil, + enabled: Bool? = nil, + endDate: String? = nil, + ongoing: Bool? = nil, + person: OutOfOfficePerson? = nil, + startDate: String? = nil + ) { + self.backOnDate = backOnDate + self.enabled = enabled + self.endDate = endDate + self.ongoing = ongoing + self.person = person + self.startDate = startDate + } } diff --git a/swift/Sources/Basecamp/Generated/Models/PauseQuestionResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/PauseQuestionResponseContent.swift index d80dab5a1..b8771d570 100644 --- a/swift/Sources/Basecamp/Generated/Models/PauseQuestionResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/PauseQuestionResponseContent.swift @@ -3,4 +3,8 @@ import Foundation public struct PauseQuestionResponseContent: Codable, Sendable { public var paused: Bool? + + public init(paused: Bool? = nil) { + self.paused = paused + } } diff --git a/swift/Sources/Basecamp/Generated/Models/Preferences.swift b/swift/Sources/Basecamp/Generated/Models/Preferences.swift index 382afdb3f..f81c8fd6e 100644 --- a/swift/Sources/Basecamp/Generated/Models/Preferences.swift +++ b/swift/Sources/Basecamp/Generated/Models/Preferences.swift @@ -7,4 +7,18 @@ public struct Preferences: Codable, Sendable { public var timeFormat: String? public var timeZoneName: String? public var url: String? + + public init( + appUrl: String? = nil, + firstWeekDay: String? = nil, + timeFormat: String? = nil, + timeZoneName: String? = nil, + url: String? = nil + ) { + self.appUrl = appUrl + self.firstWeekDay = firstWeekDay + self.timeFormat = timeFormat + self.timeZoneName = timeZoneName + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/PreferencesPayload.swift b/swift/Sources/Basecamp/Generated/Models/PreferencesPayload.swift index 90767c68d..09dc79e59 100644 --- a/swift/Sources/Basecamp/Generated/Models/PreferencesPayload.swift +++ b/swift/Sources/Basecamp/Generated/Models/PreferencesPayload.swift @@ -5,4 +5,10 @@ public struct PreferencesPayload: Codable, Sendable { public var firstWeekDay: String? public var timeFormat: String? public var timeZoneName: String? + + public init(firstWeekDay: String? = nil, timeFormat: String? = nil, timeZoneName: String? = nil) { + self.firstWeekDay = firstWeekDay + self.timeFormat = timeFormat + self.timeZoneName = timeZoneName + } } diff --git a/swift/Sources/Basecamp/Generated/Models/PreviewableAttachment.swift b/swift/Sources/Basecamp/Generated/Models/PreviewableAttachment.swift index d773f27b8..739866df5 100644 --- a/swift/Sources/Basecamp/Generated/Models/PreviewableAttachment.swift +++ b/swift/Sources/Basecamp/Generated/Models/PreviewableAttachment.swift @@ -10,4 +10,24 @@ public struct PreviewableAttachment: Codable, Sendable { public var id: Int? public var url: String? public var width: Int32? + + public init( + appUrl: String? = nil, + contentType: String? = nil, + filename: String? = nil, + filesize: Int? = nil, + height: Int32? = nil, + id: Int? = nil, + url: String? = nil, + width: Int32? = nil + ) { + self.appUrl = appUrl + self.contentType = contentType + self.filename = filename + self.filesize = filesize + self.height = height + self.id = id + self.url = url + self.width = width + } } diff --git a/swift/Sources/Basecamp/Generated/Models/ProjectAccessResult.swift b/swift/Sources/Basecamp/Generated/Models/ProjectAccessResult.swift index f1ac0f564..186b194c6 100644 --- a/swift/Sources/Basecamp/Generated/Models/ProjectAccessResult.swift +++ b/swift/Sources/Basecamp/Generated/Models/ProjectAccessResult.swift @@ -4,4 +4,9 @@ import Foundation public struct ProjectAccessResult: Codable, Sendable { public var granted: [Person]? public var revoked: [Person]? + + public init(granted: [Person]? = nil, revoked: [Person]? = nil) { + self.granted = granted + self.revoked = revoked + } } diff --git a/swift/Sources/Basecamp/Generated/Models/QuestionReminder.swift b/swift/Sources/Basecamp/Generated/Models/QuestionReminder.swift index 1133771e4..d0207f38a 100644 --- a/swift/Sources/Basecamp/Generated/Models/QuestionReminder.swift +++ b/swift/Sources/Basecamp/Generated/Models/QuestionReminder.swift @@ -6,4 +6,16 @@ public struct QuestionReminder: Codable, Sendable { public var question: Question? public var remindAt: String? public var reminderId: Int? + + public init( + groupOn: String? = nil, + question: Question? = nil, + remindAt: String? = nil, + reminderId: Int? = nil + ) { + self.groupOn = groupOn + self.question = question + self.remindAt = remindAt + self.reminderId = reminderId + } } diff --git a/swift/Sources/Basecamp/Generated/Models/QuestionSchedule.swift b/swift/Sources/Basecamp/Generated/Models/QuestionSchedule.swift index 35a67cccb..2a05e6be1 100644 --- a/swift/Sources/Basecamp/Generated/Models/QuestionSchedule.swift +++ b/swift/Sources/Basecamp/Generated/Models/QuestionSchedule.swift @@ -11,4 +11,26 @@ public struct QuestionSchedule: Codable, Sendable { public var startDate: String? public var weekInstance: Int32? public var weekInterval: Int32? + + public init( + days: [Int32]? = nil, + endDate: String? = nil, + frequency: String? = nil, + hour: Int32? = nil, + minute: Int32? = nil, + monthInterval: Int32? = nil, + startDate: String? = nil, + weekInstance: Int32? = nil, + weekInterval: Int32? = nil + ) { + self.days = days + self.endDate = endDate + self.frequency = frequency + self.hour = hour + self.minute = minute + self.monthInterval = monthInterval + self.startDate = startDate + self.weekInstance = weekInstance + self.weekInterval = weekInterval + } } diff --git a/swift/Sources/Basecamp/Generated/Models/ResumeQuestionResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/ResumeQuestionResponseContent.swift index c0b01b850..76296cddc 100644 --- a/swift/Sources/Basecamp/Generated/Models/ResumeQuestionResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/ResumeQuestionResponseContent.swift @@ -3,4 +3,8 @@ import Foundation public struct ResumeQuestionResponseContent: Codable, Sendable { public var paused: Bool? + + public init(paused: Bool? = nil) { + self.paused = paused + } } diff --git a/swift/Sources/Basecamp/Generated/Models/ScheduleAttributes.swift b/swift/Sources/Basecamp/Generated/Models/ScheduleAttributes.swift index ce678f030..05537243b 100644 --- a/swift/Sources/Basecamp/Generated/Models/ScheduleAttributes.swift +++ b/swift/Sources/Basecamp/Generated/Models/ScheduleAttributes.swift @@ -4,4 +4,9 @@ import Foundation public struct ScheduleAttributes: Codable, Sendable { public var endDate: String? public var startDate: String? + + public init(endDate: String? = nil, startDate: String? = nil) { + self.endDate = endDate + self.startDate = startDate + } } diff --git a/swift/Sources/Basecamp/Generated/Models/TimelineAttachment.swift b/swift/Sources/Basecamp/Generated/Models/TimelineAttachment.swift index f4824f52c..9191ac709 100644 --- a/swift/Sources/Basecamp/Generated/Models/TimelineAttachment.swift +++ b/swift/Sources/Basecamp/Generated/Models/TimelineAttachment.swift @@ -39,4 +39,82 @@ public struct TimelineAttachment: Codable, Sendable { public var url: String? public var visibleToClients: Bool? public var width: Int32? + + public init( + appDownloadUrl: String? = nil, + appUrl: String? = nil, + attachableSgid: String? = nil, + bookmarkUrl: String? = nil, + boostsCount: Int32? = nil, + boostsUrl: String? = nil, + bucket: TodoBucket? = nil, + byteSize: Int? = nil, + caption: String? = nil, + commentsCount: Int32? = nil, + commentsUrl: String? = nil, + contentType: String? = nil, + createdAt: String? = nil, + creator: Person? = nil, + description: String? = nil, + descriptionAttachments: [RichTextAttachment]? = nil, + downloadUrl: String? = nil, + filename: String? = nil, + height: Int32? = nil, + id: Int? = nil, + inheritsStatus: Bool? = nil, + key: String? = nil, + parent: RecordingParent? = nil, + position: Int32? = nil, + previewUrl: String? = nil, + previewable: Bool? = nil, + sgid: String? = nil, + status: String? = nil, + statusUrl: String? = nil, + subscriptionUrl: String? = nil, + thumbnailUrl: String? = nil, + title: String? = nil, + type: String? = nil, + updatedAt: String? = nil, + url: String? = nil, + visibleToClients: Bool? = nil, + width: Int32? = nil + ) { + self.appDownloadUrl = appDownloadUrl + self.appUrl = appUrl + self.attachableSgid = attachableSgid + self.bookmarkUrl = bookmarkUrl + self.boostsCount = boostsCount + self.boostsUrl = boostsUrl + self.bucket = bucket + self.byteSize = byteSize + self.caption = caption + self.commentsCount = commentsCount + self.commentsUrl = commentsUrl + self.contentType = contentType + self.createdAt = createdAt + self.creator = creator + self.description = description + self.descriptionAttachments = descriptionAttachments + self.downloadUrl = downloadUrl + self.filename = filename + self.height = height + self.id = id + self.inheritsStatus = inheritsStatus + self.key = key + self.parent = parent + self.position = position + self.previewUrl = previewUrl + self.previewable = previewable + self.sgid = sgid + self.status = status + self.statusUrl = statusUrl + self.subscriptionUrl = subscriptionUrl + self.thumbnailUrl = thumbnailUrl + self.title = title + self.type = type + self.updatedAt = updatedAt + self.url = url + self.visibleToClients = visibleToClients + self.width = width + } } diff --git a/swift/Sources/Basecamp/Generated/Models/TimelineEvent.swift b/swift/Sources/Basecamp/Generated/Models/TimelineEvent.swift index c8e925a44..2d1880b6f 100644 --- a/swift/Sources/Basecamp/Generated/Models/TimelineEvent.swift +++ b/swift/Sources/Basecamp/Generated/Models/TimelineEvent.swift @@ -17,4 +17,38 @@ public struct TimelineEvent: Codable, Sendable { public var target: String? public var title: String? public var url: String? + + public init( + action: String? = nil, + appUrl: String? = nil, + attachments: [TimelineAttachment]? = nil, + avatarsSample: [String]? = nil, + bucket: TodoBucket? = nil, + createdAt: String? = nil, + creator: Person? = nil, + data: TimelineEventData? = nil, + id: Int? = nil, + kind: String? = nil, + parentRecordingId: Int? = nil, + summaryExcerpt: String? = nil, + target: String? = nil, + title: String? = nil, + url: String? = nil + ) { + self.action = action + self.appUrl = appUrl + self.attachments = attachments + self.avatarsSample = avatarsSample + self.bucket = bucket + self.createdAt = createdAt + self.creator = creator + self.data = data + self.id = id + self.kind = kind + self.parentRecordingId = parentRecordingId + self.summaryExcerpt = summaryExcerpt + self.target = target + self.title = title + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/UpdateQuestionNotificationSettingsResponseContent.swift b/swift/Sources/Basecamp/Generated/Models/UpdateQuestionNotificationSettingsResponseContent.swift index a89dd46af..512047c87 100644 --- a/swift/Sources/Basecamp/Generated/Models/UpdateQuestionNotificationSettingsResponseContent.swift +++ b/swift/Sources/Basecamp/Generated/Models/UpdateQuestionNotificationSettingsResponseContent.swift @@ -4,4 +4,9 @@ import Foundation public struct UpdateQuestionNotificationSettingsResponseContent: Codable, Sendable { public var responding: Bool? public var subscribed: Bool? + + public init(responding: Bool? = nil, subscribed: Bool? = nil) { + self.responding = responding + self.subscribed = subscribed + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookCopy.swift b/swift/Sources/Basecamp/Generated/Models/WebhookCopy.swift index ee638b7eb..a55381598 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookCopy.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookCopy.swift @@ -6,4 +6,16 @@ public struct WebhookCopy: Codable, Sendable { public var bucket: WebhookCopyBucket? public var id: Int? public var url: String? + + public init( + appUrl: String? = nil, + bucket: WebhookCopyBucket? = nil, + id: Int? = nil, + url: String? = nil + ) { + self.appUrl = appUrl + self.bucket = bucket + self.id = id + self.url = url + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookCopyBucket.swift b/swift/Sources/Basecamp/Generated/Models/WebhookCopyBucket.swift index 3fd5f0239..6f7ec7d3e 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookCopyBucket.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookCopyBucket.swift @@ -3,4 +3,8 @@ import Foundation public struct WebhookCopyBucket: Codable, Sendable { public var id: Int? + + public init(id: Int? = nil) { + self.id = id + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookDelivery.swift b/swift/Sources/Basecamp/Generated/Models/WebhookDelivery.swift index 1fd7f5c8e..dbbf37e01 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookDelivery.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookDelivery.swift @@ -6,4 +6,16 @@ public struct WebhookDelivery: Codable, Sendable { public var id: Int? public var request: WebhookDeliveryRequest? public var response: WebhookDeliveryResponse? + + public init( + createdAt: String? = nil, + id: Int? = nil, + request: WebhookDeliveryRequest? = nil, + response: WebhookDeliveryResponse? = nil + ) { + self.createdAt = createdAt + self.id = id + self.request = request + self.response = response + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryRequest.swift b/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryRequest.swift index 5447e5796..6186f2472 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryRequest.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryRequest.swift @@ -4,4 +4,9 @@ import Foundation public struct WebhookDeliveryRequest: Codable, Sendable { public var body: WebhookEvent? public var headers: WebhookHeadersMap? + + public init(body: WebhookEvent? = nil, headers: WebhookHeadersMap? = nil) { + self.body = body + self.headers = headers + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryResponse.swift b/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryResponse.swift index 84d3684c0..23dbcd0f4 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryResponse.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookDeliveryResponse.swift @@ -5,4 +5,10 @@ public struct WebhookDeliveryResponse: Codable, Sendable { public var code: Int32? public var headers: WebhookHeadersMap? public var message: String? + + public init(code: Int32? = nil, headers: WebhookHeadersMap? = nil, message: String? = nil) { + self.code = code + self.headers = headers + self.message = message + } } diff --git a/swift/Sources/Basecamp/Generated/Models/WebhookEvent.swift b/swift/Sources/Basecamp/Generated/Models/WebhookEvent.swift index 02170960c..afcc3be6b 100644 --- a/swift/Sources/Basecamp/Generated/Models/WebhookEvent.swift +++ b/swift/Sources/Basecamp/Generated/Models/WebhookEvent.swift @@ -9,4 +9,22 @@ public struct WebhookEvent: Codable, Sendable { public var id: Int? public var kind: String? public var recording: Recording? + + public init( + copy: WebhookCopy? = nil, + createdAt: String? = nil, + creator: Person? = nil, + details: String? = nil, + id: Int? = nil, + kind: String? = nil, + recording: Recording? = nil + ) { + self.copy = copy + self.createdAt = createdAt + self.creator = creator + self.details = details + self.id = id + self.kind = kind + self.recording = recording + } } diff --git a/swift/Sources/BasecampGenerator/ModelEmitter.swift b/swift/Sources/BasecampGenerator/ModelEmitter.swift index e89b95420..81826744c 100644 --- a/swift/Sources/BasecampGenerator/ModelEmitter.swift +++ b/swift/Sources/BasecampGenerator/ModelEmitter.swift @@ -213,37 +213,47 @@ func emitEntityModel(schemaName: String, schemas: [String: Any]) -> String { } } - if !requiredFields.isEmpty { - lines.append("") - var initParams: [String] = [] - for propName in orderedProps { - guard let propSchema = properties[propName] as? [String: Any] else { continue } - let baseType = schemaToSwiftType(propSchema) - let camelName = toCamelCase(propName) - let required = requiredFields.contains(propName) - let valueOptional = schemaIsNullable(propSchema) || !required - let propType = baseType + (valueOptional ? "?" : "") - // Required members take no default (caller must supply presence). - initParams.append(required ? "\(camelName): \(propType)" : "\(camelName): \(propType) = nil") - } + // Emitted unconditionally, matching `emitRequestModel`. Swift's implicit + // memberwise initializer is `internal`, so a struct without an explicit + // `public init` is unconstructible outside the module — an all-optional + // model would otherwise compile in-repo (every test source that imports + // the SDK imports it as `@testable import Basecamp`, and none plain-imports + // it) and be uncallable for a consumer that plain-`import`s the SDK (#735). + // `Sources/BasecampPublicAPIConsumer` is the target that observes this from + // outside. + lines.append("") + var initParams: [String] = [] + for propName in orderedProps { + guard let propSchema = properties[propName] as? [String: Any] else { continue } + let baseType = schemaToSwiftType(propSchema) + let camelName = toCamelCase(propName) + let required = requiredFields.contains(propName) + let valueOptional = schemaIsNullable(propSchema) || !required + let propType = baseType + (valueOptional ? "?" : "") + // Required members take no default (caller must supply presence). + initParams.append(required ? "\(camelName): \(propType)" : "\(camelName): \(propType) = nil") + } - if initParams.count <= 3 { - lines.append(" public init(\(initParams.joined(separator: ", "))) {") - } else { - lines.append(" public init(") - for (i, param) in initParams.enumerated() { - let comma = i < initParams.count - 1 ? "," : "" - lines.append(" \(param)\(comma)") - } - lines.append(" ) {") + if initParams.count <= 3 { + lines.append(" public init(\(initParams.joined(separator: ", "))) {") + } else { + lines.append(" public init(") + for (i, param) in initParams.enumerated() { + let comma = i < initParams.count - 1 ? "," : "" + lines.append(" \(param)\(comma)") } + lines.append(" ) {") + } - for propName in orderedProps { - let camelName = toCamelCase(propName) - lines.append(" self.\(camelName) = \(camelName)") - } - lines.append(" }") + // Same guard as the two loops above: a property whose schema is not a + // dictionary declares no member and takes no parameter, so it must not be + // assigned either. The three loops have to agree on which properties exist. + for propName in orderedProps { + guard properties[propName] is [String: Any] else { continue } + let camelName = toCamelCase(propName) + lines.append(" self.\(camelName) = \(camelName)") } + lines.append(" }") // Synthesized Codable treats an optional-typed property as decodeIfPresent // (missing OK) and omits nil on encode — which is wrong for a diff --git a/swift/Sources/BasecampPublicAPIConsumer/PublicAPIConsumer.swift b/swift/Sources/BasecampPublicAPIConsumer/PublicAPIConsumer.swift new file mode 100644 index 000000000..82cbcfc50 --- /dev/null +++ b/swift/Sources/BasecampPublicAPIConsumer/PublicAPIConsumer.swift @@ -0,0 +1,146 @@ +// A stand-in for a customer's app. This target imports the SDK the way an +// external package does — plain `import Basecamp`, never `@testable import` — +// and it exists solely to be compiled. +// +// Why it exists (#735): Swift's implicit memberwise initializer is `internal`, +// so a generated model with no explicit `public init` is unconstructible +// outside the module. Every test source in Tests/BasecampTests that imports the +// SDK imports it as `@testable import Basecamp`, which raises internal to +// visible, and none plain-imports it — so 35 all-optional models, two of them +// *request* payloads whose operations were therefore uncallable, compiled fine +// in-repo and shipped broken. Nothing in the package built against the public +// surface, so no CI job modelled a customer. The missing init was the defect; +// the missing consumer is why it took a customer to find it. +// +// Two rules keep this target honest, both enforced by PublicInitCoverageTests: +// +// 1. It must never use `@testable`. That single word re-opens the exact +// blind spot this target closes, and it is the natural "fix" for anyone +// hitting a compile error here. +// 2. It must stay a non-test target. `swift build` compiles it — so +// `make swift-build`, `make swift-check`, the Swift CI job, the release +// workflow, and the CodeQL Swift build all cover it, not just `swift test`. +// +// One way this target is *not* a customer, stated so nobody assumes otherwise: +// it lives in the same SwiftPM package, so `package`-level declarations — e.g. +// `BasecampClient.httpClient` — are visible here and are not visible to an +// external package. Nothing below touches one, and nothing added below should. +// Closing that last gap would mean a separate nested package, which would drop +// out of `swift build` and need its own CI step; the failure class this target +// exists for is internal-vs-public, which it does observe. +// +// Nothing here performs I/O; the async entry points are type-checked, never run. + +import Basecamp +import Foundation + +/// Public-surface exercises. Every member is written the way a consumer would +/// write it, using only what `import Basecamp` exports. +public enum PublicAPIConsumer { + // MARK: - The #735 case: all-optional request payloads + + /// `GaugeNeedleUpdatePayload` has one optional member and no required one. + /// Before #735 this function could not be written outside the module: the + /// payload had no `public init`, so `UpdateGaugeNeedleRequest(gaugeNeedle:)` + /// could only ever be handed `nil` — an empty `{}` PUT that bc3 rejects. + public static func updateGaugeNeedleDescription( + account: AccountClient, + needleId: Int, + description: String + ) async throws -> GaugeNeedle { + var payload = GaugeNeedleUpdatePayload() + payload.description = description + return try await account.gauges.updateGaugeNeedle( + needleId: needleId, + req: UpdateGaugeNeedleRequest(gaugeNeedle: payload) + ) + } + + /// The same defect on the other affected operation: `PreferencesPayload` is + /// all-optional, and `UpdateMyPreferences` carries nothing else. + public static func updateMyTimeZone( + account: AccountClient, + timeZoneName: String + ) async throws -> Preferences { + let payload = PreferencesPayload(timeZoneName: timeZoneName) + return try await account.people.updateMyPreferences( + req: UpdateMyPreferencesRequest(person: payload) + ) + } + + // MARK: - Client construction + + /// The documented entry point, exercised end to end so the consumer path is + /// not just model construction. + public static func makeAccountClient(accessToken: String, accountId: String) -> AccountClient { + let client = BasecampClient( + accessToken: accessToken, + userAgent: "PublicAPIConsumer/1.0 (sdk@basecamp.com)" + ) + return client.forAccount(accountId) + } + + // MARK: - Every all-optional model, constructed from outside the module + + /// The full roster of generated models that carry no required member, each + /// built with the zero-argument initializer a consumer needs. + /// + /// Listing all of them rather than a sample is deliberate: each line is an + /// independent compile-time assertion, so reverting the generator fix + /// produces 35 errors here instead of one that could be argued away. A + /// model that disappears or is renamed also breaks this list, which is the + /// intended signal — the roster is meant to be re-read, not auto-followed. + /// + /// The complementary guarantee lives in `PublicInitCoverageTests`, which scans + /// *every* generated model for a `public init` and so covers models added + /// after this list was written. + public static func allOptionalModelsAreConstructibleFromOutsideTheModule() { + _ = AccountLimits() + _ = AccountLogo() + _ = AccountSettings() + _ = AccountSubscription() + _ = CampfireLineAttachment() + _ = ClientApprovalResponse() + _ = ClientSide() + _ = CreateAttachmentResponseContent() + _ = DoorService() + _ = EventDetails() + _ = EverythingFile() + _ = GaugeNeedleUpdatePayload() + _ = GetAssignedTodosResponseContent() + _ = GetMyAssignmentsResponseContent() + _ = GetOverdueTodosResponseContent() + _ = GetPersonProgressResponseContent() + _ = OutOfOffice() + _ = PauseQuestionResponseContent() + _ = Preferences() + _ = PreferencesPayload() + _ = PreviewableAttachment() + _ = ProjectAccessResult() + _ = QuestionReminder() + _ = QuestionSchedule() + _ = ResumeQuestionResponseContent() + _ = ScheduleAttributes() + _ = TimelineAttachment() + _ = TimelineEvent() + _ = UpdateQuestionNotificationSettingsResponseContent() + _ = WebhookCopy() + _ = WebhookCopyBucket() + _ = WebhookDelivery() + _ = WebhookDeliveryRequest() + _ = WebhookDeliveryResponse() + _ = WebhookEvent() + } + + // MARK: - Models that already had an init, so the fix is checked both ways + + /// `GaugeNeedlePayload` has a required `position` and was always + /// constructible. Keeping it here proves the generator change widened the + /// init to all-optional models without altering the required-member shape: + /// required parameters still take no default, optional ones still default + /// to nil. + public static func requiredMemberModelsKeepTheirInitShape() { + _ = GaugeNeedlePayload(position: 50) + _ = GaugeNeedlePayload(position: 50, color: "#00ff00", description: "