Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Sources/NextcloudKit/Models/NKOCSWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/// Generic `Decodable` envelope for OCS v2 responses.
/// Server replies follow `{ "ocs": { "meta": ..., "data": ... } }`.
public struct NKOCSWrapper<T: Decodable>: Decodable {
public let ocs: Inner

public struct Inner: Decodable {
public let meta: NKOCSMeta
public let data: T
}
}

/// OCS response metadata. `message` is optional per the OCS contract.
public struct NKOCSMeta: Decodable, Sendable {
public let status: String
public let statuscode: Int
public let message: String?
}
29 changes: 8 additions & 21 deletions Sources/NextcloudKit/Models/NKTermsOfService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import Foundation

public class NKTermsOfService: NSObject {
public var meta: Meta?
/// Source-compat alias for callers that still reference `NKTermsOfService.Meta`.
public typealias Meta = NKOCSMeta

public var meta: NKOCSMeta?
public var data: OCSData?

public override init() {
Expand All @@ -14,9 +17,9 @@ public class NKTermsOfService: NSObject {

public func loadFromJSON(_ jsonData: Data) -> Bool {
do {
let decodedResponse = try JSONDecoder().decode(OCSResponse.self, from: jsonData)
self.meta = decodedResponse.ocs.meta
self.data = decodedResponse.ocs.data
let decoded = try JSONDecoder().decode(NKOCSWrapper<OCSData>.self, from: jsonData)
self.meta = decoded.ocs.meta
self.data = decoded.ocs.data
return true
} catch {
debugPrint("[DEBUG] decode error:", error)
Expand All @@ -36,26 +39,10 @@ public class NKTermsOfService: NSObject {
return data?.hasSigned ?? false
}

public func getMeta() -> Meta? {
public func getMeta() -> NKOCSMeta? {
return meta
}

// MARK: - Codable
private class OCSResponse: Codable {
let ocs: OCS
}

private class OCS: Codable {
let meta: Meta
let data: OCSData
}

public class Meta: Codable {
public let status: String
public let statuscode: Int
public let message: String
}

public class OCSData: Codable {
public let terms: [Term]
public let languages: [String: String]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

#if DEBUG
import Foundation

public extension NKUnifiedShare {
/// Sample share for SwiftUI previews and tests, covering all property variants.
static var mock: NKUnifiedShare {
NKUnifiedShare(
id: "preview-1",
owner: NKUnifiedShareOwner(
userId: "alice",
instance: nil,
displayName: "Alice",
icon: NKUnifiedShareIcon(svg: "<svg/>", light: nil, dark: nil)
),
lastUpdated: 1_730_000_000_000,
state: .draft,
sources: [
NKUnifiedShareSource(
class: "file",
value: "/Test.txt",
displayName: "Test.txt",
icon: NKUnifiedShareIcon(svg: nil, light: "https://example.com/light.png", dark: "https://example.com/dark.png")
)
],
recipients: [
// NKUnifiedShareRecipient(
// class: "user",
// value: "bob",
// instance: nil,
// displayName: "Bob",
// icon: NKUnifiedShareIcon(svg: "<svg/>", light: nil, dark: nil)
// )
],
properties: [
NKUnifiedSharePropertyDate(
class: "expiration",
displayName: "Expiration",
priority: 10,
required: false,
minDate: "2026-01-01"
),
NKUnifiedSharePropertyEnum(
class: "role",
displayName: "Role",
priority: 20,
required: true,
value: "editor",
validValues: ["viewer", "editor"]
),
NKUnifiedSharePropertyBoolean(
class: "download",
displayName: "Allow download",
priority: 30,
required: false,
value: "true"
),
NKUnifiedSharePropertyPassword(
class: "password",
displayName: "Password",
hint: "Min 8 chars",
priority: 40,
required: false
),
NKUnifiedSharePropertyString(
class: "note",
displayName: "Note",
priority: 50,
required: false,
value: "hi",
minLength: 0,
maxLength: 1000
)
],
permissions: [
NKUnifiedSharePermission(
class: "download",
displayName: "Allow download",
hint: nil,
category: nil,
enabled: true
)
]
)
}
}
#endif
101 changes: 101 additions & 0 deletions Sources/NextcloudKit/Models/UnifiedSharing/NKUnifiedShare.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/// A unified share returned by `ocs/v2.php/apps/sharing/api/v1/...`.
public final class NKUnifiedShare: Codable {
public let id: String
public let owner: NKUnifiedShareOwner
/// Unix time in milliseconds.
public let lastUpdated: Int64
public let state: NKUnifiedShareState
public let sources: [NKUnifiedShareSource]
public let recipients: [NKUnifiedShareRecipient]
public let properties: [NKUnifiedShareProperty]
public let permissions: [NKUnifiedSharePermission]

enum CodingKeys: String, CodingKey {
case id
case owner
case lastUpdated = "last_updated"
case state
case sources
case recipients
case properties
case permissions
}

public init(id: String,
owner: NKUnifiedShareOwner,
lastUpdated: Int64,
state: NKUnifiedShareState,
sources: [NKUnifiedShareSource],
recipients: [NKUnifiedShareRecipient],
properties: [NKUnifiedShareProperty],
permissions: [NKUnifiedSharePermission]) {
self.id = id
self.owner = owner
self.lastUpdated = lastUpdated
self.state = state
self.sources = sources
self.recipients = recipients
self.properties = properties
self.permissions = permissions
}

public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decode(String.self, forKey: .id)
self.owner = try c.decode(NKUnifiedShareOwner.self, forKey: .owner)
self.lastUpdated = try c.decode(Int64.self, forKey: .lastUpdated)
self.state = try c.decode(NKUnifiedShareState.self, forKey: .state)
self.sources = try c.decode([NKUnifiedShareSource].self, forKey: .sources)
self.recipients = try c.decode([NKUnifiedShareRecipient].self, forKey: .recipients)
self.permissions = try c.decode([NKUnifiedSharePermission].self, forKey: .permissions)
self.properties = try Self.decodeProperties(from: c)
}

public func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(id, forKey: .id)
try c.encode(owner, forKey: .owner)
try c.encode(lastUpdated, forKey: .lastUpdated)
try c.encode(state, forKey: .state)
try c.encode(sources, forKey: .sources)
try c.encode(recipients, forKey: .recipients)
try c.encode(permissions, forKey: .permissions)
try c.encode(properties, forKey: .properties)
}

/// Dispatch each `properties` element to the correct subclass based on its `type` discriminator.
/// Uses two independent unkeyed containers — one to peek, one to decode — so we can read the
/// `type` of every element without consuming the cursor we actually need for the concrete decode.
private static func decodeProperties(from c: KeyedDecodingContainer<CodingKeys>) throws -> [NKUnifiedShareProperty] {
var peek = try c.nestedUnkeyedContainer(forKey: .properties)
var real = try c.nestedUnkeyedContainer(forKey: .properties)
var result: [NKUnifiedShareProperty] = []

while !real.isAtEnd {
let holder = try peek.decode(TypeHolder.self)
switch holder.type {
case .date:
result.append(try real.decode(NKUnifiedSharePropertyDate.self))
case .enumeration:
result.append(try real.decode(NKUnifiedSharePropertyEnum.self))
case .boolean:
result.append(try real.decode(NKUnifiedSharePropertyBoolean.self))
case .password:
result.append(try real.decode(NKUnifiedSharePropertyPassword.self))
case .string:
result.append(try real.decode(NKUnifiedSharePropertyString.self))
}
}
return result
}

private struct TypeHolder: Decodable {
let type: NKUnifiedSharePropertyType
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/// Icon attached to an owner, source, recipient, or permission category.
///
/// The OpenAPI declares this as `anyOf <IconSVG, IconURL>`. The two variants have disjoint key
/// sets (`svg` vs `light`+`dark`) so a single flat struct with all keys optional decodes either
/// shape cleanly with synthesized Codable.
public struct NKUnifiedShareIcon: Codable, Sendable {
/// Inline SVG body (IconSVG variant).
public let svg: String?

/// Absolute URL to a light-theme image (IconURL variant).
public let light: String?

/// Absolute URL to a dark-theme image (IconURL variant).
public let dark: String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/// Owner of a unified share.
public struct NKUnifiedShareOwner: Codable, Sendable {
public let userId: String
public let instance: String?
public let displayName: String
public let icon: NKUnifiedShareIcon

enum CodingKeys: String, CodingKey {
case userId = "user_id"
case instance
case displayName = "display_name"
case icon
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/// A toggleable permission on a unified share (can view / can edit / can comment / …).
public struct NKUnifiedSharePermission: Codable, Sendable {
public let `class`: String
public let displayName: String
public let hint: String?
public let category: String?
public let enabled: Bool

enum CodingKeys: String, CodingKey {
case `class`
case displayName = "display_name"
case hint
case category
case enabled
}
}
Loading
Loading