Skip to content
Merged
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
70 changes: 70 additions & 0 deletions Sources/Ontology/Types/ItemList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

/// An ItemList model following Schema.org ontology (https://schema.org/ItemList)
public struct ItemList: Hashable, Sendable {
/// Unique identifier for the item list
public var identifier: String?

/// The name/title of the item list
public var name: String?

/// URL associated with the item list
public var url: URL?

/// The number of items in the list
public var numberOfItems: Int?

public init(name: String? = nil, numberOfItems: Int? = nil) {
self.name = name
self.numberOfItems = numberOfItems
}
}

#if canImport(EventKit)
import EventKit

extension ItemList {
/// Initialize an ItemList from an EKCalendar
public init(_ calendar: EKCalendar) {
self.identifier = calendar.calendarIdentifier
self.name = calendar.title
}
}
#endif

extension ItemList: Codable {
private enum CodingKeys: String, CodingKey {
case name, url, numberOfItems
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: JSONLDCodingKey<CodingKeys>.self)

if encoder.codingPath.isEmpty {
try container.encode(schema.org, forKey: .context)
}

try container.encode("ItemList", forKey: .type)
try container.encodeIfPresent(identifier, forKey: .id)
try container.encodeIfPresent(name, forKey: .attribute(.name))
try container.encodeIfPresent(url, forKey: .attribute(.url))
try container.encodeIfPresent(numberOfItems, forKey: .attribute(.numberOfItems))
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: JSONLDCodingKey<CodingKeys>.self)
let decodedType = try container.decode(String.self, forKey: .type)
guard decodedType == "ItemList" else {
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Expected type to be 'ItemList', but found \(decodedType)"
)
}

identifier = try container.decodeIfPresent(String.self, forKey: .id)
name = try container.decodeIfPresent(String.self, forKey: .attribute(.name))
url = try container.decodeIfPresent(URL.self, forKey: .attribute(.url))
numberOfItems = try container.decodeIfPresent(Int.self, forKey: .attribute(.numberOfItems))
}
}
10 changes: 9 additions & 1 deletion Sources/Ontology/Types/PlanAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public struct PlanAction: Hashable, Sendable {
/// URLs associated with the plan action
public var url: URL?

/// The list this reminder belongs to, modeled as a Schema.org ItemList.
public var object: ItemList?

public init(
name: String,
dueDate: Date? = nil,
Expand Down Expand Up @@ -58,6 +61,9 @@ public struct PlanAction: Hashable, Sendable {
self.status = reminder.isCompleted ? .completed : .potential
self.priority = reminder.priority > 0 ? reminder.priority : nil
self.url = reminder.url
if let calendar = reminder.calendar {
self.object = ItemList(calendar)
}
}
}
#endif
Expand All @@ -66,7 +72,7 @@ extension PlanAction: Codable {
private enum CodingKeys: String, CodingKey {
case name, description, scheduledTime
case status = "actionStatus"
case priority, url
case priority, url, object
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -90,6 +96,7 @@ extension PlanAction: Codable {
try container.encodeIfPresent(status?.rawValue, forKey: .attribute(.status))
try container.encodeIfPresent(priority, forKey: .attribute(.priority))
try container.encodeIfPresent(url, forKey: .attribute(.url))
try container.encodeIfPresent(object, forKey: .attribute(.object))
}

public init(from decoder: Decoder) throws {
Expand Down Expand Up @@ -123,5 +130,6 @@ extension PlanAction: Codable {

priority = try container.decodeIfPresent(Int.self, forKey: .attribute(.priority))
url = try container.decodeIfPresent(URL.self, forKey: .attribute(.url))
object = try container.decodeIfPresent(ItemList.self, forKey: .attribute(.object))
}
}
88 changes: 88 additions & 0 deletions Tests/OntologyTests/ItemListTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Foundation
import Testing

@testable import Ontology

@Suite
struct ItemListTests {
@Test("ItemList basic initialization")
func testBasicInitialization() throws {
let itemList = ItemList(name: "Shopping List", numberOfItems: 5)

#expect(itemList.name == "Shopping List")
#expect(itemList.numberOfItems == 5)
#expect(itemList.identifier == nil)
#expect(itemList.url == nil)
}

@Test("ItemList JSON-LD encoding")
func testJSONLDEncoding() throws {
var itemList = ItemList(name: "Test List", numberOfItems: 3)
itemList.identifier = "list-id"
itemList.url = URL(string: "https://example.com/list")

let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(itemList)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]

#expect(json["@context"] as? String == "https://schema.org")
#expect(json["@type"] as? String == "ItemList")
#expect(json["@id"] as? String == "list-id")
#expect(json["name"] as? String == "Test List")
#expect(json["numberOfItems"] as? Int == 3)
#expect(json["url"] as? String == "https://example.com/list")
}

@Test("ItemList JSON-LD decoding")
func testJSONLDDecoding() throws {
let json = """
{
"@context": "https://schema.org",
"@type": "ItemList",
"@id": "decoded-list",
"name": "Decoded List",
"numberOfItems": 7,
"url": "https://example.com/decoded"
}
"""

let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
let itemList = try decoder.decode(ItemList.self, from: data)

#expect(itemList.identifier == "decoded-list")
#expect(itemList.name == "Decoded List")
#expect(itemList.numberOfItems == 7)
#expect(itemList.url?.absoluteString == "https://example.com/decoded")
}

@Test("ItemList equality and hashing")
func testEqualityAndHashing() throws {
let itemList1 = ItemList(name: "Test List", numberOfItems: 5)
let itemList2 = ItemList(name: "Test List", numberOfItems: 5)
let itemList3 = ItemList(name: "Different List", numberOfItems: 5)

#expect(itemList1 == itemList2)
#expect(itemList1 != itemList3)
#expect(itemList1.hashValue == itemList2.hashValue)
}

@Test("ItemList type validation on decode")
func testTypeValidation() throws {
let invalidJson = """
{
"@context": "https://schema.org",
"@type": "WrongType",
"name": "Invalid"
}
"""

let data = invalidJson.data(using: .utf8)!
let decoder = JSONDecoder()

#expect(throws: DecodingError.self) {
try decoder.decode(ItemList.self, from: data)
}
}
}
130 changes: 130 additions & 0 deletions Tests/OntologyTests/PlanActionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import Foundation
import Testing

@testable import Ontology

@Suite
struct PlanActionTests {
@Test("PlanAction basic initialization")
func testBasicInitialization() throws {
let planAction = PlanAction(
name: "Buy groceries",
dueDate: Date(timeIntervalSince1970: 1_640_995_200), // 2022-01-01
description: "Weekly grocery shopping",
completed: false
)

#expect(planAction.name == "Buy groceries")
#expect(planAction.description == "Weekly grocery shopping")
#expect(planAction.status == .potential)
#expect(planAction.scheduledTime?.value == Date(timeIntervalSince1970: 1_640_995_200))
}

@Test("PlanAction completed status")
func testCompletedStatus() throws {
let planAction = PlanAction(
name: "Complete project",
completed: true
)

#expect(planAction.status == .completed)
}

@Test("PlanAction status enum values")
func testStatusEnumValues() throws {
#expect(PlanAction.Status.active.rawValue == "ActiveAction")
#expect(PlanAction.Status.completed.rawValue == "CompletedAction")
#expect(PlanAction.Status.failed.rawValue == "FailedAction")
#expect(PlanAction.Status.potential.rawValue == "PotentialAction")
}

@Test("PlanAction JSON-LD encoding")
func testJSONLDEncoding() throws {
var planAction = PlanAction(
name: "Test task",
dueDate: Date(timeIntervalSince1970: 1_640_995_200),
description: "A test task",
completed: false
)
planAction.identifier = "test-id"
planAction.priority = 5
planAction.url = URL(string: "https://example.com/task")

let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(planAction)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]

#expect(json["@context"] as? String == "https://schema.org")
#expect(json["@type"] as? String == "PlanAction")
#expect(json["@id"] as? String == "test-id")
#expect(json["name"] as? String == "Test task")
#expect(json["description"] as? String == "A test task")
#expect(json["actionStatus"] as? String == "PotentialAction")
#expect(json["priority"] as? Int == 5)
#expect(json["url"] as? String == "https://example.com/task")
}

@Test("PlanAction JSON-LD decoding")
func testJSONLDDecoding() throws {
let json = """
{
"@context": "https://schema.org",
"@type": "PlanAction",
"@id": "test-id",
"name": "Decoded task",
"description": "A decoded task",
"actionStatus": "CompletedAction",
"priority": 3,
"url": "https://example.com/decoded"
}
"""

let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
let planAction = try decoder.decode(PlanAction.self, from: data)

#expect(planAction.identifier == "test-id")
#expect(planAction.name == "Decoded task")
#expect(planAction.description == "A decoded task")
#expect(planAction.status == .completed)
#expect(planAction.priority == 3)
#expect(planAction.url?.absoluteString == "https://example.com/decoded")
}

@Test("PlanAction with ItemList object")
func testPlanActionWithItemList() throws {
var planAction = PlanAction(
name: "Task in list",
completed: false
)

let itemList = ItemList(name: "My Tasks", numberOfItems: 10)
planAction.object = itemList

let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(planAction)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]

#expect(json["@context"] as? String == "https://schema.org")
#expect(json["@type"] as? String == "PlanAction")
#expect(json["name"] as? String == "Task in list")

let object = json["object"] as! [String: Any]
#expect(object["@type"] as? String == "ItemList")
#expect(object["name"] as? String == "My Tasks")
#expect(object["numberOfItems"] as? Int == 10)
}

@Test("PlanAction equality and hashing")
func testEqualityAndHashing() throws {
let planAction1 = PlanAction(name: "Test", completed: false)
let planAction2 = PlanAction(name: "Test", completed: false)
let planAction3 = PlanAction(name: "Different", completed: false)

#expect(planAction1 == planAction2)
#expect(planAction1 != planAction3)
#expect(planAction1.hashValue == planAction2.hashValue)
}
}