mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-19 23:07:33 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
571
mobile/ios/Sync/BookmarkPayload.swift
Normal file
571
mobile/ios/Sync/BookmarkPayload.swift
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import Storage
|
||||
import XCGLogger
|
||||
import SwiftyJSON
|
||||
|
||||
private let log = Logger.syncLogger
|
||||
|
||||
public protocol MirrorItemable {
|
||||
func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem
|
||||
}
|
||||
|
||||
extension BookmarkMirrorItem {
|
||||
func asPayload() -> BookmarkBasePayload {
|
||||
return BookmarkType.somePayloadFromJSON(self.asJSON())
|
||||
}
|
||||
|
||||
func asPayloadWithChildren(_ children: [GUID]?) -> BookmarkBasePayload {
|
||||
let remappedChildren: [GUID]?
|
||||
if let children = children {
|
||||
if BookmarkRoots.RootGUID == self.guid {
|
||||
// Only the root contains roots, and so only its children
|
||||
// need to be translated.
|
||||
remappedChildren = children.map(BookmarkRoots.translateOutgoingRootGUID)
|
||||
} else {
|
||||
remappedChildren = children
|
||||
}
|
||||
} else {
|
||||
remappedChildren = nil
|
||||
}
|
||||
|
||||
let json = self.asJSONWithChildren(remappedChildren)
|
||||
return BookmarkType.somePayloadFromJSON(json)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchy:
|
||||
* - BookmarkBasePayload
|
||||
* \_ FolderPayload
|
||||
* \_ LivemarkPayload
|
||||
* \_ SeparatorPayload
|
||||
* \_ BookmarkPayload
|
||||
* \_ BookmarkQueryPayload
|
||||
*/
|
||||
|
||||
public enum BookmarkType: String {
|
||||
case livemark
|
||||
case separator
|
||||
case folder
|
||||
case bookmark
|
||||
case query
|
||||
case microsummary // Dead: now a bookmark.
|
||||
|
||||
// The result might be invalid, but it won't be nil.
|
||||
public static func somePayloadFromJSON(_ json: JSON) -> BookmarkBasePayload {
|
||||
return payloadFromJSON(json) ?? BookmarkBasePayload(json)
|
||||
}
|
||||
|
||||
public static func payloadFromJSON(_ json: JSON) -> BookmarkBasePayload? {
|
||||
if json["deleted"].bool ?? false {
|
||||
// Deleted records won't have a type.
|
||||
return BookmarkBasePayload(json)
|
||||
}
|
||||
|
||||
guard let typeString = json["type"].string else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let type = BookmarkType(rawValue: typeString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch type {
|
||||
case microsummary:
|
||||
fallthrough
|
||||
case bookmark:
|
||||
return BookmarkPayload(json)
|
||||
case folder:
|
||||
return FolderPayload(json)
|
||||
case livemark:
|
||||
return LivemarkPayload(json)
|
||||
case separator:
|
||||
return SeparatorPayload(json)
|
||||
case query:
|
||||
return BookmarkQueryPayload(json)
|
||||
}
|
||||
}
|
||||
|
||||
public static func isValid(_ type: String?) -> Bool {
|
||||
guard let type = type else {
|
||||
return false
|
||||
}
|
||||
|
||||
return BookmarkType(rawValue: type) != nil
|
||||
}
|
||||
}
|
||||
|
||||
open class LivemarkPayload: BookmarkBasePayload {
|
||||
open var feedURI: String? {
|
||||
return self["feedUri"].string
|
||||
}
|
||||
|
||||
open var siteURI: String? {
|
||||
return self["siteUri"].string
|
||||
}
|
||||
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
return self.hasRequiredStringFields(["feedUri", "siteUri"])
|
||||
}
|
||||
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? LivemarkPayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if self.feedURI != p.feedURI {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.siteURI != p.siteURI {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
if self.deleted {
|
||||
return BookmarkMirrorItem.deleted(.livemark, guid: self.id, modified: modified)
|
||||
}
|
||||
|
||||
return BookmarkMirrorItem.livemark(
|
||||
self.id,
|
||||
dateAdded: self["dateAdded"].uInt64,
|
||||
modified: modified,
|
||||
hasDupe: self.hasDupe,
|
||||
// TODO: these might need to be weakened if real-world data is dirty.
|
||||
parentID: self["parentid"].stringValue,
|
||||
parentName: self["parentName"].string,
|
||||
title: self["title"].string,
|
||||
description: self["description"].string,
|
||||
feedURI: self.feedURI!,
|
||||
siteURI: self.siteURI!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class SeparatorPayload: BookmarkBasePayload {
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
if !self["pos"].isInt() {
|
||||
log.warning("Separator \(self.id) missing pos.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? SeparatorPayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if self["pos"].int != p["pos"].int {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
if self.deleted {
|
||||
return BookmarkMirrorItem.deleted(.separator, guid: self.id, modified: modified)
|
||||
}
|
||||
|
||||
return BookmarkMirrorItem.separator(
|
||||
self.id,
|
||||
dateAdded: self["dateAdded"].uInt64,
|
||||
modified: modified,
|
||||
hasDupe: self.hasDupe,
|
||||
// TODO: these might need to be weakened if real-world data is dirty.
|
||||
parentID: self["parentid"].string!,
|
||||
parentName: self["parentName"].string,
|
||||
pos: self["pos"].int!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class FolderPayload: BookmarkBasePayload {
|
||||
fileprivate var childrenAreValid: Bool {
|
||||
return self.hasStringArrayField("children")
|
||||
}
|
||||
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.hasRequiredStringFields(["title"]) {
|
||||
log.warning("Folder \(self.id) missing title.")
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.hasOptionalStringFields(["description"]) {
|
||||
log.warning("Folder \(self.id) missing string description.")
|
||||
return false
|
||||
|
||||
}
|
||||
if !self.childrenAreValid {
|
||||
log.warning("Folder \(self.id) has invalid children.")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
open var children: [String] {
|
||||
return self["children"].arrayValue.map { $0.string! }
|
||||
}
|
||||
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? FolderPayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if self["title"].string != p["title"].string {
|
||||
return false
|
||||
}
|
||||
|
||||
if self["description"].string != p["description"].string {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.children != p.children {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
if self.deleted {
|
||||
return BookmarkMirrorItem.deleted(.folder, guid: self.id, modified: modified)
|
||||
}
|
||||
|
||||
return BookmarkMirrorItem.folder(
|
||||
self.id,
|
||||
dateAdded: self["dateAdded"].uInt64,
|
||||
modified: modified,
|
||||
hasDupe: self.hasDupe,
|
||||
// TODO: these might need to be weakened if real-world data is dirty.
|
||||
parentID: self["parentid"].string!,
|
||||
parentName: self["parentName"].string,
|
||||
title: self["title"].string!,
|
||||
description: self["description"].string,
|
||||
children: self.children
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class BookmarkPayload: BookmarkBasePayload {
|
||||
fileprivate static let requiredBookmarkStringFields = ["bmkUri"]
|
||||
|
||||
// Title *should* be required, but can be missing for queries. Great.
|
||||
fileprivate static let optionalBookmarkStringFields = ["title", "keyword", "description"]
|
||||
fileprivate static let optionalBookmarkBooleanFields = ["loadInSidebar"]
|
||||
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.hasRequiredStringFields(BookmarkPayload.requiredBookmarkStringFields) {
|
||||
log.warning("Bookmark \(self.id) missing required string field.")
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.hasStringArrayField("tags") {
|
||||
log.warning("Bookmark \(self.id) missing tags array. We'll replace with an empty array.")
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
if !self.hasOptionalStringFields(BookmarkPayload.optionalBookmarkStringFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.hasOptionalBooleanFields(BookmarkPayload.optionalBookmarkBooleanFields)
|
||||
}
|
||||
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? BookmarkPayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if !BookmarkPayload.requiredBookmarkStringFields.every({ p[$0].string! == self[$0].string! }) {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: compare optional fields.
|
||||
|
||||
if Set(self.tags) != Set(p.tags) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self["loadInSidebar"].bool != p["loadInSidebar"].bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
lazy var tags: [String] = {
|
||||
return self["tags"].arrayValue.flatMap { $0.string }
|
||||
}()
|
||||
|
||||
lazy var tagsString: String = {
|
||||
if self["tags"].isArray() {
|
||||
return self["tags"].stringValue() ?? "[]"
|
||||
}
|
||||
return "[]"
|
||||
}()
|
||||
|
||||
override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
if self.deleted {
|
||||
return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified)
|
||||
}
|
||||
|
||||
return BookmarkMirrorItem.bookmark(
|
||||
self.id,
|
||||
dateAdded: self["dateAdded"].uInt64,
|
||||
modified: modified,
|
||||
hasDupe: self.hasDupe,
|
||||
// TODO: these might need to be weakened if real-world data is dirty.
|
||||
parentID: self["parentid"].string!,
|
||||
parentName: self["parentName"].string,
|
||||
title: self["title"].string ?? "",
|
||||
description: self["description"].string,
|
||||
URI: self["bmkUri"].string!,
|
||||
tags: self.tagsString, // Stringify it so we can put the array in the DB.
|
||||
keyword: self["keyword"].string
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class BookmarkQueryPayload: BookmarkPayload {
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
if !self.hasOptionalStringFields(["queryId", "folderName"]) {
|
||||
log.warning("Query \(self.id) missing queryId or folderName.")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? BookmarkQueryPayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if self["folderName"].string != p["folderName"].string {
|
||||
return false
|
||||
}
|
||||
|
||||
if self["queryId"].string != p["queryId"].string {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
if self.deleted {
|
||||
return BookmarkMirrorItem.deleted(.query, guid: self.id, modified: modified)
|
||||
}
|
||||
|
||||
return BookmarkMirrorItem.query(
|
||||
self.id,
|
||||
dateAdded: self["dateAdded"].uInt64,
|
||||
modified: modified,
|
||||
hasDupe: self.hasDupe,
|
||||
parentID: self["parentid"].string!,
|
||||
parentName: self["parentName"].string,
|
||||
title: self["title"].string ?? "",
|
||||
description: self["description"].string,
|
||||
URI: self["bmkUri"].string!,
|
||||
tags: self.tagsString, // Stringify it so we can put the array in the DB.
|
||||
keyword: self["keyword"].string,
|
||||
folderName: self["folderName"].string,
|
||||
queryID: self["queryID"].string
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open class BookmarkBasePayload: CleartextPayloadJSON, MirrorItemable {
|
||||
fileprivate static let requiredStringFields: [String] = ["parentid", "type"]
|
||||
fileprivate static let optionalBooleanFields: [String] = ["hasDupe"]
|
||||
|
||||
static func deletedPayload(_ guid: GUID) -> BookmarkBasePayload {
|
||||
let remappedGUID = BookmarkRoots.translateOutgoingRootGUID(guid)
|
||||
return BookmarkBasePayload(JSON(["id": remappedGUID, "deleted": true]))
|
||||
}
|
||||
|
||||
func hasStringArrayField(_ name: String) -> Bool {
|
||||
guard let arr = self[name].array else {
|
||||
return false
|
||||
}
|
||||
return arr.every { $0.isString() }
|
||||
}
|
||||
|
||||
func hasRequiredStringFields(_ fields: [String]) -> Bool {
|
||||
return fields.every { self[$0].isString() }
|
||||
}
|
||||
|
||||
func hasOptionalStringFields(_ fields: [String]) -> Bool {
|
||||
return fields.every { field in
|
||||
let val = self[field]
|
||||
// Yup, 404 is not found, so this means "string or nothing".
|
||||
let valid = val.isString() || val.isNull() || val.isError()
|
||||
if !valid {
|
||||
log.debug("Field \(field) is invalid: \(val).")
|
||||
}
|
||||
return valid
|
||||
}
|
||||
}
|
||||
|
||||
func hasOptionalBooleanFields(_ fields: [String]) -> Bool {
|
||||
return fields.every { field in
|
||||
let val = self[field]
|
||||
// Yup, 404 is not found, so this means "boolean or nothing".
|
||||
let valid = val.isBool() || val.isNull() || val.error?.code == 404
|
||||
if !valid {
|
||||
log.debug("Field \(field) is invalid: \(val).")
|
||||
}
|
||||
return valid
|
||||
}
|
||||
}
|
||||
|
||||
override open func isValid() -> Bool {
|
||||
if !super.isValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
if self["deleted"].bool ?? false {
|
||||
return true
|
||||
}
|
||||
|
||||
// If not deleted, we must be a specific, known, type!
|
||||
if !BookmarkType.isValid(self["type"].string) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !(self["parentName"].isString() || self.id == "places") {
|
||||
if self["parentid"].string! == "places" {
|
||||
log.debug("Accepting root with missing parent name.")
|
||||
} else {
|
||||
// Bug 1318414.
|
||||
log.warning("Accepting bookmark with missing parent name.")
|
||||
}
|
||||
}
|
||||
|
||||
if !self.hasRequiredStringFields(BookmarkBasePayload.requiredStringFields) {
|
||||
log.warning("Item missing required string field.")
|
||||
return false
|
||||
}
|
||||
|
||||
return self.hasOptionalBooleanFields(BookmarkBasePayload.optionalBooleanFields)
|
||||
}
|
||||
|
||||
open var hasDupe: Bool {
|
||||
return self["hasDupe"].bool ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* This only makes sense for valid payloads.
|
||||
*/
|
||||
override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool {
|
||||
guard let p = obj as? BookmarkBasePayload else {
|
||||
return false
|
||||
}
|
||||
|
||||
if !super.equalPayloads(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.deleted {
|
||||
return true
|
||||
}
|
||||
|
||||
if p.deleted {
|
||||
return self.deleted == p.deleted
|
||||
}
|
||||
|
||||
// If either record is deleted, these other fields might be missing.
|
||||
// But we just checked, so we're good to roll on.
|
||||
|
||||
let same: (String) -> Bool = { field in
|
||||
let left = self[field].string
|
||||
let right = p[field].string
|
||||
return left == right
|
||||
}
|
||||
|
||||
if !BookmarkBasePayload.requiredStringFields.every(same) {
|
||||
return false
|
||||
}
|
||||
|
||||
if p["parentName"].string != self["parentName"].string {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.hasDupe == p.hasDupe
|
||||
}
|
||||
|
||||
// This goes here because extensions cannot override methods yet.
|
||||
open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem {
|
||||
precondition(self.deleted, "Non-deleted items should have a specific type.")
|
||||
return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue