diff --git a/README.md b/README.md index 582a906..2a3092b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Things JSON Coder -This repo contains a Swift file that allows the creation of the JSON required to be passed to the `add-json` command of Things’ URL scheme. +This repo contains a Swift file that allows the creation of the JSON required to be passed to the `json` command of Things’ URL scheme. ## Installation @@ -33,6 +33,9 @@ There are two wrapper enums used to package objects into arrays. Associated valu * `TJSProject.Item` – This enum has cases for todo and heading objects. Only todo and heading objects can be items inside a project. +#### Dates +Dates should be formatted according to ISO8601. Setting the JSON encoder’s `dateEncodingStrategy` to `ThingsJSONDateEncodingStrategy()` is the easiest way to do this (see example below). + ## Example Create two todos and a project, encode them into JSON and send to Things’ add command. @@ -52,10 +55,13 @@ let container = TJSContainer(items: [.todo(todo1), .project(project)]) do { let encoder = JSONEncoder() + encoder.dateEncodingStrategy = ThingsJSONDateEncodingStrategy() let data = try encoder.encode(container) - let json = String.init(data: data, encoding: .utf8)! - let jsonEncoded = json.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! - let url = URL(string: "things:///add-json?data=\(jsonEncoded)")! + let json = String(data: data, encoding: .utf8)! + var components = URLComponents(string: "things:///add-json")! + let queryItem = URLQueryItem(name: "data", value: json) + components.queryItems = [queryItem] + let url = components.url! UIApplication.shared.open(url, options: [:], completionHandler: nil) } catch { diff --git a/ThingsJSON.swift b/ThingsJSON.swift index fe75d85..7677215 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -27,24 +27,24 @@ import Foundation // MARK: Container /// The container holding the array of items to be encoded to JSON. -class TJSContainer : Codable { +public class TJSContainer : Codable { /// The array of items that will be encoded or decoded from the JSON. - var items = [Item]() + public var items = [Item]() /// Create and return a new ThingsJSON object configured with the provided items. - init(items: [Item]) { + public init(items: [Item]) { self.items = items } /// Creates a new instance by decoding from the given decoder. - required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.items = try container.decode([Item].self) } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { try self.items.encode(to: encoder) } @@ -53,12 +53,12 @@ class TJSContainer : Codable { /// This is an enum that wraps a TJSTodo or TJSProject object and handles its encoding /// and decoding to JSON. This is required because there is no way of specifiying a /// strongly typed array that contains more than one type. - enum Item : Codable { + public enum Item : Codable { case todo(TJSTodo) case project(TJSProject) /// Creates a new instance by decoding from the given decoder. - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { @@ -66,7 +66,7 @@ class TJSContainer : Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_) { + catch TJSError.invalidType(expectedType: _, errorContext: _) { // If it's the wrong type, try a project let project = try container.decode(TJSProject.self) self = .project(project) @@ -74,7 +74,7 @@ class TJSContainer : Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { switch self { case .todo(let todo): try todo.encode(to: encoder) @@ -91,20 +91,44 @@ class TJSContainer : Codable { /// The superclass of all the Things JSON model items. /// /// Do not instantiate this class itself. Instead use one of the subclasses. -class TJSModelItem { +public class TJSModelItem { fileprivate var type: String = "" + /// The operation to perform on the object. + public var operation: Operation + + /// The ID of the item to update. + public var id: String? + private enum CodingKeys: String, CodingKey { case type + case operation + case id case attributes } + public enum Operation: String, Codable { + /// Create a new item. + case create = "create" + /// Update an existing item. + /// + /// Requires id to be set. + case update = "update" + } + + public init(operation: Operation, id: String? = nil) { + self.operation = operation + self.id = id + } + fileprivate func attributes(_ type: T.Type, from decoder: Decoder) throws -> KeyedDecodingContainer { let container = try decoder.container(keyedBy: CodingKeys.self) let decodedType = try container.decode(String.self, forKey: .type) + self.operation = try container.decodeIfPresent(Operation.self, forKey: .operation) ?? .create + self.id = try container.decodeIfPresent(String.self, forKey: .id) guard decodedType == self.type else { - let description = String.init(format: "Expected to decode a %@ but found a %@ instead.", self.type, decodedType) - let errorContext = DecodingError.Context.init(codingPath: [CodingKeys.type], debugDescription: description) + let description = String(format: "Expected to decode a %@ but found a %@ instead.", self.type, decodedType) + let errorContext = DecodingError.Context(codingPath: [CodingKeys.type], debugDescription: description) let expectedType = Swift.type(of: self) throw TJSError.invalidType(expectedType: expectedType, errorContext: errorContext) } @@ -114,6 +138,8 @@ class TJSModelItem { fileprivate func attributes(_ type: T.Type, for encoder: Encoder) throws -> KeyedEncodingContainer { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.type, forKey: .type) + try container.encode(self.operation, forKey: .operation) + try container.encodeIfPresent(self.id, forKey: .id) return container.nestedContainer(keyedBy: T.self, forKey: .attributes) } } @@ -122,93 +148,150 @@ class TJSModelItem { // MARK: - /// Represents a to-do in Things. -class TJSTodo : TJSModelItem, Codable { - var title: String? - var notes: String? - var when: String? - var deadline: String? - var tags: [String]? - var checklistItems: [TJSChecklistItem]? - var listID: String? - var list: String? - var heading: String? - var completed: Bool? - var canceled: Bool? +public class TJSTodo : TJSModelItem, Codable { + public var title: String? + public var notes: String? + public var prependNotes: String? + public var appendNotes: String? + public var when: String? + public var deadline: String? + public var tagIDs: [String]? + public var tags: [String]? + public var addTags: [String]? + public var checklistItems: [TJSChecklistItem]? + public var prependChecklistItems: [TJSChecklistItem]? + public var appendChecklistItems: [TJSChecklistItem]? + public var listID: String? + public var list: String? + public var headingID: String? + public var heading: String? + public var completed: Bool? + public var canceled: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case notes + case prependNotes = "prepend-notes" + case appendNotes = "append-notes" case when case deadline + case tagIDs = "tag-ids" case tags + case addTags = "add-tags" case checklistItems = "checklist-items" + case prependChecklistItems = "prepend-checklist-items" + case appendChecklistItems = "append-checklist-items" case listID = "list-id" case list + case headingID = "heading-id" case heading case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new todo configured with the provided values. - init(title: String? = nil, + public init(operation: Operation = .create, + id: String? = nil, + title: String? = nil, notes: String? = nil, + prependNotes: String? = nil, + appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, + tagIDs: [String]? = nil, tags: [String]? = nil, + addTags: [String]? = nil, checklistItems: [TJSChecklistItem]? = nil, + prependChecklistItems: [TJSChecklistItem]? = nil, + appendChecklistItems: [TJSChecklistItem]? = nil, listID: String? = nil, list: String? = nil, + headingID: String? = nil, heading: String? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation, id: id) self.type = "to-do" self.title = title self.notes = notes + self.prependNotes = prependNotes + self.appendNotes = appendNotes self.when = when self.deadline = deadline + self.tagIDs = tagIDs self.tags = tags + self.addTags = addTags self.checklistItems = checklistItems + self.prependChecklistItems = prependChecklistItems + self.appendChecklistItems = appendChecklistItems self.listID = listID self.list = list self.heading = heading + self.headingID = headingID self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new todo configured with same values as the provided todo. - convenience init(_ todo: TJSTodo) { - self.init(title: todo.title, + public convenience init(_ todo: TJSTodo) { + self.init(id: todo.id, + title: todo.title, notes: todo.notes, + prependNotes: todo.prependNotes, + appendNotes: todo.appendNotes, when: todo.when, deadline: todo.deadline, + tagIDs: todo.tagIDs, tags: todo.tags, + addTags: todo.addTags, checklistItems: todo.checklistItems, + prependChecklistItems: todo.prependChecklistItems, + appendChecklistItems: todo.appendChecklistItems, listID: todo.listID, list: todo.list, + headingID: todo.headingID, heading: todo.heading, completed: todo.completed, - canceled: todo.canceled) + canceled: todo.canceled, + creationDate: todo.creationDate, + completionDate: todo.completionDate) } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) do { title = try attributes.decodeIfPresent(String.self, forKey: .title) notes = try attributes.decodeIfPresent(String.self, forKey: .notes) + prependNotes = try attributes.decodeIfPresent(String.self, forKey: .prependNotes) + appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) + tagIDs = try attributes.decodeIfPresent([String].self, forKey: .tagIDs) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) + addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) checklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .checklistItems) + prependChecklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .prependChecklistItems) + appendChecklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .appendChecklistItems) listID = try attributes.decodeIfPresent(String.self, forKey: .listID) list = try attributes.decodeIfPresent(String.self, forKey: .list) + headingID = try attributes.decodeIfPresent(String.self, forKey: .headingID) heading = try attributes.decodeIfPresent(String.self, forKey: .heading) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } catch TJSError.invalidType(let expectedType, let errorContext) { throw DecodingError.typeMismatch(expectedType, errorContext) @@ -216,19 +299,28 @@ class TJSTodo : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) + try attributes.encodeIfPresent(prependNotes, forKey: .prependNotes) + try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) + try attributes.encodeIfPresent(tagIDs, forKey: .tagIDs) try attributes.encodeIfPresent(tags, forKey: .tags) + try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(checklistItems, forKey: .checklistItems) + try attributes.encodeIfPresent(prependChecklistItems, forKey: .prependChecklistItems) + try attributes.encodeIfPresent(appendChecklistItems, forKey: .appendChecklistItems) try attributes.encodeIfPresent(listID, forKey: .listID) try attributes.encodeIfPresent(list, forKey: .list) + try attributes.encodeIfPresent(headingID, forKey: .headingID) try attributes.encodeIfPresent(heading, forKey: .heading) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -236,87 +328,126 @@ class TJSTodo : TJSModelItem, Codable { // MARK: - /// Represents a project in Things. -class TJSProject : TJSModelItem, Codable { +public class TJSProject : TJSModelItem, Codable { var title: String? var notes: String? + var prependNotes: String? + var appendNotes: String? var when: String? var deadline: String? + var tagIDs: [String]? var tags: [String]? + var addTags: [String]? var areaID: String? var area: String? var items: [Item]? var completed: Bool? var canceled: Bool? + var creationDate: Date? + var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case notes + case prependNotes = "prepend-notes" + case appendNotes = "append-notes" case when case deadline + case tagIDs = "tag-ids" case tags + case addTags = "add-tags" case areaID = "area-id" case area case items case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new project configured with the provided values. - init(title: String? = nil, + init(operation: Operation = .create, + id: String? = nil, + title: String? = nil, notes: String? = nil, + prependNotes: String? = nil, + appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, + tagIDs: [String]? = nil, tags: [String]? = nil, + addTags: [String]? = nil, areaID: String? = nil, area: String? = nil, items: [Item]? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation, id: id) self.type = "project" self.title = title self.notes = notes + self.prependNotes = prependNotes + self.appendNotes = appendNotes self.when = when self.deadline = deadline + self.tagIDs = tagIDs self.tags = tags + self.addTags = addTags self.areaID = areaID self.area = area self.items = items self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new project configured with same values as the provided project. convenience init(_ project: TJSProject) { - self.init(title: project.title, + self.init(id: project.id, + title: project.title, notes: project.notes, + prependNotes: project.prependNotes, + appendNotes: project.appendNotes, when: project.when, deadline: project.deadline, + tagIDs: project.tagIDs, tags: project.tags, + addTags: project.addTags, areaID: project.areaID, area: project.area, items: project.items, completed: project.completed, - canceled: project.canceled) + canceled: project.canceled, + creationDate: project.creationDate, + completionDate: project.completionDate) } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) do { title = try attributes.decodeIfPresent(String.self, forKey: .title) notes = try attributes.decodeIfPresent(String.self, forKey: .notes) + prependNotes = try attributes.decodeIfPresent(String.self, forKey: .prependNotes) + appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) + tagIDs = try attributes.decodeIfPresent([String].self, forKey: .tagIDs) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) + addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) areaID = try attributes.decodeIfPresent(String.self, forKey: .areaID) area = try attributes.decodeIfPresent(String.self, forKey: .area) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) items = try attributes.decodeIfPresent([Item].self, forKey: .items) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } catch TJSError.invalidType(let expectedType, let errorContext) { throw DecodingError.typeMismatch(expectedType, errorContext) @@ -324,18 +455,24 @@ class TJSProject : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) + try attributes.encodeIfPresent(prependNotes, forKey: .prependNotes) + try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) + try attributes.encodeIfPresent(tagIDs, forKey: .tagIDs) try attributes.encodeIfPresent(tags, forKey: .tags) + try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(areaID, forKey: .areaID) try attributes.encodeIfPresent(area, forKey: .area) try attributes.encodeIfPresent(items, forKey: .items) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } /// A child item of a project. @@ -343,12 +480,12 @@ class TJSProject : TJSModelItem, Codable { /// This is an enum that wraps a TJSTodo or TJSHeading object and handles its encoding /// and decoding to JSON. This is required because there is no way of specifiying a /// strongly typed array that contains more than one type. - enum Item : Codable { + public enum Item : Codable { case todo(TJSTodo) case heading(TJSHeading) /// Creates a new instance by decoding from the given decoder. - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { @@ -356,7 +493,7 @@ class TJSProject : TJSModelItem, Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_) { + catch TJSError.invalidType(expectedType: _, errorContext: _) { // If it's the wrong type, try a heading let heading = try container.decode(TJSHeading.self) self = .heading(heading) @@ -364,7 +501,7 @@ class TJSProject : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { switch self { case .todo(let todo): try todo.encode(to: encoder) @@ -379,45 +516,60 @@ class TJSProject : TJSModelItem, Codable { // MARK: - /// Represents a heading in Things. -class TJSHeading : TJSModelItem, Codable { - var title: String? - var archived: Bool? +public class TJSHeading : TJSModelItem, Codable { + public var title: String? + public var archived: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case archived + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new heading configured with the provided values. - init(title: String? = nil, - archived: Bool? = nil) { + public init(operation: Operation = .create, + title: String? = nil, + archived: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation) self.type = "heading" self.title = title self.archived = archived + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new heading configured with same values as the provided heading. - convenience init(_ heading: TJSHeading) { + public convenience init(_ heading: TJSHeading) { self.init(title: heading.title, - archived: heading.archived) + archived: heading.archived, + creationDate: heading.creationDate, + completionDate: heading.completionDate) } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) title = try attributes.decodeIfPresent(String.self, forKey: .title) archived = try attributes.decodeIfPresent(Bool.self, forKey: .archived) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(archived, forKey: .archived) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -425,52 +577,67 @@ class TJSHeading : TJSModelItem, Codable { // MARK: - /// Represents a checklist item in Things. -class TJSChecklistItem : TJSModelItem, Codable { - var title: String? - var completed: Bool? - var canceled: Bool? +public class TJSChecklistItem : TJSModelItem, Codable { + public var title: String? + public var completed: Bool? + public var canceled: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new checklist item configured with the provided values. - init(title: String? = nil, + public init(operation: Operation = .create, + title: String? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation) self.type = "checklist-item" self.title = title self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new checklist item configured with same values as the provided checklist item. - convenience init (_ checklistItem: TJSChecklistItem) { + public convenience init (_ checklistItem: TJSChecklistItem) { self.init(title: checklistItem.title, completed: checklistItem.completed, - canceled: checklistItem.canceled) + canceled: checklistItem.canceled, + creationDate: checklistItem.creationDate, + completionDate: checklistItem.completionDate) } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) title = try attributes.decodeIfPresent(String.self, forKey: .title) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -480,3 +647,20 @@ class TJSChecklistItem : TJSModelItem, Codable { private enum TJSError : Error { case invalidType(expectedType: Any.Type, errorContext: DecodingError.Context) } + + +// Mark: - Date Formatting + +/// A date encoding strategy to format a date according to ISO8601. +/// +/// Use to with a JSONEncoder to correctly format dates. +public func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { + return .iso8601 +} + +/// A date decoding strategy to format a date according to ISO8601. +/// +/// Use to with a JSONDecoder to correctly format dates. +public func ThingsJSONDateDecodingStrategy() -> JSONDecoder.DateDecodingStrategy { + return .iso8601 +}